Skip to content

Commit

Permalink
feat!(backend): added in remove/delete project endpoint [2024-12-01]
Browse files Browse the repository at this point in the history
BREAKING CHANGE: added in remove/delete project endpoint
  • Loading branch information
CHRISCARLON committed Dec 1, 2024
1 parent 371b730 commit e646ffa
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 52 deletions.
7 changes: 6 additions & 1 deletion gridwalk-backend/src/core/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,15 @@ impl Project {
Ok(())
}

pub async fn delete_project_record(&self, database: &Arc<dyn Database>) -> Result<()> {
database.delete_project(self).await?;
Ok(())
}

pub async fn get_workspace_projects(
database: &Arc<dyn Database>,
workspace: &Workspace,
) -> Result<Vec<String>> {
) -> Result<Vec<Project>> {
// Get projects from database
database.get_projects(&workspace.id).await
}
Expand Down
3 changes: 2 additions & 1 deletion gridwalk-backend/src/data/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ pub trait UserStore: Send + Sync + 'static {
async fn create_layer(&self, layer: &Layer) -> Result<()>;
async fn create_project(&self, project: &Project) -> Result<()>;
async fn get_workspaces(&self, user: &User) -> Result<Vec<String>>;
async fn get_projects(&self, workspace_id: &str) -> Result<Vec<String>>;
async fn get_projects(&self, workspace_id: &str) -> Result<Vec<Project>>;
async fn delete_project(&self, project: &Project) -> Result<()>;
}

#[async_trait]
Expand Down
31 changes: 22 additions & 9 deletions gridwalk-backend/src/data/dynamodb/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ impl UserStore for Dynamodb {
Ok(workspace_ids)
}

async fn get_projects(&self, workspace_id: &str) -> Result<Vec<String>> {
async fn get_projects(&self, workspace_id: &str) -> Result<Vec<Project>> {
let projects = self
.client
.query()
Expand All @@ -327,19 +327,14 @@ impl UserStore for Dynamodb {
.await
.map_err(|e| anyhow!("Failed to query DynamoDB: {}", e))?;

let project_names: Vec<String> = projects
let projects: Vec<Project> = projects
.items
.unwrap_or_default()
.into_iter()
.filter_map(|item| {
// Just get the name attribute directly
item.get("name")
.and_then(|av| av.as_s().ok())
.map(String::from)
})
.map(|item| item.into())
.collect();

Ok(project_names)
Ok(projects)
}

async fn add_workspace_member(
Expand Down Expand Up @@ -612,6 +607,24 @@ impl UserStore for Dynamodb {
Ok(())
}

async fn delete_project(&self, project: &Project) -> Result<()> {
let mut key = std::collections::HashMap::new();
key.insert(
String::from("PK"),
AV::S(format!("WSP#{}", project.workspace_id)),
);
key.insert(String::from("SK"), AV::S(format!("PROJ#{}", project.id)));

self.client
.delete_item()
.table_name(&self.table_name)
.set_key(Some(key))
.send()
.await?;

Ok(())
}

async fn create_project(&self, project: &Project) -> Result<()> {
let mut item = std::collections::HashMap::new();

Expand Down
25 changes: 24 additions & 1 deletion gridwalk-backend/src/data/dynamodb/conversions.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::core::{
Connection, Email, PostgresConnection, Session, User, Workspace, WorkspaceMember,
Connection, Email, PostgresConnection, Project, Session, User, Workspace, WorkspaceMember,
};
use aws_sdk_dynamodb::types::AttributeValue as AV;
use std::collections::HashMap;
Expand Down Expand Up @@ -126,3 +126,26 @@ impl From<HashMap<String, AV>> for Connection {
}
}
}

impl From<HashMap<String, AV>> for Project {
fn from(value: HashMap<String, AV>) -> Self {
Project {
id: split_at_hash(value.get("SK").unwrap().as_s().unwrap()).to_string(),
workspace_id: split_at_hash(value.get("PK").unwrap().as_s().unwrap()).to_string(),
name: value.get("name").unwrap().as_s().unwrap().to_string(),
uploaded_by: value
.get("uploaded_by")
.unwrap()
.as_s()
.unwrap()
.to_string(),
created_at: value
.get("created_at")
.unwrap()
.as_n()
.unwrap()
.parse()
.unwrap(),
}
}
}
2 changes: 1 addition & 1 deletion gridwalk-backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async fn main() -> Result<()> {
let table_name = env::var("DYNAMODB_TABLE").unwrap_or_else(|_| "gridwalk".to_string());
let app_db = Dynamodb::new(false, &table_name).await.unwrap();
// FOR LOCAL DB DEV
// let app_db = Dynamodb::new(true, "gridwalk").await.unwrap();
// let app_db = Dynamodb::new(true, &table_name).await.unwrap();

// Create GeospatialConfig
let geospatial_config = GeospatialConfig::new();
Expand Down
141 changes: 105 additions & 36 deletions gridwalk-backend/src/routes/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ use crate::core::{CreateProject, Project, User, Workspace, WorkspaceRole};
use axum::{
extract::{Extension, Query, State},
http::StatusCode,
response::IntoResponse,
response::{IntoResponse, Response},
Json,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;

#[derive(Serialize)]
pub struct ErrorResponse {
error: String,
}

#[derive(Debug, Deserialize)]
pub struct ProjectRequest {
workspace_id: String,
Expand Down Expand Up @@ -104,49 +109,113 @@ pub async fn get_projects(
State(state): State<Arc<AppState>>,
Extension(auth_user): Extension<AuthUser>,
Query(query): Query<ProjectRequest>,
) -> impl IntoResponse {
#[derive(Serialize)]
struct ProjectsResponse {
status: String,
data: Option<Vec<String>>,
error: Option<String>,
}

) -> Response {
if let Some(_user) = auth_user.user {
println!("Fetching projects for workspace: {:?}", query.workspace_id);

match state.app_data.get_projects(&query.workspace_id).await {
Ok(project_ids) => {
println!("Found projects: {:?}", project_ids);
(
StatusCode::OK,
Json(ProjectsResponse {
status: "success".to_string(),
data: Some(project_ids),
error: None,
}),
)
Ok(projects) => {
println!("Found projects: {:?}", projects);
Json(projects).into_response()
}
Err(e) => {
println!("Error fetching projects: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ProjectsResponse {
status: "error".to_string(),
data: None,
error: Some("Failed to fetch projects".to_string()),
}),
)
let error = ErrorResponse {
error: "Failed to fetch projects".to_string(),
};
Json(error).into_response()
}
}
} else {
(
StatusCode::UNAUTHORIZED,
Json(ProjectsResponse {
status: "error".to_string(),
data: None,
error: Some("Unauthorized".to_string()),
}),
)
println!("No authenticated user found");
let error = ErrorResponse {
error: "Unauthorized".to_string(),
};
Json(error).into_response()
}
}

#[derive(Debug, Deserialize)]
pub struct DeleteProjectQuery {
workspace_id: String,
project_id: String,
}

pub async fn delete_project(
State(state): State<Arc<AppState>>,
Extension(auth_user): Extension<AuthUser>,
Query(query): Query<DeleteProjectQuery>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
// Ensure user is authenticated
let user = auth_user.user.as_ref().ok_or_else(|| {
let error = json!({
"error": "Unauthorized request",
"details": null
});
(StatusCode::UNAUTHORIZED, Json(error))
})?;

// First validate workspace access and permissions
// This ensures user can't probe for workspace existence without access
let workspace = Workspace::from_id(&state.app_data, &query.workspace_id)
.await
.map_err(|e| {
let error = json!({
"error": "Workspace not found",
"details": e.to_string()
});
(StatusCode::NOT_FOUND, Json(error))
})?;

// Check user's workspace permissions first
let member = workspace
.get_member(&state.app_data, user)
.await
.map_err(|e| {
let error = json!({
"error": "Access forbidden",
"details": e.to_string()
});
(StatusCode::FORBIDDEN, Json(error))
})?;

// Verify write permissions
if member.role == WorkspaceRole::Read {
let error = json!({
"error": "Read-only access",
"details": "User does not have write permission"
});
return Err((StatusCode::FORBIDDEN, Json(error)));
}

// Create a dummy project for deletion
let project = Project {
workspace_id: query.workspace_id.clone(),
id: query.project_id.clone(),
name: String::new(), // These fields aren't needed for deletion
uploaded_by: String::new(), // since we only use workspace_id and id
created_at: 0,
};

// Delete project record from database
project
.delete_project_record(&state.app_data)
.await
.map_err(|e| {
let error = json!({
"error": "Failed to delete project record from Database",
"details": e.to_string()
});
(StatusCode::INTERNAL_SERVER_ERROR, Json(error))
})?;

// Return success response
Ok((
StatusCode::OK,
Json(json!({
"status": "success",
"message": "Project deleted successfully",
"project_id": project.id,
"workspace_id": project.workspace_id
})),
))
}
7 changes: 4 additions & 3 deletions gridwalk-backend/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ use crate::app_state::AppState;
use crate::auth::auth_middleware;
use crate::routes::{
add_workspace_member, create_connection, create_project, create_workspace, delete_connection,
generate_os_token, get_projects, get_workspace_members, get_workspaces, health_check,
list_connections, list_sources, login, logout, profile, register, remove_workspace_member,
tiles, upload_layer,
delete_project, generate_os_token, get_projects, get_workspace_members, get_workspaces,
health_check, list_connections, list_sources, login, logout, profile, register,
remove_workspace_member, tiles, upload_layer,
};
use axum::{
extract::DefaultBodyLimit,
Expand All @@ -30,6 +30,7 @@ pub fn create_app(app_state: AppState) -> Router {

Router::new()
.route("/projects", get(get_projects))
.route("/projects", delete(delete_project))
.route("/workspaces", get(get_workspaces))
.route("/logout", post(logout))
.route("/profile", get(profile))
Expand Down

0 comments on commit e646ffa

Please sign in to comment.