diff --git a/database/bindings/PrivilegeLevel.ts b/database/bindings/PrivilegeLevel.ts new file mode 100644 index 00000000..3ea85174 --- /dev/null +++ b/database/bindings/PrivilegeLevel.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PrivilegeLevel = "Read" | "Edit" | "Admin"; \ No newline at end of file diff --git a/database/migrations/0004_registered_apps.sql b/database/migrations/0004_registered_apps.sql index 45b4eb9d..dd741ac7 100644 --- a/database/migrations/0004_registered_apps.sql +++ b/database/migrations/0004_registered_apps.sql @@ -4,9 +4,7 @@ CREATE TABLE registered_apps( app_name TEXT NOT NULL, whitelisted_domains TEXT [] NOT NULL, ack_public_keys TEXT [] NOT NULL, - email TEXT, - registration_timestamp TIMESTAMPTZ NOT NULL, - pass_hash TEXT + registration_timestamp TIMESTAMPTZ NOT NULL ); CREATE UNIQUE INDEX app_id_idx ON registered_apps(app_id); \ No newline at end of file diff --git a/database/src/aggregated_views_queries/requests_stats.rs b/database/src/aggregated_views_queries/requests_stats.rs index 7f15b879..d98c26e6 100644 --- a/database/src/aggregated_views_queries/requests_stats.rs +++ b/database/src/aggregated_views_queries/requests_stats.rs @@ -315,11 +315,8 @@ mod test { app_id: second_app_id.to_string(), app_name: "test_app_name".to_string(), whitelisted_domains: vec!["test_domain".to_string()], - subscription: None, ack_public_keys: vec!["test_key".to_string()], - email: None, registration_timestamp: to_microsecond_precision(&Utc::now()), - pass_hash: None, }; db_arc.register_new_app(&app).await.unwrap(); diff --git a/database/src/structs/privilege_level.rs b/database/src/structs/privilege_level.rs index 3fe5706c..87b97e1a 100644 --- a/database/src/structs/privilege_level.rs +++ b/database/src/structs/privilege_level.rs @@ -1,6 +1,9 @@ +use serde::{Deserialize, Serialize}; use sqlx::Type; +use ts_rs::TS; -#[derive(Debug, Clone, Eq, PartialEq, Type)] +#[derive(Debug, Clone, Eq, PartialEq, Type, Serialize, Deserialize, TS)] +#[ts(export)] #[sqlx(type_name = "privilege_level_enum")] pub enum PrivilegeLevel { Read, diff --git a/database/src/tables/registered_app/table_struct.rs b/database/src/tables/registered_app/table_struct.rs index 5f9cfe7e..528671c3 100644 --- a/database/src/tables/registered_app/table_struct.rs +++ b/database/src/tables/registered_app/table_struct.rs @@ -1,4 +1,3 @@ -use crate::structs::subscription::Subscription; use sqlx::{ postgres::PgRow, types::chrono::{DateTime, Utc}, @@ -6,7 +5,8 @@ use sqlx::{ }; pub const REGISTERED_APPS_TABLE_NAME: &str = "registered_apps"; -pub const REGISTERED_APPS_KEYS: &str = "team_id, app_id, app_name, whitelisted_domains, ack_public_keys, email, registration_timestamp, pass_hash"; +pub const REGISTERED_APPS_KEYS: &str = + "team_id, app_id, app_name, whitelisted_domains, ack_public_keys, registration_timestamp"; #[derive(Clone, Debug, Eq, PartialEq)] pub struct DbRegisteredApp { @@ -14,11 +14,8 @@ pub struct DbRegisteredApp { pub app_id: String, pub app_name: String, pub whitelisted_domains: Vec, - pub subscription: Option, pub ack_public_keys: Vec, - pub email: Option, pub registration_timestamp: DateTime, - pub pass_hash: Option, } impl FromRow<'_, PgRow> for DbRegisteredApp { @@ -28,12 +25,8 @@ impl FromRow<'_, PgRow> for DbRegisteredApp { app_id: row.get("app_id"), app_name: row.get("app_name"), whitelisted_domains: row.get("whitelisted_domains"), - // TEMP - subscription: None, ack_public_keys: row.get("ack_public_keys"), - email: row.get("email"), registration_timestamp: row.get("registration_timestamp"), - pass_hash: row.get("pass_hash"), }) } } diff --git a/database/src/tables/registered_app/update.rs b/database/src/tables/registered_app/update.rs index 3b9f5707..4545cd02 100644 --- a/database/src/tables/registered_app/update.rs +++ b/database/src/tables/registered_app/update.rs @@ -5,7 +5,7 @@ use sqlx::{query, Transaction}; impl Db { pub async fn register_new_app(&self, app: &DbRegisteredApp) -> Result<(), DbError> { let query_body = format!( - "INSERT INTO {REGISTERED_APPS_TABLE_NAME} ({REGISTERED_APPS_KEYS}) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)" + "INSERT INTO {REGISTERED_APPS_TABLE_NAME} ({REGISTERED_APPS_KEYS}) VALUES ($1, $2, $3, $4, $5, $6)" ); let query_result = query(&query_body) @@ -14,9 +14,7 @@ impl Db { .bind(&app.app_name) .bind(&app.whitelisted_domains) .bind(&app.ack_public_keys) - .bind(&app.email) .bind(&app.registration_timestamp) - .bind(&app.pass_hash) .execute(&self.connection_pool) .await; @@ -32,7 +30,7 @@ impl Db { app: &DbRegisteredApp, ) -> Result<(), DbError> { let query_body = format!( - "INSERT INTO {REGISTERED_APPS_TABLE_NAME} ({}) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + "INSERT INTO {REGISTERED_APPS_TABLE_NAME} ({}) VALUES ($1, $2, $3, $4, $5, $6)", REGISTERED_APPS_KEYS ); @@ -42,9 +40,7 @@ impl Db { .bind(&app.app_name) .bind(&app.whitelisted_domains) .bind(&app.ack_public_keys) - .bind(&app.email) .bind(&app.registration_timestamp) - .bind(&app.pass_hash) .execute(&mut **tx) .await; diff --git a/database/src/tables/test_utils.rs b/database/src/tables/test_utils.rs index 7106abe4..55e46514 100644 --- a/database/src/tables/test_utils.rs +++ b/database/src/tables/test_utils.rs @@ -104,10 +104,7 @@ pub mod test_utils { app_id: app_id.clone(), app_name: "test_app".to_string(), whitelisted_domains: vec!["localhost".to_string()], - subscription: None, ack_public_keys: vec!["key".to_string()], - email: None, - pass_hash: None, registration_timestamp: registration_timestamp, }; diff --git a/database/src/tables/user_app_privileges/select.rs b/database/src/tables/user_app_privileges/select.rs index ba28cf42..2c5728ca 100644 --- a/database/src/tables/user_app_privileges/select.rs +++ b/database/src/tables/user_app_privileges/select.rs @@ -1,9 +1,17 @@ use super::table_struct::UserAppPrivilege; -use crate::db::Db; -use crate::structs::db_error::DbError; -use crate::tables::registered_app::table_struct::REGISTERED_APPS_TABLE_NAME; -use crate::tables::user_app_privileges::table_struct::USER_APP_PRIVILEGES_TABLE_NAME; -use sqlx::query_as; +use crate::{ + db::Db, + structs::db_error::DbError, + tables::{ + grafana_users::table_struct::GRAFANA_USERS_TABLE_NAME, + registered_app::table_struct::{DbRegisteredApp, REGISTERED_APPS_TABLE_NAME}, + team::table_struct::{Team, TEAM_TABLE_NAME}, + user_app_privileges::table_struct::USER_APP_PRIVILEGES_TABLE_NAME, + }, +}; +use sqlx::{query_as, types::chrono::DateTime}; +use sqlx::{types::chrono::Utc, Row}; +use std::collections::HashMap; impl Db { pub async fn get_privilege_by_user_id_and_app_id( @@ -89,4 +97,109 @@ impl Db { .await .map_err(|e| e.into()); } + + pub async fn get_joined_teams_by_user_id( + &self, + user_id: &String, + ) -> Result< + Vec<( + Team, + String, + DateTime, + Vec<(DbRegisteredApp, UserAppPrivilege)>, + )>, + DbError, + > { + let query = format!( + "WITH RelevantTeams AS ( + SELECT DISTINCT t.team_id, t.team_name, t.personal, t.subscription, + t.registration_timestamp, gu.email AS team_admin_email, + gu.user_id AS team_admin_id, + CASE + WHEN t.team_admin_id = $1 THEN t.registration_timestamp + ELSE MAX(uap.creation_timestamp) OVER (PARTITION BY t.team_id) + END as user_joined_team_timestamp + FROM {TEAM_TABLE_NAME} t + LEFT JOIN {REGISTERED_APPS_TABLE_NAME} ra ON t.team_id = ra.team_id + LEFT JOIN {USER_APP_PRIVILEGES_TABLE_NAME} uap ON ra.app_id = uap.app_id AND uap.user_id = $1 + JOIN {GRAFANA_USERS_TABLE_NAME} gu ON t.team_admin_id = gu.user_id + WHERE t.team_admin_id = $1 OR uap.user_id = $1 + ) + SELECT rt.team_id, rt.team_name, rt.personal, rt.subscription, rt.registration_timestamp, + rt.team_admin_email, rt.team_admin_id, ra.app_id, ra.app_name, ra.whitelisted_domains, + ra.ack_public_keys, ra.registration_timestamp AS app_registration_timestamp, + uap.user_id, uap.privilege_level, uap.creation_timestamp AS privilege_creation_timestamp, + rt.user_joined_team_timestamp + FROM RelevantTeams rt + LEFT JOIN {REGISTERED_APPS_TABLE_NAME} ra ON rt.team_id = ra.team_id + LEFT JOIN {USER_APP_PRIVILEGES_TABLE_NAME} uap ON ra.app_id = uap.app_id AND uap.user_id = $1 + ORDER BY rt.team_id, ra.app_id" + ); + + let rows = sqlx::query(&query) + .bind(user_id) + .fetch_all(&self.connection_pool) + .await; + + let rows = match rows { + Ok(rows) => rows, + Err(err) => { + return Err(err.into()); + } + }; + + let mut team_app_map: HashMap< + String, + ( + Team, + String, + DateTime, + Vec<(DbRegisteredApp, UserAppPrivilege)>, + ), + > = HashMap::new(); + + for row in rows { + let team = Team { + team_id: row.get("team_id"), + personal: row.get("personal"), + team_name: row.get("team_name"), + subscription: row.get("subscription"), + registration_timestamp: row.get("registration_timestamp"), + team_admin_id: row.get("team_admin_id"), + }; + + let admin_email = row.get("team_admin_email"); + let user_joined_team_timestamp: DateTime = row.get("user_joined_team_timestamp"); + + let team_id = team.team_id.clone(); + let team_entry = team_app_map + .entry(team.team_id.clone()) + .or_insert_with(|| (team, admin_email, user_joined_team_timestamp, Vec::new())); + + if let Ok(app_id) = row.try_get("app_id") { + if app_id != "" { + // Checking if app_id is present and not an empty string + let app = DbRegisteredApp { + team_id: team_id.clone(), + app_id, + app_name: row.get("app_name"), + whitelisted_domains: row.get("whitelisted_domains"), + ack_public_keys: row.get("ack_public_keys"), + registration_timestamp: row.get("app_registration_timestamp"), + }; + + let privilege = UserAppPrivilege { + user_id: row.get("user_id"), + app_id: app.app_id.clone(), + privilege_level: row.get("privilege_level"), + creation_timestamp: row.get("privilege_creation_timestamp"), + }; + + team_entry.3.push((app, privilege)); + } + } + } + + Ok(team_app_map.into_values().collect()) + } } diff --git a/database/src/tables/user_app_privileges/update.rs b/database/src/tables/user_app_privileges/update.rs index e069ae6b..6724a985 100644 --- a/database/src/tables/user_app_privileges/update.rs +++ b/database/src/tables/user_app_privileges/update.rs @@ -148,6 +148,9 @@ impl Db { } } + // Commit the transaction + tx.commit().await?; + Ok(()) } @@ -284,10 +287,7 @@ mod tests { app_id: app_id.clone(), app_name: format!("test_app_name_{}", i), team_id: team_id.clone(), - email: None, - pass_hash: None, registration_timestamp: to_microsecond_precision(&Utc::now()), - subscription: None, }; db.register_new_app(&app).await.unwrap(); @@ -323,10 +323,7 @@ mod tests { app_id: app_id.clone(), app_name: format!("test_app_name_{}", i), team_id: team_id.clone(), - email: None, - pass_hash: None, registration_timestamp: to_microsecond_precision(&Utc::now()), - subscription: None, }; db.register_new_app(&app).await.unwrap(); diff --git a/server/bindings/AppInfo.ts b/server/bindings/AppInfo.ts new file mode 100644 index 00000000..93723b88 --- /dev/null +++ b/server/bindings/AppInfo.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface AppInfo { teamId: string, appId: string, appName: string, registeredAt: string, whitelistedDomains: Array, ackPublicKeys: Array, } \ No newline at end of file diff --git a/server/bindings/HttpCloudEndpoint.ts b/server/bindings/HttpCloudEndpoint.ts index bf727a80..ad92decb 100644 --- a/server/bindings/HttpCloudEndpoint.ts +++ b/server/bindings/HttpCloudEndpoint.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type HttpCloudEndpoint = "/register_new_app" | "/register_with_password" | "/login_with_password" | "/register_new_team" | "/add_user_to_team" | "/remove_user_from_team"; \ No newline at end of file +export type HttpCloudEndpoint = "/register_new_app" | "/register_with_password" | "/login_with_password" | "/register_new_team" | "/add_user_to_team" | "/remove_user_from_team" | "/get_user_joined_teams"; \ No newline at end of file diff --git a/server/bindings/HttpGetUserJoinedTeamsResponse.ts b/server/bindings/HttpGetUserJoinedTeamsResponse.ts new file mode 100644 index 00000000..65783cf5 --- /dev/null +++ b/server/bindings/HttpGetUserJoinedTeamsResponse.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AppInfo } from "./AppInfo"; +import type { JoinedTeam } from "./JoinedTeam"; +import type { UserPrivilege } from "./UserPrivilege"; + +export interface HttpGetUserJoinedTeamsResponse { teams: Record, teams_apps: Record>, user_privileges: Record>, } \ No newline at end of file diff --git a/server/bindings/JoinedTeam.ts b/server/bindings/JoinedTeam.ts new file mode 100644 index 00000000..fa7345c0 --- /dev/null +++ b/server/bindings/JoinedTeam.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface JoinedTeam { teamId: string, teamName: string, creatorEmail: string, createdAt: string, joinedAt: string, personal: boolean, } \ No newline at end of file diff --git a/server/bindings/UserPrivilege.ts b/server/bindings/UserPrivilege.ts new file mode 100644 index 00000000..c81dade8 --- /dev/null +++ b/server/bindings/UserPrivilege.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PrivilegeLevel } from "./PrivilegeLevel"; + +export interface UserPrivilege { appId: string, grantedAt: string, privilege: PrivilegeLevel, } \ No newline at end of file diff --git a/server/src/bin/nightly-connect-server.rs b/server/src/bin/nightly-connect-server.rs index 1a0e1e5f..60b91f5a 100644 --- a/server/src/bin/nightly-connect-server.rs +++ b/server/src/bin/nightly-connect-server.rs @@ -6,7 +6,7 @@ use tracing_subscriber::EnvFilter; #[tokio::main] async fn main() { - let filter: EnvFilter = "debug,tower_http=trace,hyper=warn" + let filter: EnvFilter = "debug,sqlx=warn,tower_http=trace,hyper=warn" .parse() .expect("filter should parse"); diff --git a/server/src/http/cloud/add_user_to_team.rs b/server/src/http/cloud/add_user_to_team.rs index 634d5bc8..97a1ef4f 100644 --- a/server/src/http/cloud/add_user_to_team.rs +++ b/server/src/http/cloud/add_user_to_team.rs @@ -156,7 +156,10 @@ pub async fn add_user_to_team( } } Ok(None) => { - return Err((StatusCode::BAD_REQUEST, "Team does not exist".to_string())); + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::TeamDoesNotExist.to_string(), + )); } Err(err) => { error!("Failed to get team: {:?}", err); @@ -167,3 +170,272 @@ pub async fn add_user_to_team( } } } + +#[cfg(test)] +mod tests { + use crate::{ + env::JWT_SECRET, + http::cloud::{ + add_user_to_team::{HttpAddUserToTeamRequest, HttpAddUserToTeamResponse}, + register_new_app::HttpRegisterNewAppRequest, + }, + statics::USERS_AMOUNT_LIMIT_PER_TEAM, + structs::{api_cloud_errors::CloudApiErrors, cloud_http_endpoints::HttpCloudEndpoint}, + test_utils::test_utils::{ + add_test_app, add_test_team, add_user_to_test_team, convert_response, create_test_app, + generate_valid_name, register_and_login_random_user, + }, + }; + use axum::{ + body::Body, + extract::ConnectInfo, + http::{Method, Request}, + }; + use std::net::SocketAddr; + use tower::ServiceExt; + + #[tokio::test] + async fn test_add_user_to_team() { + let test_app = create_test_app(false).await; + + let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; + + // Register new team + let team_name = generate_valid_name(); + let team_id = add_test_team(&team_name, &auth_token, &test_app, false) + .await + .unwrap(); + + // Register app under the team + let app_name = generate_valid_name(); + let request = HttpRegisterNewAppRequest { + team_id: team_id.clone(), + app_name: app_name.clone(), + whitelisted_domains: vec![], + ack_public_keys: vec![], + }; + + // unwrap err as it should have failed + let _ = add_test_app(&request, &auth_token, &test_app) + .await + .unwrap(); + + // Register new user + let (_test_user_auth_token, test_user_email, _test_user_password) = + register_and_login_random_user(&test_app).await; + + // Add user to the team + let request = HttpAddUserToTeamRequest { + team_id: team_id.clone(), + user_email: test_user_email.clone(), + }; + + let ip: ConnectInfo = ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080))); + let json = serde_json::to_string(&request).unwrap(); + let auth = auth_token.encode(JWT_SECRET()).unwrap(); + + let req = Request::builder() + .method(Method::POST) + .header("content-type", "application/json") + .header("authorization", format!("Bearer {auth}")) + .uri(format!( + "/cloud/private{}", + HttpCloudEndpoint::AddUserToTeam.to_string() + )) + .extension(ip.clone()) + .body(Body::from(json)) + .unwrap(); + + // Send request + let response = test_app.clone().oneshot(req).await.unwrap(); + // Validate response + convert_response::(response) + .await + .unwrap(); + + // Try to add user to the team again, should fail as user is already in the team + let request = HttpAddUserToTeamRequest { + team_id: team_id.clone(), + user_email: test_user_email.clone(), + }; + + let json = serde_json::to_string(&request).unwrap(); + let auth = auth_token.encode(JWT_SECRET()).unwrap(); + + let req = Request::builder() + .method(Method::POST) + .header("content-type", "application/json") + .header("authorization", format!("Bearer {auth}")) + .uri(format!( + "/cloud/private{}", + HttpCloudEndpoint::AddUserToTeam.to_string() + )) + .extension(ip) + .body(Body::from(json)) + .unwrap(); + + // Send request + let response = test_app.clone().oneshot(req).await.unwrap(); + // Validate response + let err = convert_response::(response) + .await + .unwrap_err(); + + assert_eq!( + err.to_string(), + CloudApiErrors::UserAlreadyBelongsToTheTeam.to_string() + ); + } + + #[tokio::test] + async fn test_add_user_to_team_team_not_found() { + let test_app = create_test_app(false).await; + + let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; + + // Team does not exist, use random uuid + let resp = add_user_to_test_team( + &uuid7::uuid7().to_string(), + &"test_user_email@gmail.com".to_string(), + &auth_token, + &test_app, + ) + .await + .unwrap_err(); + + assert_eq!( + resp.to_string(), + CloudApiErrors::TeamDoesNotExist.to_string() + ); + } + + #[tokio::test] + async fn test_add_user_to_team_no_registered_apps() { + let test_app = create_test_app(false).await; + + let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; + + // Register new team + let team_name = generate_valid_name(); + let team_id = add_test_team(&team_name, &auth_token, &test_app, false) + .await + .unwrap(); + + // Team does not exist, use random uuid + let resp = add_user_to_test_team( + &team_id.to_string(), + &"test_user_email@gmail.com".to_string(), + &auth_token, + &test_app, + ) + .await + .unwrap_err(); + + assert_eq!( + resp.to_string(), + CloudApiErrors::TeamHasNoRegisteredApps.to_string() + ); + } + + #[tokio::test] + async fn test_add_user_to_team_user_limit_reached() { + let test_app = create_test_app(false).await; + + let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; + + // Register new team + let team_name = generate_valid_name(); + let team_id = add_test_team(&team_name, &auth_token, &test_app, false) + .await + .unwrap(); + + // Register app under the team + let app_name = generate_valid_name(); + let request = HttpRegisterNewAppRequest { + team_id: team_id.clone(), + app_name: app_name.clone(), + whitelisted_domains: vec![], + ack_public_keys: vec![], + }; + + let _ = add_test_app(&request, &auth_token, &test_app) + .await + .unwrap(); + + // Add [USERS_AMOUNT_LIMIT_PER_TEAM] users to the team + for _ in 1..USERS_AMOUNT_LIMIT_PER_TEAM { + let (_, test_user_email, _) = register_and_login_random_user(&test_app).await; + + // Add user to the team + add_user_to_test_team( + &team_id.to_string(), + &test_user_email.to_string(), + &auth_token, + &test_app, + ) + .await + .unwrap(); + } + + // Try to add another user to the team, should fail as user limit has been reached + let (_, test_user_email, _) = register_and_login_random_user(&test_app).await; + + // Add user to the team + let resp = add_user_to_test_team( + &team_id.to_string(), + &test_user_email.to_string(), + &auth_token, + &test_app, + ) + .await + .unwrap_err(); + + assert_eq!( + resp.to_string(), + CloudApiErrors::MaximumUsersPerTeamReached.to_string() + ); + } + + #[tokio::test] + async fn test_add_user_to_team_user_does_not_exist() { + let test_app = create_test_app(false).await; + + let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; + + // Register new team + let team_name = generate_valid_name(); + let team_id = add_test_team(&team_name, &auth_token, &test_app, false) + .await + .unwrap(); + + // Register app under the team + let app_name = generate_valid_name(); + let request = HttpRegisterNewAppRequest { + team_id: team_id.clone(), + app_name: app_name.clone(), + whitelisted_domains: vec![], + ack_public_keys: vec![], + }; + + let _ = add_test_app(&request, &auth_token, &test_app) + .await + .unwrap(); + + // Try to add non-existing user to the team, should fail as user limit has been reached + + // Add user to the team + let resp = add_user_to_test_team( + &team_id.to_string(), + &"non-existing-user@gmail.com".to_string(), + &auth_token, + &test_app, + ) + .await + .unwrap_err(); + + assert_eq!( + resp.to_string(), + CloudApiErrors::UserDoesNotExist.to_string() + ); + } +} diff --git a/server/src/http/cloud/get_user_joined_teams.rs b/server/src/http/cloud/get_user_joined_teams.rs new file mode 100644 index 00000000..d4b1f318 --- /dev/null +++ b/server/src/http/cloud/get_user_joined_teams.rs @@ -0,0 +1,313 @@ +use crate::{ + auth::auth_middleware::UserId, + state::AppId, + structs::{ + api_cloud_errors::CloudApiErrors, + app_info::AppInfo, + joined_team::{JoinedTeam, TeamId}, + user_privilege::UserPrivilege, + }, +}; +use axum::{extract::State, http::StatusCode, Extension, Json}; +use database::db::Db; +use log::error; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, sync::Arc}; +use ts_rs::TS; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct HttpGetUserJoinedTeamsResponse { + pub teams: HashMap, + pub teams_apps: HashMap>, + pub user_privileges: HashMap>, +} + +pub async fn get_user_joined_teams( + State(db): State>>, + Extension(user_id): Extension, +) -> Result, (StatusCode, String)> { + // Db connection has already been checked in the middleware + let db = db.as_ref().ok_or(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::CloudFeatureDisabled.to_string(), + ))?; + + // Check if user already belongs to the team + match db.get_joined_teams_by_user_id(&user_id).await { + Ok(joined_teams) => { + let mut teams = HashMap::new(); + let mut teams_apps = HashMap::new(); + let mut user_privileges = HashMap::new(); + + for (team, admin_email, joined_timestamp, registered_apps) in joined_teams { + let team_id = team.team_id.clone(); + + // Parse joined team + let joined_team = JoinedTeam { + team_id: team.team_id.clone(), + team_name: team.team_name, + created_at: team.registration_timestamp, + creator_email: admin_email, + personal: team.personal, + joined_at: joined_timestamp, + }; + teams.insert(team_id.clone(), joined_team); + + // Parse teams apps and user privileges + let mut apps_info = Vec::new(); + let mut privileges = HashMap::new(); + + for (app, privilege) in registered_apps { + let app_info: AppInfo = app.into(); + let privilege: UserPrivilege = privilege.into(); + + privileges.insert(app_info.app_id.clone(), privilege); + apps_info.push(app_info); + } + + if !apps_info.is_empty() { + teams_apps.insert(team_id.clone(), apps_info); + } + + if !privileges.is_empty() { + user_privileges.insert(team_id.clone(), privileges); + } + } + + Ok(Json(HttpGetUserJoinedTeamsResponse { + teams, + teams_apps, + user_privileges, + })) + } + Err(err) => { + error!( + "Failed to get teams and apps membership by user id: {:?}", + err + ); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + } +} + +#[cfg(test)] +mod tests { + use crate::test_utils::test_utils::{ + add_user_to_test_team, generate_valid_name, get_test_user_joined_teams, + }; + use crate::{ + env::JWT_SECRET, + http::cloud::register_new_app::HttpRegisterNewAppRequest, + structs::cloud_http_endpoints::HttpCloudEndpoint, + test_utils::test_utils::{ + add_test_app, add_test_team, convert_response, create_test_app, + register_and_login_random_user, + }, + }; + use axum::{ + body::Body, + extract::ConnectInfo, + http::{Method, Request}, + }; + use database::tables::utils::get_current_datetime; + use std::net::SocketAddr; + use tower::ServiceExt; + + #[tokio::test] + async fn test_get_user_joined_teams() { + let test_app = create_test_app(false).await; + + let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; + + let num_of_teams = 4; + let mut team_ids = Vec::new(); + + // Create teams + for _ in 0..num_of_teams { + let team_name = generate_valid_name(); + let team_id = add_test_team(&team_name, &auth_token, &test_app, false) + .await + .unwrap(); + team_ids.push(team_id); + } + + let mut app_ids = Vec::new(); + // Register 3 + [team index] apps for each team + for (j, team_id) in team_ids.iter().enumerate() { + let mut team_app_ids = Vec::new(); + for _ in 0..3 + j { + let app_name = generate_valid_name(); + let request = HttpRegisterNewAppRequest { + team_id: team_id.clone(), + app_name: app_name.clone(), + whitelisted_domains: vec![], + ack_public_keys: vec![], + }; + let app_id = add_test_app(&request, &auth_token, &test_app) + .await + .unwrap(); + team_app_ids.push(app_id); + } + + app_ids.push(team_app_ids); + } + + // Register new user + let (app_user_auth_token, app_user_email, _app_user_password) = + register_and_login_random_user(&test_app).await; + + // Add user to first team + let before_first_join = get_current_datetime(); + add_user_to_test_team(&team_ids[0], &app_user_email, &auth_token, &test_app) + .await + .unwrap(); + let after_first_join = get_current_datetime(); + + // Wait for 1 second + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + // Add user to second team + add_user_to_test_team(&team_ids[1], &app_user_email, &auth_token, &test_app) + .await + .unwrap(); + let after_second_join = get_current_datetime(); + + // Get user joined teams + let ip: ConnectInfo = ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080))); + let auth = app_user_auth_token.encode(JWT_SECRET()).unwrap(); + + let req = Request::builder() + .method(Method::GET) + .header("content-type", "application/json") + .header("authorization", format!("Bearer {auth}")) + .uri(format!( + "/cloud/private{}", + HttpCloudEndpoint::GetUserJoinedTeams.to_string() + )) + .extension(ip) + .body(Body::empty()) + .unwrap(); + + // Send request + let response = test_app.clone().oneshot(req).await.unwrap(); + // Validate response + let response = convert_response::(response) + .await + .unwrap(); + + // Check if dates for team join are correct + assert!(response.teams.get(&team_ids[0]).unwrap().joined_at >= before_first_join); + assert!(response.teams.get(&team_ids[0]).unwrap().joined_at <= after_first_join); + assert!(response.teams.get(&team_ids[1]).unwrap().joined_at >= after_first_join); + assert!(response.teams.get(&team_ids[1]).unwrap().joined_at <= after_second_join); + + // Check returned data + assert!(response.teams.len() == 2); + assert!(response.teams_apps.len() == 2); + assert!(response.teams_apps.get(&team_ids[0]).unwrap().len() == 3); + assert!(response.teams_apps.get(&team_ids[1]).unwrap().len() == 4); + assert!(response.user_privileges.len() == 2); + assert!(response.user_privileges.get(&team_ids[0]).unwrap().len() == 3); + assert!(response.user_privileges.get(&team_ids[1]).unwrap().len() == 4); + + // Create personal team as a test user + let personal_team_name = generate_valid_name(); + let personal_team_id = + add_test_team(&personal_team_name, &app_user_auth_token, &test_app, true) + .await + .unwrap(); + + // Get user joined teams + let ip: ConnectInfo = ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080))); + let auth = app_user_auth_token.encode(JWT_SECRET()).unwrap(); + + let req = Request::builder() + .method(Method::GET) + .header("content-type", "application/json") + .header("authorization", format!("Bearer {auth}")) + .uri(format!( + "/cloud/private{}", + HttpCloudEndpoint::GetUserJoinedTeams.to_string() + )) + .extension(ip) + .body(Body::empty()) + .unwrap(); + + // Send request + let response = test_app.clone().oneshot(req).await.unwrap(); + // Validate response + let response = convert_response::(response) + .await + .unwrap(); + + // Check returned data + assert!(response.teams.len() == 3); + assert!(response.teams.get(&personal_team_id).unwrap().personal); + assert!(response.teams_apps.len() == 2); + assert!(response.teams_apps.get(&team_ids[0]).unwrap().len() == 3); + assert!(response.teams_apps.get(&team_ids[1]).unwrap().len() == 4); + assert!(response.user_privileges.len() == 2); + assert!(response.user_privileges.get(&team_ids[0]).unwrap().len() == 3); + assert!(response.user_privileges.get(&team_ids[1]).unwrap().len() == 4); + } + + #[tokio::test] + async fn test_get_user_joined_teams_empty_teams() { + let test_app = create_test_app(false).await; + + let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; + + // Create personal team + let personal_team_name = generate_valid_name(); + let personal_team_id = add_test_team(&personal_team_name, &auth_token, &test_app, true) + .await + .unwrap(); + + // Get user joined teams + let response = get_test_user_joined_teams(&auth_token, &test_app) + .await + .unwrap(); + + assert!(response.teams.len() == 1); + assert!(response.teams.get(&personal_team_id).unwrap().personal); + assert!(response.teams_apps.len() == 0); + assert!(response.user_privileges.len() == 0); + + // Add new "normal" team + let team_name = generate_valid_name(); + let team_id = add_test_team(&team_name, &auth_token, &test_app, false) + .await + .unwrap(); + + // Get user joined teams + let response = get_test_user_joined_teams(&auth_token, &test_app) + .await + .unwrap(); + + assert!(response.teams.len() == 2); + assert!(response.teams.get(&team_id).unwrap().team_name == team_name); + assert!(response.teams.get(&team_id).unwrap().personal == false); + assert!(response.teams_apps.len() == 0); + assert!(response.user_privileges.len() == 0); + } + + #[tokio::test] + async fn test_get_user_joined_teams_empty_account() { + let test_app = create_test_app(false).await; + + let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; + + // Get user joined teams + let response = get_test_user_joined_teams(&auth_token, &test_app) + .await + .unwrap(); + + assert!(response.teams.len() == 0); + assert!(response.teams_apps.len() == 0); + assert!(response.user_privileges.len() == 0); + } +} diff --git a/server/src/http/cloud/mod.rs b/server/src/http/cloud/mod.rs index 0e05c86e..03cc8d72 100644 --- a/server/src/http/cloud/mod.rs +++ b/server/src/http/cloud/mod.rs @@ -1,5 +1,6 @@ pub mod add_user_to_team; pub mod cloud_middleware; +pub mod get_user_joined_teams; pub mod login_with_password; pub mod register_new_app; pub mod register_new_team; diff --git a/server/src/http/cloud/register_new_app.rs b/server/src/http/cloud/register_new_app.rs index c9c908af..3a8faaa5 100644 --- a/server/src/http/cloud/register_new_app.rs +++ b/server/src/http/cloud/register_new_app.rs @@ -117,10 +117,7 @@ pub async fn register_new_app( app_name: request.app_name.clone(), ack_public_keys: request.ack_public_keys.clone(), whitelisted_domains: request.whitelisted_domains.clone(), - email: None, - pass_hash: None, registration_timestamp: get_current_datetime(), - subscription: None, }; if let Err(err) = db @@ -210,8 +207,8 @@ mod tests { http::cloud::register_new_app::{HttpRegisterNewAppRequest, HttpRegisterNewAppResponse}, structs::{api_cloud_errors::CloudApiErrors, cloud_http_endpoints::HttpCloudEndpoint}, test_utils::test_utils::{ - add_test_team, convert_response, create_test_app, register_and_login_random_user, - truncate_all_tables, + add_test_team, convert_response, create_test_app, generate_valid_name, + register_and_login_random_user, }, }; use axum::{ @@ -219,7 +216,6 @@ mod tests { extract::ConnectInfo, http::{Method, Request}, }; - use database::db::Db; use std::net::SocketAddr; use tower::ServiceExt; @@ -227,20 +223,16 @@ mod tests { async fn test_register_new_app() { let test_app = create_test_app(false).await; - // Truncate db - let mut db = Db::connect_to_the_pool().await; - truncate_all_tables(&mut db).await.unwrap(); - let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; // Register new team - let team_name = "MyFirstTeam".to_string(); - let team_id = add_test_team(&team_name, &auth_token, &test_app) + let team_name = generate_valid_name(); + let team_id = add_test_team(&team_name, &auth_token, &test_app, false) .await .unwrap(); // Register new app - let app_name = "MyFirstApp".to_string(); + let app_name = generate_valid_name(); let request = HttpRegisterNewAppRequest { team_id: team_id.clone(), app_name: app_name.clone(), @@ -309,20 +301,16 @@ mod tests { async fn test_invalid_app_name() { let test_app = create_test_app(false).await; - // Truncate db - let mut db = Db::connect_to_the_pool().await; - truncate_all_tables(&mut db).await.unwrap(); - let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; // Register new team - let team_name = "MyFirstTeam".to_string(); - let team_id = add_test_team(&team_name, &auth_token, &test_app) + let team_name = generate_valid_name(); + let team_id = add_test_team(&team_name, &auth_token, &test_app, false) .await .unwrap(); // Register new app - let app_name = "MyINVALIDAPP_NAME!!!!".to_string(); + let app_name = generate_valid_name() + "18702dhb12n1902hd89b1n28sd1 02n>>>>>>>>>>>>>>>"; let request = HttpRegisterNewAppRequest { team_id: team_id.clone(), app_name: app_name.clone(), diff --git a/server/src/http/cloud/register_new_team.rs b/server/src/http/cloud/register_new_team.rs index 61b00acf..805aecc3 100644 --- a/server/src/http/cloud/register_new_team.rs +++ b/server/src/http/cloud/register_new_team.rs @@ -141,7 +141,7 @@ mod tests { http::cloud::register_new_team::{HttpRegisterNewTeamRequest, HttpRegisterNewTeamResponse}, structs::{api_cloud_errors::CloudApiErrors, cloud_http_endpoints::HttpCloudEndpoint}, test_utils::test_utils::{ - convert_response, create_test_app, register_and_login_random_user, truncate_all_tables, + convert_response, create_test_app, generate_valid_name, register_and_login_random_user, }, }; use axum::{ @@ -149,7 +149,6 @@ mod tests { extract::ConnectInfo, http::{Method, Request}, }; - use database::db::Db; use std::net::SocketAddr; use tower::ServiceExt; @@ -157,14 +156,10 @@ mod tests { async fn test_register_new_normal_team() { let test_app = create_test_app(false).await; - // Truncate db - let mut db = Db::connect_to_the_pool().await; - truncate_all_tables(&mut db).await.unwrap(); - let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; // Register new team - let first_team_name = "MyFirstTeam".to_string(); + let first_team_name = generate_valid_name(); let request = HttpRegisterNewTeamRequest { team_name: first_team_name.clone(), personal: false, @@ -224,14 +219,10 @@ mod tests { async fn test_register_new_personal_team() { let test_app = create_test_app(false).await; - // Truncate db - let mut db = Db::connect_to_the_pool().await; - truncate_all_tables(&mut db).await.unwrap(); - let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; // Register new team - let first_team_name = "MyFirstTeam".to_string(); + let first_team_name = generate_valid_name(); let request = HttpRegisterNewTeamRequest { team_name: first_team_name.clone(), personal: true, @@ -297,14 +288,10 @@ mod tests { async fn test_invalid_team_name() { let test_app = create_test_app(false).await; - // Truncate db - let mut db = Db::connect_to_the_pool().await; - truncate_all_tables(&mut db).await.unwrap(); - let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; let request = HttpRegisterNewTeamRequest { - team_name: "My_Invalid_Team!!!".to_string(), + team_name: generate_valid_name() + "1827389012hds012hd!!>>>>>>>>.", personal: true, }; diff --git a/server/src/http/cloud/remove_user_from_team.rs b/server/src/http/cloud/remove_user_from_team.rs index 8f0f575b..d6a9601a 100644 --- a/server/src/http/cloud/remove_user_from_team.rs +++ b/server/src/http/cloud/remove_user_from_team.rs @@ -94,7 +94,7 @@ pub async fn remove_user_from_team( } } - // Add user to the team + // Remove user from the team match db .remove_user_from_the_team(&user.user_id, &request.team_id) .await @@ -112,7 +112,10 @@ pub async fn remove_user_from_team( } } Ok(None) => { - return Err((StatusCode::BAD_REQUEST, "Team does not exist".to_string())); + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::TeamDoesNotExist.to_string(), + )); } Err(err) => { error!("Failed to get team: {:?}", err); @@ -123,3 +126,171 @@ pub async fn remove_user_from_team( } } } + +#[cfg(test)] +mod tests { + use crate::{ + env::JWT_SECRET, + http::cloud::{ + register_new_app::HttpRegisterNewAppRequest, + remove_user_from_team::{ + HttpRemoveUserFromTeamRequest, HttpRemoveUserFromTeamResponse, + }, + }, + structs::{api_cloud_errors::CloudApiErrors, cloud_http_endpoints::HttpCloudEndpoint}, + test_utils::test_utils::{ + add_test_app, add_test_team, add_user_to_test_team, convert_response, create_test_app, + generate_valid_name, register_and_login_random_user, remove_user_from_test_team, + }, + }; + use axum::{ + body::Body, + extract::{ConnectInfo, Request}, + http::Method, + }; + use std::net::SocketAddr; + use tower::ServiceExt; + + #[tokio::test] + async fn test_remove_user_from_team() { + let test_app = create_test_app(false).await; + + let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; + + // Register new team + let team_name = generate_valid_name(); + let team_id = add_test_team(&team_name, &auth_token, &test_app, false) + .await + .unwrap(); + + // Register app under the team + let app_name = generate_valid_name(); + let request = HttpRegisterNewAppRequest { + team_id: team_id.clone(), + app_name: app_name.clone(), + whitelisted_domains: vec![], + ack_public_keys: vec![], + }; + + // unwrap err as it should have failed + let _ = add_test_app(&request, &auth_token, &test_app) + .await + .unwrap(); + + // Register new user + let (_test_user_auth_token, test_user_email, _test_user_password) = + register_and_login_random_user(&test_app).await; + + // Add user to the team + add_user_to_test_team(&team_id, &test_user_email, &auth_token, &test_app) + .await + .unwrap(); + + // Remove user from the team + let request = HttpRemoveUserFromTeamRequest { + team_id: team_id.clone(), + user_email: test_user_email.clone(), + }; + + let ip: ConnectInfo = ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080))); + let json = serde_json::to_string(&request).unwrap(); + let auth = auth_token.encode(JWT_SECRET()).unwrap(); + + let req = Request::builder() + .method(Method::POST) + .header("content-type", "application/json") + .header("authorization", format!("Bearer {auth}")) + .uri(format!( + "/cloud/private{}", + HttpCloudEndpoint::RemoveUserFromTeam.to_string() + )) + .extension(ip.clone()) + .body(Body::from(json)) + .unwrap(); + + // Send request + let response = test_app.clone().oneshot(req).await.unwrap(); + // Validate response + convert_response::(response) + .await + .unwrap(); + + // Try to remove user from the team again, should fail as user is not in the team + let json = serde_json::to_string(&request).unwrap(); + let auth = auth_token.encode(JWT_SECRET()).unwrap(); + + let req = Request::builder() + .method(Method::POST) + .header("content-type", "application/json") + .header("authorization", format!("Bearer {auth}")) + .uri(format!( + "/cloud/private{}", + HttpCloudEndpoint::RemoveUserFromTeam.to_string() + )) + .extension(ip) + .body(Body::from(json)) + .unwrap(); + + // Send request + let response = test_app.clone().oneshot(req).await.unwrap(); + // Validate response + let err = convert_response::(response) + .await + .unwrap_err(); + + assert_eq!( + err.to_string(), + CloudApiErrors::UserDoesNotBelongsToTheTeam.to_string() + ); + } + + #[tokio::test] + async fn test_remove_user_from_team_team_not_found() { + let test_app = create_test_app(false).await; + + let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; + + // Team does not exist, use random uuid + let resp = remove_user_from_test_team( + &uuid7::uuid7().to_string(), + &"test_user_email@gmail.com".to_string(), + &auth_token, + &test_app, + ) + .await + .unwrap_err(); + + assert_eq!( + resp.to_string(), + CloudApiErrors::TeamDoesNotExist.to_string() + ); + } + + #[tokio::test] + async fn test_remove_user_from_team_user_does_not_exist() { + let test_app = create_test_app(false).await; + + let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; + + // Register new team + let team_name = generate_valid_name(); + let team_id = add_test_team(&team_name, &auth_token, &test_app, false) + .await + .unwrap(); + + // Team does not exist, use random uuid + let resp = remove_user_from_test_team( + &team_id.to_string(), + &"test_user_email@gmail.com".to_string(), + &auth_token, + &test_app, + ) + .await + .unwrap_err(); + + assert_eq!( + resp.to_string(), + CloudApiErrors::UserDoesNotExist.to_string() + ); + } +} diff --git a/server/src/routes/cloud_router.rs b/server/src/routes/cloud_router.rs index dd273b96..559f8b71 100644 --- a/server/src/routes/cloud_router.rs +++ b/server/src/routes/cloud_router.rs @@ -1,15 +1,19 @@ use crate::{ auth::auth_middleware::access_auth_middleware, http::cloud::{ - add_user_to_team::add_user_to_team, login_with_password::login_with_password, - register_new_app::register_new_app, register_new_team::register_new_team, - register_with_password::register_with_password, + add_user_to_team::add_user_to_team, get_user_joined_teams::get_user_joined_teams, + login_with_password::login_with_password, register_new_app::register_new_app, + register_new_team::register_new_team, register_with_password::register_with_password, remove_user_from_team::remove_user_from_team, }, state::ServerState, structs::cloud_http_endpoints::HttpCloudEndpoint, }; -use axum::{middleware, routing::post, Router}; +use axum::{ + middleware, + routing::{get, post}, + Router, +}; pub fn cloud_router(state: ServerState) -> Router { Router::new() @@ -55,5 +59,9 @@ pub fn private_router(state: ServerState) -> Router { &HttpCloudEndpoint::RemoveUserFromTeam.to_string(), post(remove_user_from_team), ) + .route( + &HttpCloudEndpoint::GetUserJoinedTeams.to_string(), + get(get_user_joined_teams), + ) .with_state(state) } diff --git a/server/src/statics.rs b/server/src/statics.rs index baefc0aa..44717e26 100644 --- a/server/src/statics.rs +++ b/server/src/statics.rs @@ -7,7 +7,7 @@ pub const USERS_AMOUNT_LIMIT_PER_TEAM: usize = 50; // Name must be 3-30 characters long and include only alphanumeric characters, underscores, or slashes. pub static NAME_REGEX: Lazy = - Lazy::new(|| Regex::new(r"^[a-zA-Z0-9_\/]{3,30}$").expect("Regex creation failed")); + Lazy::new(|| Regex::new(r"^[a-zA-Z0-9_-]{3,30}$").expect("Regex creation failed")); pub struct PasswordValidator { pub re: Regex, diff --git a/server/src/structs/app_info.rs b/server/src/structs/app_info.rs new file mode 100644 index 00000000..e7af7fc6 --- /dev/null +++ b/server/src/structs/app_info.rs @@ -0,0 +1,31 @@ +use super::joined_team::TeamId; +use crate::state::AppId; +use chrono::{DateTime, Utc}; +use database::tables::registered_app::table_struct::DbRegisteredApp; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct AppInfo { + pub team_id: TeamId, + pub app_id: AppId, + pub app_name: String, + pub registered_at: DateTime, + pub whitelisted_domains: Vec, + pub ack_public_keys: Vec, +} + +impl From for AppInfo { + fn from(app_info: DbRegisteredApp) -> Self { + AppInfo { + team_id: app_info.team_id, + app_id: app_info.app_id, + app_name: app_info.app_name, + registered_at: app_info.registration_timestamp, + whitelisted_domains: app_info.whitelisted_domains, + ack_public_keys: app_info.ack_public_keys, + } + } +} diff --git a/server/src/structs/cloud_http_endpoints.rs b/server/src/structs/cloud_http_endpoints.rs index 4f32a313..bd493632 100644 --- a/server/src/structs/cloud_http_endpoints.rs +++ b/server/src/structs/cloud_http_endpoints.rs @@ -16,6 +16,8 @@ pub enum HttpCloudEndpoint { AddUserToTeam, #[serde(rename = "/remove_user_from_team")] RemoveUserFromTeam, + #[serde(rename = "/get_user_joined_teams")] + GetUserJoinedTeams, } impl HttpCloudEndpoint { @@ -27,6 +29,7 @@ impl HttpCloudEndpoint { HttpCloudEndpoint::RegisterNewTeam => "/register_new_team".to_string(), HttpCloudEndpoint::AddUserToTeam => "/add_user_to_team".to_string(), HttpCloudEndpoint::RemoveUserFromTeam => "/remove_user_from_team".to_string(), + HttpCloudEndpoint::GetUserJoinedTeams => "/get_user_joined_teams".to_string(), } } } diff --git a/server/src/structs/joined_team.rs b/server/src/structs/joined_team.rs new file mode 100644 index 00000000..3b4117c7 --- /dev/null +++ b/server/src/structs/joined_team.rs @@ -0,0 +1,17 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +pub type TeamId = String; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct JoinedTeam { + pub team_id: TeamId, + pub team_name: String, + pub creator_email: String, + pub created_at: DateTime, + pub joined_at: DateTime, + pub personal: bool, +} diff --git a/server/src/structs/mod.rs b/server/src/structs/mod.rs index 7515b1ae..0419513e 100644 --- a/server/src/structs/mod.rs +++ b/server/src/structs/mod.rs @@ -1,11 +1,14 @@ pub mod api_cloud_errors; +pub mod app_info; pub mod app_messages; pub mod client_messages; pub mod cloud_http_endpoints; pub mod common; pub mod http_endpoints; +pub mod joined_team; pub mod notification_msg; pub mod session; +pub mod user_privilege; pub mod wallet_metadata; pub mod wallet_type; pub mod wallets; diff --git a/server/src/structs/user_privilege.rs b/server/src/structs/user_privilege.rs new file mode 100644 index 00000000..6baead37 --- /dev/null +++ b/server/src/structs/user_privilege.rs @@ -0,0 +1,27 @@ +use crate::state::AppId; +use chrono::{DateTime, Utc}; +use database::{ + structs::privilege_level::PrivilegeLevel, + tables::user_app_privileges::table_struct::UserAppPrivilege, +}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct UserPrivilege { + pub app_id: AppId, + pub granted_at: DateTime, + pub privilege: PrivilegeLevel, +} + +impl From for UserPrivilege { + fn from(user_app_privilege: UserAppPrivilege) -> Self { + UserPrivilege { + app_id: user_app_privilege.app_id, + granted_at: user_app_privilege.creation_timestamp, + privilege: user_app_privilege.privilege_level, + } + } +} diff --git a/server/src/test_utils.rs b/server/src/test_utils.rs index 6a3a8294..4ee3f4b9 100644 --- a/server/src/test_utils.rs +++ b/server/src/test_utils.rs @@ -4,11 +4,18 @@ pub mod test_utils { auth::AuthToken, env::{JWT_PUBLIC_KEY, JWT_SECRET}, http::cloud::{ + add_user_to_team::{HttpAddUserToTeamRequest, HttpAddUserToTeamResponse}, + get_user_joined_teams::HttpGetUserJoinedTeamsResponse, login_with_password::{HttpLoginRequest, HttpLoginResponse}, + register_new_app::{HttpRegisterNewAppRequest, HttpRegisterNewAppResponse}, register_new_team::{HttpRegisterNewTeamRequest, HttpRegisterNewTeamResponse}, register_with_password::HttpRegisterWithPasswordRequest, + remove_user_from_team::{ + HttpRemoveUserFromTeamRequest, HttpRemoveUserFromTeamResponse, + }, }, routes::router::get_router, + statics::NAME_REGEX, structs::cloud_http_endpoints::HttpCloudEndpoint, }; use anyhow::bail; @@ -19,7 +26,10 @@ pub mod test_utils { Router, }; use database::db::Db; - use rand::{distributions::Alphanumeric, thread_rng, Rng}; + use rand::{ + distributions::{Alphanumeric, Uniform}, + thread_rng, Rng, + }; use sqlx::Row; use std::net::SocketAddr; use tower::ServiceExt; @@ -153,11 +163,12 @@ pub mod test_utils { team_name: &String, admin_token: &AuthToken, app: &Router, + personal: bool, ) -> anyhow::Result { // Register new team let request = HttpRegisterNewTeamRequest { team_name: team_name.clone(), - personal: false, + personal: personal, }; let ip: ConnectInfo = ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080))); @@ -184,6 +195,133 @@ pub mod test_utils { .map(|response| Ok(response.team_id))? } + pub async fn add_test_app( + request: &HttpRegisterNewAppRequest, + admin_token: &AuthToken, + app: &Router, + ) -> anyhow::Result { + // Register new app + let ip: ConnectInfo = ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080))); + let json = serde_json::to_string(&request).unwrap(); + let auth = admin_token.encode(JWT_SECRET()).unwrap(); + + let req = Request::builder() + .method(Method::POST) + .header("content-type", "application/json") + .header("authorization", format!("Bearer {auth}")) + .uri(format!( + "/cloud/private{}", + HttpCloudEndpoint::RegisterNewApp.to_string() + )) + .extension(ip) + .body(Body::from(json)) + .unwrap(); + + // Send request + let response = app.clone().oneshot(req).await.unwrap(); + // Validate response + convert_response::(response) + .await + .map(|response| Ok(response.app_id))? + } + + pub async fn add_user_to_test_team( + team_id: &String, + user_email: &String, + admin_token: &AuthToken, + app: &Router, + ) -> anyhow::Result<()> { + // Add user to test team + let request = HttpAddUserToTeamRequest { + team_id: team_id.clone(), + user_email: user_email.clone(), + }; + + let ip: ConnectInfo = ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080))); + let json = serde_json::to_string(&request).unwrap(); + let auth = admin_token.encode(JWT_SECRET()).unwrap(); + + let req = Request::builder() + .method(Method::POST) + .header("content-type", "application/json") + .header("authorization", format!("Bearer {auth}")) + .uri(format!( + "/cloud/private{}", + HttpCloudEndpoint::AddUserToTeam.to_string() + )) + .extension(ip) + .body(Body::from(json)) + .unwrap(); + + // Send request + let response = app.clone().oneshot(req).await.unwrap(); + // Validate response + convert_response::(response) + .await + .map(|_| Ok(()))? + } + + pub async fn remove_user_from_test_team( + team_id: &String, + user_email: &String, + admin_token: &AuthToken, + app: &Router, + ) -> anyhow::Result<()> { + // Add user to test team + let request = HttpRemoveUserFromTeamRequest { + team_id: team_id.clone(), + user_email: user_email.clone(), + }; + + let ip: ConnectInfo = ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080))); + let json = serde_json::to_string(&request).unwrap(); + let auth = admin_token.encode(JWT_SECRET()).unwrap(); + + let req = Request::builder() + .method(Method::POST) + .header("content-type", "application/json") + .header("authorization", format!("Bearer {auth}")) + .uri(format!( + "/cloud/private{}", + HttpCloudEndpoint::RemoveUserFromTeam.to_string() + )) + .extension(ip) + .body(Body::from(json)) + .unwrap(); + + // Send request + let response = app.clone().oneshot(req).await.unwrap(); + // Validate response + convert_response::(response) + .await + .map(|_| Ok(()))? + } + + pub async fn get_test_user_joined_teams( + user_token: &AuthToken, + app: &Router, + ) -> anyhow::Result { + let ip: ConnectInfo = ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080))); + let auth = user_token.encode(JWT_SECRET()).unwrap(); + + let req = Request::builder() + .method(Method::GET) + .header("content-type", "application/json") + .header("authorization", format!("Bearer {auth}")) + .uri(format!( + "/cloud/private{}", + HttpCloudEndpoint::GetUserJoinedTeams.to_string() + )) + .extension(ip) + .body(Body::empty()) + .unwrap(); + + // Send request + let response = app.clone().oneshot(req).await.unwrap(); + // Validate response + convert_response::(response).await + } + pub async fn body_to_vec(response: Response) -> anyhow::Result> { match to_bytes(response.into_body(), usize::MAX).await { Ok(body) => Ok(body.to_vec()), @@ -206,13 +344,48 @@ pub mod test_utils { { match response.status() { StatusCode::OK => { - let payload = serde_json::from_slice(&body_to_vec(response).await?)?; - return Ok(payload); + let body = body_to_vec(response).await?; + match serde_json::from_slice::(&body) { + Ok(payload) => Ok(payload), + Err(e) => bail!("Error deserializing response: {}", e), + } } - _ => { + StatusCode::INTERNAL_SERVER_ERROR | StatusCode::BAD_REQUEST => { let error_message = convert_response_into_error_string(response).await?; - bail!(error_message); + bail!("{}", error_message) + } + _ => { + let status = response.status(); + bail!("{}", status) } } } + + pub fn generate_valid_name() -> String { + let mut rng = rand::thread_rng(); + + // Define ranges for alphanumeric characters and individual characters for underscore and slash. + let char_ranges = ['a'..'z', 'A'..'Z', '0'..'9']; + let single_chars = ['_', '-']; + + // Flatten the char_ranges into a single collection of characters and add individual characters. + let mut chars: Vec = char_ranges.into_iter().flat_map(|range| range).collect(); + chars.extend(single_chars.iter()); + + // Ensure the distribution covers the range of available characters. + let dist = Uniform::from(0..chars.len()); + + // Define minimum and maximum length based on the regex pattern. + let min_len = 3; + let max_len = 30; + let name_len = rng.gen_range(min_len..=max_len); + + // Generate a random string of valid characters within the defined length. + let name: String = (0..name_len).map(|_| chars[rng.sample(&dist)]).collect(); + + // Ensure the generated name matches the regex pattern. + assert!(NAME_REGEX.is_match(&name)); + + name + } }