Skip to content

Commit

Permalink
feat: add workspace and connection functions/endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
zerj9 committed Nov 24, 2024
1 parent 2322e39 commit 6d3a8a3
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 30 deletions.
5 changes: 5 additions & 0 deletions gridwalk-backend/src/core/connector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ impl Connection {
let connections = database.get_workspace_connections(workspace_id).await?;
Ok(connections)
}

pub async fn delete(&self, database: &Arc<dyn Database>) -> Result<()> {
database.delete_workspace_connection(self).await
}
}

// Trait for all geospatial data sources
Expand All @@ -62,6 +66,7 @@ pub struct PostgresConnection {
pub database: String,
pub username: String,
pub password: String,
pub schema: Option<String>,
}

#[derive(Clone, Debug)]
Expand Down
22 changes: 21 additions & 1 deletion gridwalk-backend/src/core/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ pub struct RemoveOrgMember {

impl Workspace {
pub async fn from_id(database: &Arc<dyn Database>, id: &str) -> Result<Self> {
Ok(database.get_workspace_by_id(id).await.unwrap())
Ok(database.get_workspace_by_id(id).await?)
}

pub async fn create(database: &Arc<dyn Database>, wsp: &Workspace) -> Result<()> {
Expand Down Expand Up @@ -144,3 +144,23 @@ impl Workspace {
database.get_workspaces(user).await
}
}

impl WorkspaceMember {
pub async fn get(
database: &Arc<dyn Database>,
workspace: &Workspace,
user: &User,
) -> Result<Self> {
Ok(database
.get_workspace_member(workspace, user)
.await
.unwrap())
}

pub fn is_admin(&self) -> bool {
if self.role == WorkspaceRole::Admin {
return true;
}
false
}
}
1 change: 1 addition & 0 deletions gridwalk-backend/src/data/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub trait UserStore: Send + Sync + 'static {
connection_id: &str,
) -> Result<Connection>;
async fn get_workspace_connections(&self, workspace_id: &str) -> Result<Vec<Connection>>;
async fn delete_workspace_connection(&self, con: &Connection) -> Result<()>;
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>>;
Expand Down
22 changes: 20 additions & 2 deletions gridwalk-backend/src/data/dynamodb/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,11 @@ impl UserStore for Dynamodb {
.send()
.await
{
Ok(response) => Ok(response.item.unwrap().into()),
Err(_e) => Err(anyhow!("org not found")),
Ok(response) => response
.item
.ok_or_else(|| anyhow!("workspace not found"))
.map(Into::into),
Err(e) => Err(anyhow!("failed to query workspace: {}", e)),
}
}

Expand Down Expand Up @@ -512,6 +515,9 @@ impl UserStore for Dynamodb {
String::from("pg_password"),
AV::S(con.clone().config.password),
);
if let Some(schema) = &con.config.schema {
item.insert(String::from("pg_schema"), AV::S(schema.to_string()));
}

self.client
.put_item()
Expand Down Expand Up @@ -566,6 +572,18 @@ impl UserStore for Dynamodb {
}
}

async fn delete_workspace_connection(&self, con: &Connection) -> Result<()> {
self.client
.delete_item()
.table_name(&self.table_name)
.key("PK", AV::S(format!("WSP#{}", con.workspace_id)))
.key("SK", AV::S(format!("CON#{}", con.id)))
.send()
.await?;

Ok(())
}

async fn create_layer(&self, layer: &Layer) -> Result<()> {
let mut item = std::collections::HashMap::new();

Expand Down
4 changes: 4 additions & 0 deletions gridwalk-backend/src/data/dynamodb/conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ impl From<HashMap<String, AV>> for Connection {
database: value.get("pg_db").unwrap().as_s().unwrap().into(),
username: value.get("pg_username").unwrap().as_s().unwrap().into(),
password: value.get("pg_password").unwrap().as_s().unwrap().into(),
schema: value
.get("pg_schema")
.and_then(|v| v.as_s().ok())
.map(Into::into),
},
}
}
Expand Down
138 changes: 115 additions & 23 deletions gridwalk-backend/src/routes/connector.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use crate::app_state::AppState;
use crate::auth::AuthUser;
use crate::core::{Connection, PostgisConfig, PostgresConnection, User, Workspace};
use crate::core::{
Connection, PostgisConfig, PostgresConnection, User, Workspace, WorkspaceMember,
};
use axum::{
extract::{Extension, Path, State},
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;

Expand All @@ -32,30 +34,104 @@ impl Connection {
}
}

// TODO: Check workspace role: only allow admin to create connection
pub async fn create_connection(
State(state): State<Arc<AppState>>,
Extension(auth_user): Extension<AuthUser>,
Json(req): Json<CreateConnectionRequest>,
) -> impl IntoResponse {
if let Some(user) = auth_user.user {
let workspace = match Workspace::from_id(&state.app_data, &req.workspace_id).await {
Ok(workspace) => workspace,
Err(_) => return (StatusCode::NOT_FOUND, "".to_string()),
};

// Check if the requesting user is a member of the workspace
let member = match WorkspaceMember::get(&state.app_data, &workspace, &user).await {
Ok(member) => member,
Err(_) => return (StatusCode::NOT_FOUND, "".to_string()),
};

// Only allow admins to create connections
if !member.is_admin() {
return (StatusCode::FORBIDDEN, "".to_string());
}

let connection_info = Connection::from_req(req, user);
match connection_info.clone().create_record(&state.app_data).await {
Ok(_) => {
println!("Created connection record");
let connections = state
.app_data
.clone()
.get_workspace_connections(&connection_info.workspace_id)
.await;
println!("{:?}", connections);
(StatusCode::OK, "connection creation submitted".to_string())
}
Err(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"connection creation failed".to_string(),
),
}
} else {
(StatusCode::FORBIDDEN, "".to_string())
}
}

pub async fn delete_connection(
State(state): State<Arc<AppState>>,
Extension(auth_user): Extension<AuthUser>,
Path((workspace_id, connection_id)): Path<(String, String)>,
) -> Response {
let connection_info = Connection::from_req(req, auth_user.user.unwrap());

match connection_info.clone().create_record(&state.app_data).await {
Ok(_) => {
println!("Created connection record");
let connections = state
.app_data
.clone()
.get_workspace_connections(&connection_info.workspace_id)
.await;
println!("{:?}", connections);
(StatusCode::OK, "connection creation submitted").into_response()
if let Some(req_user) = auth_user.user {
// Get the workspace
let workspace = match Workspace::from_id(&state.app_data, &workspace_id).await {
Ok(ws) => ws,
Err(_) => return "workspace not found".into_response(),
};

// Check permissions
match WorkspaceMember::get(&state.app_data, &workspace, &req_user).await {
Ok(mem) => {
if !mem.is_admin() {
return "permission denied".into_response();
};
}
Err(_) => return "member not found".into_response(),
};

let con = match Connection::from_id(&state.app_data, &workspace_id, &connection_id).await {
Ok(c) => c,
Err(_) => return "connection not found".into_response(),
};

match con.delete(&state.app_data).await {
Ok(_) => "".into_response(),
Err(_) => "delete failed".into_response(),
}
} else {
"unauthorized".into_response()
}
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ConnectionResponse {
pub id: String,
pub workspace_id: String,
pub name: String,
pub created_by: String,
pub connector_type: String,
}

impl From<Connection> for ConnectionResponse {
fn from(con: Connection) -> Self {
ConnectionResponse {
id: con.id,
workspace_id: con.workspace_id,
name: con.name,
created_by: con.created_by,
connector_type: con.connector_type,
}
Err(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"connection creation failed",
)
.into_response(),
}
}

Expand All @@ -66,13 +142,29 @@ pub async fn list_connections(
) -> impl IntoResponse {
match auth_user.user {
// TODO: Check permissions
Some(_user) => {
println!("");
Some(user) => {
let workspace = Workspace::from_id(&state.app_data, &workspace_id)
.await
.map_err(|_| (StatusCode::NOT_FOUND, "".to_string()))?;

// Check if the requesting user is a member of the workspace
WorkspaceMember::get(&state.app_data, &workspace, &user)
.await
.map_err(|_| (StatusCode::FORBIDDEN, "unauthorized".to_string()))?;

let connections = Connection::get_all(&state.app_data, &workspace_id)
.await
.ok()
.unwrap();
Ok(Json(connections))

// Convert Vec<Connection> to Vec<ConnectionResponse>
// Removes the config from the response
let connection_responses: Vec<ConnectionResponse> = connections
.into_iter()
.map(ConnectionResponse::from)
.collect();

Ok(Json(connection_responses))
}
None => Err((StatusCode::FORBIDDEN, "unauthorized".to_string())),
}
Expand Down
13 changes: 9 additions & 4 deletions gridwalk-backend/src/server.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use crate::app_state::AppState;
use crate::auth::auth_middleware;
use crate::routes::{
add_workspace_member, create_connection, create_project, create_workspace, 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,
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,
};
use axum::{
extract::DefaultBodyLimit,
Expand Down Expand Up @@ -48,9 +49,13 @@ pub fn create_app(app_state: AppState) -> Router {
get(list_sources),
)
.route(
"/workspace/:workspace_id/connections",
"/workspaces/:workspace_id/connections",
get(list_connections),
)
.route(
"/workspaces/:workspace_id/connections/:connection_id",
delete(delete_connection),
)
.route("/create_project", post(create_project))
.route("/upload_layer", post(upload_layer))
.layer(DefaultBodyLimit::disable())
Expand Down

0 comments on commit 6d3a8a3

Please sign in to comment.