diff --git a/database/migrations/0003_users.sql b/database/migrations/0003_users.sql index 2cb107fc..da39a956 100644 --- a/database/migrations/0003_users.sql +++ b/database/migrations/0003_users.sql @@ -3,7 +3,8 @@ CREATE TABLE users( email TEXT NOT NULL UNIQUE, password_hash TEXT, passkeys TEXT, - creation_timestamp TIMESTAMPTZ NOT NULL + creation_timestamp TIMESTAMPTZ NOT NULL, + deactivated_at TIMESTAMPTZ ); CREATE INDEX users_name_idx ON users(user_id); diff --git a/database/src/tables/registered_app/update.rs b/database/src/tables/registered_app/update.rs index 6f2d25a3..82361c15 100644 --- a/database/src/tables/registered_app/update.rs +++ b/database/src/tables/registered_app/update.rs @@ -1,5 +1,10 @@ use super::table_struct::{DbRegisteredApp, REGISTERED_APPS_KEYS, REGISTERED_APPS_TABLE_NAME}; -use crate::{db::Db, structs::db_error::DbError, tables::utils::get_current_datetime}; + +use crate::{ + db::Db, + structs::db_error::DbError, + tables::{team::table_struct::TEAM_TABLE_NAME, utils::get_current_datetime}, +}; use sqlx::{query, Transaction}; impl Db { @@ -114,4 +119,25 @@ impl Db { Err(e) => Err(e).map_err(|e| e.into()), } } + + pub async fn deactivate_user_apps( + &self, + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + user_id: &str, + ) -> Result<(), DbError> { + let query_body = format!( + "UPDATE {REGISTERED_APPS_TABLE_NAME} SET deactivated_at = $1 WHERE team_id IN (SELECT team_id FROM {TEAM_TABLE_NAME} WHERE team_admin_id = $2 AND deactivated_at IS NULL) AND deactivated_at IS NULL", + ); + + let query_result = query(&query_body) + .bind(&get_current_datetime()) + .bind(user_id) + .execute(&mut **tx) + .await; + + match query_result { + Ok(_) => Ok(()), + Err(e) => Err(e).map_err(|e| e.into()), + } + } } diff --git a/database/src/tables/team/select.rs b/database/src/tables/team/select.rs index 42a7453c..00c35dca 100644 --- a/database/src/tables/team/select.rs +++ b/database/src/tables/team/select.rs @@ -101,18 +101,6 @@ impl Db { .map_err(|e| e.into()); } - pub async fn get_team_by_admin_id(&self, admin_id: &String) -> Result, DbError> { - let query = format!( - "SELECT * FROM {TEAM_TABLE_NAME} WHERE team_admin_id = $1 AND deactivated_at IS NULL" - ); - let typed_query = query_as::<_, Team>(&query); - - return typed_query - .bind(&admin_id) - .fetch_optional(&self.connection_pool) - .await - .map_err(|e| e.into()); - } pub async fn get_all_teams(&self) -> Result, DbError> { let query = format!("SELECT * FROM {TEAM_TABLE_NAME} WHERE deactivated_at IS NULL"); let typed_query = query_as::<_, Team>(&query); diff --git a/database/src/tables/team/table_struct.rs b/database/src/tables/team/table_struct.rs index 756d7acd..e007a080 100644 --- a/database/src/tables/team/table_struct.rs +++ b/database/src/tables/team/table_struct.rs @@ -12,7 +12,7 @@ pub const TEAM_KEYS: &str = #[derive(Clone, Debug, Eq, PartialEq)] pub struct Team { pub team_id: String, - pub grafana_id: String, + pub grafana_id: Option, pub personal: bool, pub team_name: String, // Subscription is required to get access to the statistics @@ -26,9 +26,7 @@ impl FromRow<'_, PgRow> for Team { fn from_row(row: &sqlx::postgres::PgRow) -> std::result::Result { Ok(Team { team_id: row.get("team_id"), - grafana_id: row - .try_get::, _>("grafana_id")? - .unwrap_or_default(), + grafana_id: row.get("grafana_id"), team_name: row.get("team_name"), personal: row.get("personal"), subscription: row.get("subscription"), diff --git a/database/src/tables/team/update.rs b/database/src/tables/team/update.rs index 4f274cb2..50ae0caa 100644 --- a/database/src/tables/team/update.rs +++ b/database/src/tables/team/update.rs @@ -126,6 +126,25 @@ impl Db { Err(e) => Err(e).map_err(|e| e.into()), } } + + pub async fn delete_all_user_teams(&self, + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + user_id: &str) -> Result<(), DbError> { + let query_body = format!( + "UPDATE {TEAM_TABLE_NAME} SET deactivated_at = $1 WHERE team_admin_id = $2 AND deactivated_at IS NULL", + ); + + let query_result = query(&query_body) + .bind(&get_current_datetime()) + .bind(user_id) + .execute(&mut **tx) + .await; + + match query_result { + Ok(_) => Ok(()), + Err(e) => Err(e).map_err(|e| e.into()), + } + } } #[cfg(feature = "cloud_integration_tests")] @@ -151,7 +170,7 @@ mod tests { // Create team and register app let team = Team { team_id: "test_team_id".to_string(), - grafana_id: "test_grafana_id".to_string(), + grafana_id: Some("test_grafana_id".to_string()), team_name: "test_team_name".to_string(), personal: false, subscription: None, diff --git a/database/src/tables/team_invites/update.rs b/database/src/tables/team_invites/update.rs index 22baf5a4..58792cd6 100644 --- a/database/src/tables/team_invites/update.rs +++ b/database/src/tables/team_invites/update.rs @@ -1,7 +1,7 @@ use super::table_struct::{TEAM_INVITES_KEYS, TEAM_INVITES_TABLE_NAME}; use crate::db::Db; use crate::structs::db_error::DbError; -use crate::tables::utils::get_current_datetime; +use crate::tables::{team::table_struct::TEAM_TABLE_NAME, utils::get_current_datetime}; use sqlx::{query, Transaction}; impl Db { @@ -92,4 +92,27 @@ impl Db { Err(e) => Err(e).map_err(|e| e.into()), } } + + pub async fn cancel_all_team_invites_containing_email( + &self, + tx: &mut Transaction<'_, sqlx::Postgres>, + user_email: &String, + user_id: &String, + ) -> Result<(), DbError> { + let query_body = format!( + "UPDATE {TEAM_INVITES_TABLE_NAME} SET cancelled_at = $1 WHERE (user_email = $2 OR team_id IN (SELECT team_id FROM {TEAM_TABLE_NAME} WHERE team_admin_id = $3 AND deactivated_at IS NULL) ) AND accepted_at IS NULL AND cancelled_at IS NULL" + ); + + let query_result = query(&query_body) + .bind(&get_current_datetime()) + .bind(&user_email) + .bind(&user_id) + .execute(&mut **tx) + .await; + + match query_result { + Ok(_) => Ok(()), + Err(e) => Err(e).map_err(|e| e.into()), + } + } } diff --git a/database/src/tables/test_utils.rs b/database/src/tables/test_utils.rs index 7343415e..cc35c01b 100644 --- a/database/src/tables/test_utils.rs +++ b/database/src/tables/test_utils.rs @@ -97,7 +97,7 @@ pub mod test_utils { let team = Team { team_id: team_id.clone(), - grafana_id: "test_grafana_id".to_string(), + grafana_id: None, team_name: "test_team_name".to_string(), personal: false, subscription: None, diff --git a/database/src/tables/user_app_privileges/update.rs b/database/src/tables/user_app_privileges/update.rs index 700fa216..a82721cb 100644 --- a/database/src/tables/user_app_privileges/update.rs +++ b/database/src/tables/user_app_privileges/update.rs @@ -263,6 +263,40 @@ impl Db { Err(e) => Err(e).map_err(|e| e.into()), } } + + pub async fn remove_inactive_user_from_teams( + &self, + tx: &mut Transaction<'_, sqlx::Postgres>, + user_id: &String, + ) -> Result<(), DbError> { + let query_body = format!("DELETE FROM {USER_APP_PRIVILEGES_TABLE_NAME} WHERE user_id = $1"); + let query_result = query(&query_body).bind(user_id).execute(&mut **tx).await; + match query_result { + Ok(_) => Ok(()), + Err(e) => Err(e).map_err(|e| e.into()), + } + } + + pub async fn remove_privileges_for_inactive_teams( + &self, + tx: &mut Transaction<'_, sqlx::Postgres>, + user_id: &String, + ) -> Result<(), DbError> { + let query_body = format!( + "DELETE FROM {USER_APP_PRIVILEGES_TABLE_NAME} + WHERE app_id IN ( + SELECT app_id + FROM {REGISTERED_APPS_TABLE_NAME} r + INNER JOIN team t ON r.team_id = t.team_id + WHERE t.team_admin_id = $1 + )" + ); + let query_result = query(&query_body).bind(user_id).execute(&mut **tx).await; + match query_result { + Ok(_) => Ok(()), + Err(e) => Err(e).map_err(|e| e.into()), + } + } } #[cfg(feature = "cloud_integration_tests")] @@ -371,7 +405,7 @@ mod tests { let team = Team { team_id: team_id.clone(), - grafana_id: "test_grafana_id_2".to_string(), + grafana_id: None, team_name: "test_team_name".to_string(), personal: false, subscription: None, diff --git a/database/src/tables/users/select.rs b/database/src/tables/users/select.rs index db2a3956..56addc12 100644 --- a/database/src/tables/users/select.rs +++ b/database/src/tables/users/select.rs @@ -8,7 +8,9 @@ use sqlx::query_as; impl Db { pub async fn get_user_by_user_id(&self, user_id: &String) -> Result, DbError> { - let query = format!("SELECT * FROM {USERS_TABLE_NAME} WHERE user_id = $1"); + let query = format!( + "SELECT * FROM {USERS_TABLE_NAME} WHERE user_id = $1 AND deactivated_at IS NULL" + ); let typed_query = query_as::<_, User>(&query); return typed_query @@ -19,7 +21,8 @@ impl Db { } pub async fn get_user_by_email(&self, email: &String) -> Result, DbError> { - let query = format!("SELECT * FROM {USERS_TABLE_NAME} WHERE email = $1"); + let query = + format!("SELECT * FROM {USERS_TABLE_NAME} WHERE email = $1 AND deactivated_at IS NULL"); let typed_query = query_as::<_, User>(&query); return typed_query @@ -34,7 +37,7 @@ impl Db { emails: &Vec, ) -> Result, DbError> { // User email to user id - let query = format!("SELECT user_id, email FROM {USERS_TABLE_NAME} WHERE email = ANY($1)"); + let query = format!("SELECT user_id, email FROM {USERS_TABLE_NAME} WHERE email = ANY($1) AND deactivated_at IS NULL"); let typed_query = query_as::<_, UserIdEmail>(&query); let data_vec = typed_query @@ -52,7 +55,7 @@ impl Db { ) -> Result, DbError> { // User id to user email let query = - format!("SELECT user_id, email FROM {USERS_TABLE_NAME} WHERE user_id = ANY($1)"); + format!("SELECT user_id, email FROM {USERS_TABLE_NAME} WHERE user_id = ANY($1) AND deactivated_at IS NULL"); let typed_query = query_as::<_, UserIdEmail>(&query); let data_vec = typed_query diff --git a/database/src/tables/users/table_struct.rs b/database/src/tables/users/table_struct.rs index ccb7f772..f131528a 100644 --- a/database/src/tables/users/table_struct.rs +++ b/database/src/tables/users/table_struct.rs @@ -6,7 +6,8 @@ use sqlx::{ use webauthn_rs::prelude::Passkey; pub const USERS_TABLE_NAME: &str = "users"; -pub const USERS_KEYS: &str = "user_id, email, password_hash, passkeys, creation_timestamp"; +pub const USERS_KEYS: &str = + "user_id, email, password_hash, passkeys, creation_timestamp, deactivated_at"; #[derive(Clone, Debug, PartialEq)] pub struct User { @@ -15,6 +16,7 @@ pub struct User { pub password_hash: Option, pub passkeys: Option>, pub creation_timestamp: DateTime, + pub deactivated_at: Option>, } impl FromRow<'_, PgRow> for User { @@ -31,6 +33,7 @@ impl FromRow<'_, PgRow> for User { }, user_id: row.get("user_id"), creation_timestamp: row.get("creation_timestamp"), + deactivated_at: row.get("deactivated_at"), }) } } diff --git a/database/src/tables/users/update.rs b/database/src/tables/users/update.rs index 6205e1bc..d37389d9 100644 --- a/database/src/tables/users/update.rs +++ b/database/src/tables/users/update.rs @@ -2,7 +2,7 @@ use super::table_struct::{USERS_KEYS, USERS_TABLE_NAME}; use crate::db::Db; use crate::structs::db_error::DbError; use crate::tables::utils::get_current_datetime; -use sqlx::query; +use sqlx::{query, Transaction}; use webauthn_rs::prelude::Passkey; impl Db { @@ -13,8 +13,9 @@ impl Db { password_hash: Option<&String>, passkey: Option<&Passkey>, ) -> Result<(), DbError> { - let query_body = - format!("INSERT INTO {USERS_TABLE_NAME} ({USERS_KEYS}) VALUES ($1, $2, $3, $4, $5)"); + let query_body = format!( + "INSERT INTO {USERS_TABLE_NAME} ({USERS_KEYS}) VALUES ($1, $2, $3, $4, $5, NULL)" + ); let passkey = match passkey { Some(passkey) => { @@ -51,7 +52,7 @@ impl Db { new_password: &String, ) -> Result<(), DbError> { let query_body = - format!("UPDATE {USERS_TABLE_NAME} SET password_hash = $1 WHERE email = $2"); + format!("UPDATE {USERS_TABLE_NAME} SET password_hash = $1 WHERE email = $2 AND deactivated_at IS NULL"); let query_result = query(&query_body) .bind(new_password) @@ -74,7 +75,7 @@ impl Db { DbError::DatabaseError(format!("Failed to serialize passkey: {}", e.to_string())) })?; - let query_body = format!("UPDATE {USERS_TABLE_NAME} SET passkeys = $1 WHERE email = $2"); + let query_body = format!("UPDATE {USERS_TABLE_NAME} SET passkeys = $1 WHERE email = $2 AND deactivated_at IS NULL"); let query_result = query(&query_body) .bind(&serialized_passkey) @@ -87,6 +88,27 @@ impl Db { Err(e) => Err(e).map_err(|e| e.into()), } } + + pub async fn deactivate_user( + &self, + user_id: &String, + tx: &mut Transaction<'_, sqlx::Postgres>, + ) -> Result<(), DbError> { + let query_body = format!( + "UPDATE {USERS_TABLE_NAME} SET deactivated_at = $1 WHERE user_id = $2 AND deactivated_at IS NULL" + ); + + let query_result = query(&query_body) + .bind(&get_current_datetime()) + .bind(user_id) + .execute(&mut **tx) + .await; + + match query_result { + Ok(_) => Ok(()), + Err(e) => Err(e).map_err(|e| e.into()), + } + } } #[cfg(feature = "cloud_integration_tests")] @@ -115,6 +137,7 @@ mod tests { user_id: "test_user_id".to_string(), passkeys: None, creation_timestamp: to_microsecond_precision(&Utc::now()), + deactivated_at: None, }; db.add_new_user(&user.user_id, &user.email, Some(&password), None) diff --git a/sdk/bindings/CloudApiErrors.ts b/sdk/bindings/CloudApiErrors.ts index 273cba52..09aa1d84 100644 --- a/sdk/bindings/CloudApiErrors.ts +++ b/sdk/bindings/CloudApiErrors.ts @@ -1,3 +1,53 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type CloudApiErrors = "TeamDoesNotExist" | "UserDoesNotExist" | "CloudFeatureDisabled" | "InsufficientPermissions" | "TeamHasNoRegisteredApps" | "DatabaseError" | "MaximumUsersPerTeamReached" | "UserAlreadyBelongsToTheTeam" | "IncorrectPassword" | "AccessTokenFailure" | "RefreshTokenFailure" | "AppAlreadyExists" | "MaximumAppsPerTeamReached" | "TeamAlreadyExists" | "PersonalTeamAlreadyExists" | "EmailAlreadyExists" | "InternalServerError" | "UserDoesNotBelongsToTheTeam" | "InvalidName" | "UnauthorizedOriginError" | "AppDoesNotExist" | "UserAlreadyInvitedToTheTeam" | "MaximumInvitesPerTeamReached" | "InviteNotFound" | "ActionForbiddenForPersonalTeam" | "InviteDoesNotExist" | "InvalidPaginationCursor" | "InvalidOrExpiredVerificationCode" | "InvalidOrExpiredAuthCode" | "InvalidDomainName" | "DomainAlreadyVerified" | "DomainVerificationFailure" | "DomainNotFound" | "DomainVerificationNotStarted" | "DomainAlreadyVerifiedByAnotherApp" | "NoPendingDomainVerification" | "WebAuthnError" | "PasswordNotSet" | "UserDoesNotHavePasskey" | "PasskeyAlreadyExists" | "InvalidPasskeyCredential" | "PasskeyDoesNotExist" | "FailedToCreateTeam" | "DashboardImportFail" | "OriginHeaderRequired" | "InvalidOrigin" | "InvalidAction" | "AdminCannotLeaveTeam" | "GrafanaError"; \ No newline at end of file +export type CloudApiErrors = + | 'TeamDoesNotExist' + | 'UserDoesNotExist' + | 'CloudFeatureDisabled' + | 'InsufficientPermissions' + | 'TeamHasNoRegisteredApps' + | 'DatabaseError' + | 'MaximumUsersPerTeamReached' + | 'UserAlreadyBelongsToTheTeam' + | 'IncorrectPassword' + | 'AccessTokenFailure' + | 'RefreshTokenFailure' + | 'AppAlreadyExists' + | 'MaximumAppsPerTeamReached' + | 'TeamAlreadyExists' + | 'PersonalTeamAlreadyExists' + | 'EmailAlreadyExists' + | 'InternalServerError' + | 'UserDoesNotBelongsToTheTeam' + | 'InvalidName' + | 'UnauthorizedOriginError' + | 'AppDoesNotExist' + | 'UserAlreadyInvitedToTheTeam' + | 'MaximumInvitesPerTeamReached' + | 'InviteNotFound' + | 'ActionForbiddenForPersonalTeam' + | 'InviteDoesNotExist' + | 'InvalidPaginationCursor' + | 'InvalidOrExpiredVerificationCode' + | 'InvalidOrExpiredAuthCode' + | 'InvalidDomainName' + | 'DomainAlreadyVerified' + | 'DomainVerificationFailure' + | 'DomainNotFound' + | 'DomainVerificationNotStarted' + | 'DomainAlreadyVerifiedByAnotherApp' + | 'NoPendingDomainVerification' + | 'WebAuthnError' + | 'PasswordNotSet' + | 'UserDoesNotHavePasskey' + | 'PasskeyAlreadyExists' + | 'InvalidPasskeyCredential' + | 'PasskeyDoesNotExist' + | 'FailedToCreateTeam' + | 'DashboardImportFail' + | 'OriginHeaderRequired' + | 'InvalidOrigin' + | 'InvalidAction' + | 'AdminCannotLeaveTeam' + | 'GrafanaError' + | 'TeamWithoutGrafanaId' diff --git a/sdk/bindings/HttpCloudEndpoint.ts b/sdk/bindings/HttpCloudEndpoint.ts index 3061c445..26ee84a8 100644 --- a/sdk/bindings/HttpCloudEndpoint.ts +++ b/sdk/bindings/HttpCloudEndpoint.ts @@ -1,3 +1,46 @@ // 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_start" | "/register_with_password_finish" | "/login_with_password" | "/login_with_google" | "/refresh_token" | "/register_new_team" | "/remove_user_from_team" | "/get_user_joined_teams" | "/events" | "/invite_user_to_team" | "/accept_team_invite" | "/get_team_user_invites" | "/get_user_team_invites" | "/cancel_team_user_invite" | "/cancel_user_team_invite" | "/get_app_events" | "/reset_password_start" | "/reset_password_finish" | "/verify_domain_start" | "/verify_domain_finish" | "/remove_whitelisted_domain" | "/cancel_pending_domain_verification" | "/register_with_passkey_start" | "/register_with_passkey_finish" | "/reset_passkey_start" | "/reset_passkey_finish" | "/get_passkey_challenge" | "/delete_passkey" | "/add_passkey_start" | "/add_passkey_finish" | "/get_user_metadata" | "/get_team_metadata" | "/get_team_users_privileges" | "/change_user_privileges" | "/login_with_passkey_start" | "/login_with_passkey_finish" | "/verify_code" | "/leave_team" | "/delete_app" | "/delete_team"; \ No newline at end of file +export type HttpCloudEndpoint = + | '/register_new_app' + | '/register_with_password_start' + | '/register_with_password_finish' + | '/login_with_password' + | '/login_with_google' + | '/refresh_token' + | '/register_new_team' + | '/remove_user_from_team' + | '/get_user_joined_teams' + | '/events' + | '/invite_user_to_team' + | '/accept_team_invite' + | '/get_team_user_invites' + | '/get_user_team_invites' + | '/cancel_team_user_invite' + | '/cancel_user_team_invite' + | '/get_app_events' + | '/reset_password_start' + | '/reset_password_finish' + | '/verify_domain_start' + | '/verify_domain_finish' + | '/remove_whitelisted_domain' + | '/cancel_pending_domain_verification' + | '/register_with_passkey_start' + | '/register_with_passkey_finish' + | '/reset_passkey_start' + | '/reset_passkey_finish' + | '/get_passkey_challenge' + | '/delete_passkey' + | '/add_passkey_start' + | '/add_passkey_finish' + | '/get_user_metadata' + | '/get_team_metadata' + | '/get_team_users_privileges' + | '/change_user_privileges' + | '/login_with_passkey_start' + | '/login_with_passkey_finish' + | '/verify_code' + | '/leave_team' + | '/delete_app' + | '/delete_team' + | '/delete_account_start' + | '/delete_account_finish' diff --git a/sdk/bindings/HttpDeleteAccountFinishRequest.ts b/sdk/bindings/HttpDeleteAccountFinishRequest.ts new file mode 100644 index 00000000..415d900f --- /dev/null +++ b/sdk/bindings/HttpDeleteAccountFinishRequest.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface HttpDeleteAccountFinishRequest { + authCode: string +} diff --git a/sdk/bindings/HttpDeleteAccountStartRequest.ts b/sdk/bindings/HttpDeleteAccountStartRequest.ts new file mode 100644 index 00000000..f6bfabe2 --- /dev/null +++ b/sdk/bindings/HttpDeleteAccountStartRequest.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. + +export interface HttpDeleteAccountStartRequest { + device: string + browser: string +} diff --git a/sdk/bindings/VerificationAction.ts b/sdk/bindings/VerificationAction.ts index c7637845..aa133d19 100644 --- a/sdk/bindings/VerificationAction.ts +++ b/sdk/bindings/VerificationAction.ts @@ -1,3 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type VerificationAction = "registerPassword" | "registerPasskey" | "resetPassword" | "resetPasskey"; \ No newline at end of file +export type VerificationAction = + | 'registerPassword' + | 'registerPasskey' + | 'resetPassword' + | 'resetPasskey' + | 'deleteAccount' diff --git a/sdk/bindings/index.ts b/sdk/bindings/index.ts index 86d8a152..2aacfb35 100644 --- a/sdk/bindings/index.ts +++ b/sdk/bindings/index.ts @@ -1,162 +1,164 @@ // This file was auto-generated by bindings script. Do not edit this file manually. -export * from './AckMessage'; -export * from './AlreadyConnected'; -export * from './AppConnectEvent'; -export * from './AppDisconnectEvent'; -export * from './AppDisconnectedEvent'; -export * from './AppEvent'; -export * from './AppInfo'; -export * from './AppLinks'; -export * from './AppMetadata'; -export * from './AppToServer'; -export * from './ChangeNetworkEvent'; -export * from './ChangeNetworkResolveEvent'; -export * from './ChangeWalletEvent'; -export * from './ChangeWalletResolveEvent'; -export * from './ClientConnectEvent'; -export * from './ClientConnectResolveEvent'; -export * from './ClientDisconnectEvent'; -export * from './ClientInitializeRequest'; -export * from './ClientInitializeResponse'; -export * from './ClientToServer'; -export * from './CloudApiErrors'; -export * from './ConnectRequest'; -export * from './ConnectResponse'; -export * from './Deeplink'; -export * from './Device'; -export * from './DeviceMetadata'; -export * from './DomainVerificationStatus'; -export * from './DropSessionsRequest'; -export * from './DropSessionsResponse'; -export * from './ErrorMessage'; -export * from './EventData'; -export * from './EventType'; -export * from './GetInfoRequest'; -export * from './GetInfoResponse'; -export * from './GetPendingRequestsRequest'; -export * from './GetPendingRequestsResponse'; -export * from './GetSessionsRequest'; -export * from './GetSessionsResponse'; -export * from './HttpAcceptTeamInviteRequest'; -export * from './HttpAcceptTeamInviteResponse'; -export * from './HttpAddPasskeyFinishResponse'; -export * from './HttpCancelPendingDomainVerificationRequest'; -export * from './HttpCancelPendingDomainVerificationResponse'; -export * from './HttpCancelTeamUserInviteRequest'; -export * from './HttpCancelTeamUserInviteResponse'; -export * from './HttpCancelUserTeamInviteRequest'; -export * from './HttpCancelUserTeamInviteResponse'; -export * from './HttpChangeUsersPrivilegesRequest'; -export * from './HttpChangeUsersPrivilegesResponse'; -export * from './HttpCloudEndpoint'; -export * from './HttpConnectSessionRequest'; -export * from './HttpConnectSessionResponse'; -export * from './HttpDeleteAppRequest'; -export * from './HttpDeleteTeamRequest'; -export * from './HttpDropSessionsRequest'; -export * from './HttpDropSessionsResponse'; -export * from './HttpEndpoint'; -export * from './HttpGetAppEventsRequest'; -export * from './HttpGetAppEventsResponse'; -export * from './HttpGetPendingRequestRequest'; -export * from './HttpGetPendingRequestResponse'; -export * from './HttpGetPendingRequestsRequest'; -export * from './HttpGetPendingRequestsResponse'; -export * from './HttpGetSessionInfoRequest'; -export * from './HttpGetSessionInfoResponse'; -export * from './HttpGetSessionsRequest'; -export * from './HttpGetSessionsResponse'; -export * from './HttpGetTeamMetadataRequest'; -export * from './HttpGetTeamMetadataResponse'; -export * from './HttpGetTeamUserInvitesRequest'; -export * from './HttpGetTeamUserInvitesResponse'; -export * from './HttpGetTeamUsersPrivilegesRequest'; -export * from './HttpGetTeamUsersPrivilegesResponse'; -export * from './HttpGetUserJoinedTeamsResponse'; -export * from './HttpGetUserTeamInvitesResponse'; -export * from './HttpInviteUserToTeamRequest'; -export * from './HttpInviteUserToTeamResponse'; -export * from './HttpLeaveTeamRequest'; -export * from './HttpLeaveTeamResponse'; -export * from './HttpLoginRequest'; -export * from './HttpLoginResponse'; -export * from './HttpLoginWithGoogleRequest'; -export * from './HttpLoginWithGoogleResponse'; -export * from './HttpLoginWithPasskeyFinishResponse'; -export * from './HttpLoginWithPasskeyStartRequest'; -export * from './HttpNightlyConnectCloudEvent'; -export * from './HttpRefreshRequest'; -export * from './HttpRefreshResponse'; -export * from './HttpRegisterNewAppRequest'; -export * from './HttpRegisterNewAppResponse'; -export * from './HttpRegisterNewTeamRequest'; -export * from './HttpRegisterNewTeamResponse'; -export * from './HttpRegisterWithPasskeyFinishResponse'; -export * from './HttpRegisterWithPasskeyStartRequest'; -export * from './HttpRegisterWithPasswordFinishRequest'; -export * from './HttpRegisterWithPasswordFinishResponse'; -export * from './HttpRegisterWithPasswordStartRequest'; -export * from './HttpRegisterWithPasswordStartResponse'; -export * from './HttpRemoveUserFromTeamRequest'; -export * from './HttpRemoveUserFromTeamResponse'; -export * from './HttpRemoveWhitelistedDomainRequest'; -export * from './HttpRemoveWhitelistedDomainResponse'; -export * from './HttpResetPasskeyFinishResponse'; -export * from './HttpResetPasskeyStartRequest'; -export * from './HttpResetPasswordFinishRequest'; -export * from './HttpResetPasswordFinishResponse'; -export * from './HttpResetPasswordStartRequest'; -export * from './HttpResetPasswordStartResponse'; -export * from './HttpResolveRequestRequest'; -export * from './HttpResolveRequestResponse'; -export * from './HttpUserMetadataResponse'; -export * from './HttpVerifyCodeRequest'; -export * from './HttpVerifyCodeResponse'; -export * from './HttpVerifyDomainFinishRequest'; -export * from './HttpVerifyDomainFinishResponse'; -export * from './HttpVerifyDomainStartRequest'; -export * from './HttpVerifyDomainStartResponse'; -export * from './Images'; -export * from './InitializeRequest'; -export * from './InitializeResponse'; -export * from './JoinedTeam'; -export * from './MobileMetadata'; -export * from './Network'; -export * from './NewPayloadEvent'; -export * from './NewPayloadEventReply'; -export * from './NewUserPrivilegeLevel'; -export * from './NightlyError'; -export * from './Notification'; -export * from './NotificationPayload'; -export * from './PaginationCursor'; -export * from './PendingRequest'; -export * from './Platform'; -export * from './PrivilegeChange'; -export * from './PrivilegeLevel'; -export * from './RequestFail'; -export * from './RequestPayload'; -export * from './RequestType'; -export * from './ResponsePayload'; -export * from './ServerToApp'; -export * from './ServerToClient'; -export * from './SessionStatus'; -export * from './SessionType'; -export * from './SignAndSendTransactionEvent'; -export * from './SignAndSendTransactionResolveEvent'; -export * from './SignMessageEvent'; -export * from './SignMessageResolveEvent'; -export * from './SignTransactionEvent'; -export * from './SignTransactionResolveEvent'; -export * from './Subscription'; -export * from './TeamInvite'; -export * from './TeamMetadata'; -export * from './TeamUserPrivilege'; -export * from './UserConnectedEvent'; -export * from './UserDisconnectedEvent'; -export * from './UserPrivilege'; -export * from './VerificationAction'; -export * from './Version'; -export * from './WalletMetadata'; -export * from './WalletType'; -export * from './WebMetadata'; -export * from './WhitelistedDomain'; +export * from './AckMessage' +export * from './AlreadyConnected' +export * from './AppConnectEvent' +export * from './AppDisconnectEvent' +export * from './AppDisconnectedEvent' +export * from './AppEvent' +export * from './AppInfo' +export * from './AppLinks' +export * from './AppMetadata' +export * from './AppToServer' +export * from './ChangeNetworkEvent' +export * from './ChangeNetworkResolveEvent' +export * from './ChangeWalletEvent' +export * from './ChangeWalletResolveEvent' +export * from './ClientConnectEvent' +export * from './ClientConnectResolveEvent' +export * from './ClientDisconnectEvent' +export * from './ClientInitializeRequest' +export * from './ClientInitializeResponse' +export * from './ClientToServer' +export * from './CloudApiErrors' +export * from './ConnectRequest' +export * from './ConnectResponse' +export * from './Deeplink' +export * from './Device' +export * from './DeviceMetadata' +export * from './DomainVerificationStatus' +export * from './DropSessionsRequest' +export * from './DropSessionsResponse' +export * from './ErrorMessage' +export * from './EventData' +export * from './EventType' +export * from './GetInfoRequest' +export * from './GetInfoResponse' +export * from './GetPendingRequestsRequest' +export * from './GetPendingRequestsResponse' +export * from './GetSessionsRequest' +export * from './GetSessionsResponse' +export * from './HttpAcceptTeamInviteRequest' +export * from './HttpAcceptTeamInviteResponse' +export * from './HttpAddPasskeyFinishResponse' +export * from './HttpCancelPendingDomainVerificationRequest' +export * from './HttpCancelPendingDomainVerificationResponse' +export * from './HttpCancelTeamUserInviteRequest' +export * from './HttpCancelTeamUserInviteResponse' +export * from './HttpCancelUserTeamInviteRequest' +export * from './HttpCancelUserTeamInviteResponse' +export * from './HttpChangeUsersPrivilegesRequest' +export * from './HttpChangeUsersPrivilegesResponse' +export * from './HttpCloudEndpoint' +export * from './HttpConnectSessionRequest' +export * from './HttpConnectSessionResponse' +export * from './HttpDeleteAccountFinishRequest' +export * from './HttpDeleteAccountStartRequest' +export * from './HttpDeleteAppRequest' +export * from './HttpDeleteTeamRequest' +export * from './HttpDropSessionsRequest' +export * from './HttpDropSessionsResponse' +export * from './HttpEndpoint' +export * from './HttpGetAppEventsRequest' +export * from './HttpGetAppEventsResponse' +export * from './HttpGetPendingRequestRequest' +export * from './HttpGetPendingRequestResponse' +export * from './HttpGetPendingRequestsRequest' +export * from './HttpGetPendingRequestsResponse' +export * from './HttpGetSessionInfoRequest' +export * from './HttpGetSessionInfoResponse' +export * from './HttpGetSessionsRequest' +export * from './HttpGetSessionsResponse' +export * from './HttpGetTeamMetadataRequest' +export * from './HttpGetTeamMetadataResponse' +export * from './HttpGetTeamUserInvitesRequest' +export * from './HttpGetTeamUserInvitesResponse' +export * from './HttpGetTeamUsersPrivilegesRequest' +export * from './HttpGetTeamUsersPrivilegesResponse' +export * from './HttpGetUserJoinedTeamsResponse' +export * from './HttpGetUserTeamInvitesResponse' +export * from './HttpInviteUserToTeamRequest' +export * from './HttpInviteUserToTeamResponse' +export * from './HttpLeaveTeamRequest' +export * from './HttpLeaveTeamResponse' +export * from './HttpLoginRequest' +export * from './HttpLoginResponse' +export * from './HttpLoginWithGoogleRequest' +export * from './HttpLoginWithGoogleResponse' +export * from './HttpLoginWithPasskeyFinishResponse' +export * from './HttpLoginWithPasskeyStartRequest' +export * from './HttpNightlyConnectCloudEvent' +export * from './HttpRefreshRequest' +export * from './HttpRefreshResponse' +export * from './HttpRegisterNewAppRequest' +export * from './HttpRegisterNewAppResponse' +export * from './HttpRegisterNewTeamRequest' +export * from './HttpRegisterNewTeamResponse' +export * from './HttpRegisterWithPasskeyFinishResponse' +export * from './HttpRegisterWithPasskeyStartRequest' +export * from './HttpRegisterWithPasswordFinishRequest' +export * from './HttpRegisterWithPasswordFinishResponse' +export * from './HttpRegisterWithPasswordStartRequest' +export * from './HttpRegisterWithPasswordStartResponse' +export * from './HttpRemoveUserFromTeamRequest' +export * from './HttpRemoveUserFromTeamResponse' +export * from './HttpRemoveWhitelistedDomainRequest' +export * from './HttpRemoveWhitelistedDomainResponse' +export * from './HttpResetPasskeyFinishResponse' +export * from './HttpResetPasskeyStartRequest' +export * from './HttpResetPasswordFinishRequest' +export * from './HttpResetPasswordFinishResponse' +export * from './HttpResetPasswordStartRequest' +export * from './HttpResetPasswordStartResponse' +export * from './HttpResolveRequestRequest' +export * from './HttpResolveRequestResponse' +export * from './HttpUserMetadataResponse' +export * from './HttpVerifyCodeRequest' +export * from './HttpVerifyCodeResponse' +export * from './HttpVerifyDomainFinishRequest' +export * from './HttpVerifyDomainFinishResponse' +export * from './HttpVerifyDomainStartRequest' +export * from './HttpVerifyDomainStartResponse' +export * from './Images' +export * from './InitializeRequest' +export * from './InitializeResponse' +export * from './JoinedTeam' +export * from './MobileMetadata' +export * from './Network' +export * from './NewPayloadEvent' +export * from './NewPayloadEventReply' +export * from './NewUserPrivilegeLevel' +export * from './NightlyError' +export * from './Notification' +export * from './NotificationPayload' +export * from './PaginationCursor' +export * from './PendingRequest' +export * from './Platform' +export * from './PrivilegeChange' +export * from './PrivilegeLevel' +export * from './RequestFail' +export * from './RequestPayload' +export * from './RequestType' +export * from './ResponsePayload' +export * from './ServerToApp' +export * from './ServerToClient' +export * from './SessionStatus' +export * from './SessionType' +export * from './SignAndSendTransactionEvent' +export * from './SignAndSendTransactionResolveEvent' +export * from './SignMessageEvent' +export * from './SignMessageResolveEvent' +export * from './SignTransactionEvent' +export * from './SignTransactionResolveEvent' +export * from './Subscription' +export * from './TeamInvite' +export * from './TeamMetadata' +export * from './TeamUserPrivilege' +export * from './UserConnectedEvent' +export * from './UserDisconnectedEvent' +export * from './UserPrivilege' +export * from './VerificationAction' +export * from './Version' +export * from './WalletMetadata' +export * from './WalletType' +export * from './WebMetadata' +export * from './WhitelistedDomain' diff --git a/sdk/packages/cloud/package.json b/sdk/packages/cloud/package.json index d5172f53..edb89b98 100644 --- a/sdk/packages/cloud/package.json +++ b/sdk/packages/cloud/package.json @@ -1,6 +1,6 @@ { "name": "@nightlylabs/nightly-cloud", - "version": "0.0.28", + "version": "0.0.33", "type": "module", "exports": { ".": { @@ -47,4 +47,4 @@ "cross-fetch": "^3.1.6", "uuid": "^9.0.0" } -} \ No newline at end of file +} diff --git a/sdk/packages/cloud/src/app.ts b/sdk/packages/cloud/src/app.ts index 51374bee..6ab065ad 100644 --- a/sdk/packages/cloud/src/app.ts +++ b/sdk/packages/cloud/src/app.ts @@ -9,6 +9,8 @@ import { HttpChangeUsersPrivilegesRequest, HttpChangeUsersPrivilegesResponse, HttpCloudEndpoint, + HttpDeleteAccountFinishRequest, + HttpDeleteAccountStartRequest, HttpDeleteAppRequest, HttpDeleteTeamRequest, HttpGetAppEventsRequest, @@ -239,6 +241,16 @@ export class NightlyCloud { return response } + ///////////////////////////////////////////////////// Delete account + + deleteAccountStart = async (request: HttpDeleteAccountStartRequest): Promise => { + await this.sendPostJson('/delete_account_start', EndpointType.Private, request) + } + + deleteAccountFinish = async (request: HttpDeleteAccountFinishRequest): Promise => { + await this.sendPostJson('/delete_account_finish', EndpointType.Private, request) + } + ///////////////////////////////////////////////////// Login loginWithPassword = async (request: HttpLoginRequest): Promise => { diff --git a/server/bindings/CloudApiErrors.ts b/server/bindings/CloudApiErrors.ts index 273cba52..09aa1d84 100644 --- a/server/bindings/CloudApiErrors.ts +++ b/server/bindings/CloudApiErrors.ts @@ -1,3 +1,53 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type CloudApiErrors = "TeamDoesNotExist" | "UserDoesNotExist" | "CloudFeatureDisabled" | "InsufficientPermissions" | "TeamHasNoRegisteredApps" | "DatabaseError" | "MaximumUsersPerTeamReached" | "UserAlreadyBelongsToTheTeam" | "IncorrectPassword" | "AccessTokenFailure" | "RefreshTokenFailure" | "AppAlreadyExists" | "MaximumAppsPerTeamReached" | "TeamAlreadyExists" | "PersonalTeamAlreadyExists" | "EmailAlreadyExists" | "InternalServerError" | "UserDoesNotBelongsToTheTeam" | "InvalidName" | "UnauthorizedOriginError" | "AppDoesNotExist" | "UserAlreadyInvitedToTheTeam" | "MaximumInvitesPerTeamReached" | "InviteNotFound" | "ActionForbiddenForPersonalTeam" | "InviteDoesNotExist" | "InvalidPaginationCursor" | "InvalidOrExpiredVerificationCode" | "InvalidOrExpiredAuthCode" | "InvalidDomainName" | "DomainAlreadyVerified" | "DomainVerificationFailure" | "DomainNotFound" | "DomainVerificationNotStarted" | "DomainAlreadyVerifiedByAnotherApp" | "NoPendingDomainVerification" | "WebAuthnError" | "PasswordNotSet" | "UserDoesNotHavePasskey" | "PasskeyAlreadyExists" | "InvalidPasskeyCredential" | "PasskeyDoesNotExist" | "FailedToCreateTeam" | "DashboardImportFail" | "OriginHeaderRequired" | "InvalidOrigin" | "InvalidAction" | "AdminCannotLeaveTeam" | "GrafanaError"; \ No newline at end of file +export type CloudApiErrors = + | 'TeamDoesNotExist' + | 'UserDoesNotExist' + | 'CloudFeatureDisabled' + | 'InsufficientPermissions' + | 'TeamHasNoRegisteredApps' + | 'DatabaseError' + | 'MaximumUsersPerTeamReached' + | 'UserAlreadyBelongsToTheTeam' + | 'IncorrectPassword' + | 'AccessTokenFailure' + | 'RefreshTokenFailure' + | 'AppAlreadyExists' + | 'MaximumAppsPerTeamReached' + | 'TeamAlreadyExists' + | 'PersonalTeamAlreadyExists' + | 'EmailAlreadyExists' + | 'InternalServerError' + | 'UserDoesNotBelongsToTheTeam' + | 'InvalidName' + | 'UnauthorizedOriginError' + | 'AppDoesNotExist' + | 'UserAlreadyInvitedToTheTeam' + | 'MaximumInvitesPerTeamReached' + | 'InviteNotFound' + | 'ActionForbiddenForPersonalTeam' + | 'InviteDoesNotExist' + | 'InvalidPaginationCursor' + | 'InvalidOrExpiredVerificationCode' + | 'InvalidOrExpiredAuthCode' + | 'InvalidDomainName' + | 'DomainAlreadyVerified' + | 'DomainVerificationFailure' + | 'DomainNotFound' + | 'DomainVerificationNotStarted' + | 'DomainAlreadyVerifiedByAnotherApp' + | 'NoPendingDomainVerification' + | 'WebAuthnError' + | 'PasswordNotSet' + | 'UserDoesNotHavePasskey' + | 'PasskeyAlreadyExists' + | 'InvalidPasskeyCredential' + | 'PasskeyDoesNotExist' + | 'FailedToCreateTeam' + | 'DashboardImportFail' + | 'OriginHeaderRequired' + | 'InvalidOrigin' + | 'InvalidAction' + | 'AdminCannotLeaveTeam' + | 'GrafanaError' + | 'TeamWithoutGrafanaId' diff --git a/server/bindings/HttpCloudEndpoint.ts b/server/bindings/HttpCloudEndpoint.ts index 3061c445..26ee84a8 100644 --- a/server/bindings/HttpCloudEndpoint.ts +++ b/server/bindings/HttpCloudEndpoint.ts @@ -1,3 +1,46 @@ // 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_start" | "/register_with_password_finish" | "/login_with_password" | "/login_with_google" | "/refresh_token" | "/register_new_team" | "/remove_user_from_team" | "/get_user_joined_teams" | "/events" | "/invite_user_to_team" | "/accept_team_invite" | "/get_team_user_invites" | "/get_user_team_invites" | "/cancel_team_user_invite" | "/cancel_user_team_invite" | "/get_app_events" | "/reset_password_start" | "/reset_password_finish" | "/verify_domain_start" | "/verify_domain_finish" | "/remove_whitelisted_domain" | "/cancel_pending_domain_verification" | "/register_with_passkey_start" | "/register_with_passkey_finish" | "/reset_passkey_start" | "/reset_passkey_finish" | "/get_passkey_challenge" | "/delete_passkey" | "/add_passkey_start" | "/add_passkey_finish" | "/get_user_metadata" | "/get_team_metadata" | "/get_team_users_privileges" | "/change_user_privileges" | "/login_with_passkey_start" | "/login_with_passkey_finish" | "/verify_code" | "/leave_team" | "/delete_app" | "/delete_team"; \ No newline at end of file +export type HttpCloudEndpoint = + | '/register_new_app' + | '/register_with_password_start' + | '/register_with_password_finish' + | '/login_with_password' + | '/login_with_google' + | '/refresh_token' + | '/register_new_team' + | '/remove_user_from_team' + | '/get_user_joined_teams' + | '/events' + | '/invite_user_to_team' + | '/accept_team_invite' + | '/get_team_user_invites' + | '/get_user_team_invites' + | '/cancel_team_user_invite' + | '/cancel_user_team_invite' + | '/get_app_events' + | '/reset_password_start' + | '/reset_password_finish' + | '/verify_domain_start' + | '/verify_domain_finish' + | '/remove_whitelisted_domain' + | '/cancel_pending_domain_verification' + | '/register_with_passkey_start' + | '/register_with_passkey_finish' + | '/reset_passkey_start' + | '/reset_passkey_finish' + | '/get_passkey_challenge' + | '/delete_passkey' + | '/add_passkey_start' + | '/add_passkey_finish' + | '/get_user_metadata' + | '/get_team_metadata' + | '/get_team_users_privileges' + | '/change_user_privileges' + | '/login_with_passkey_start' + | '/login_with_passkey_finish' + | '/verify_code' + | '/leave_team' + | '/delete_app' + | '/delete_team' + | '/delete_account_start' + | '/delete_account_finish' diff --git a/server/bindings/HttpDeleteAccountFinishRequest.ts b/server/bindings/HttpDeleteAccountFinishRequest.ts new file mode 100644 index 00000000..415d900f --- /dev/null +++ b/server/bindings/HttpDeleteAccountFinishRequest.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface HttpDeleteAccountFinishRequest { + authCode: string +} diff --git a/server/bindings/HttpDeleteAccountStartRequest.ts b/server/bindings/HttpDeleteAccountStartRequest.ts new file mode 100644 index 00000000..f6bfabe2 --- /dev/null +++ b/server/bindings/HttpDeleteAccountStartRequest.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. + +export interface HttpDeleteAccountStartRequest { + device: string + browser: string +} diff --git a/server/bindings/VerificationAction.ts b/server/bindings/VerificationAction.ts index c7637845..aa133d19 100644 --- a/server/bindings/VerificationAction.ts +++ b/server/bindings/VerificationAction.ts @@ -1,3 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type VerificationAction = "registerPassword" | "registerPasskey" | "resetPassword" | "resetPasskey"; \ No newline at end of file +export type VerificationAction = + | 'registerPassword' + | 'registerPasskey' + | 'resetPassword' + | 'resetPasskey' + | 'deleteAccount' diff --git a/server/src/http/cloud/accept_team_invite.rs b/server/src/http/cloud/accept_team_invite.rs index 8b475403..2dd3ebea 100644 --- a/server/src/http/cloud/accept_team_invite.rs +++ b/server/src/http/cloud/accept_team_invite.rs @@ -100,9 +100,8 @@ pub async fn accept_team_invite( )); } } - - let grafana_team_id = match db.get_team_by_team_id(None, &request.team_id).await { - Ok(Some(team)) => team.grafana_id, + let team = match db.get_team_by_team_id(None, &request.team_id).await { + Ok(Some(team)) => team, Ok(None) => { return Err(( StatusCode::BAD_REQUEST, @@ -117,8 +116,19 @@ pub async fn accept_team_invite( )); } }; + // Grafana add user to the team if is_env_production() { + let grafana_team_id = match team.grafana_id { + Some(grafana_id) => grafana_id, + None => { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::TeamWithoutGrafanaId.to_string(), + )); + } + }; + if let Err(err) = handle_grafana_add_user_to_team(&grafana_conf, &grafana_team_id, &user.email).await { diff --git a/server/src/http/cloud/delete_account_finish.rs b/server/src/http/cloud/delete_account_finish.rs new file mode 100644 index 00000000..a6551253 --- /dev/null +++ b/server/src/http/cloud/delete_account_finish.rs @@ -0,0 +1,158 @@ +use crate::{ + http::cloud::utils::{check_auth_code, validate_request}, + middlewares::auth_middleware::UserId, + structs::{ + cloud::api_cloud_errors::CloudApiErrors, + session_cache::{ApiSessionsCache, SessionCache, SessionsCacheKey}, + }, +}; +use axum::{extract::State, http::StatusCode, Extension, Json}; +use database::db::Db; +use garde::Validate; +use log::error; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use ts_rs::TS; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Validate)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct HttpDeleteAccountFinishRequest { + #[garde(skip)] + pub auth_code: String, +} + +pub async fn delete_account_finish( + State(db): State>, + State(sessions_cache): State>, + Extension(user_id): Extension, + Json(request): Json, +) -> Result, (StatusCode, String)> { + // Validate request + validate_request(&request, &())?; + + // check if user exists + let user = match db.get_user_by_user_id(&user_id).await { + Ok(Some(user)) => user, + Ok(None) => { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotExist.to_string(), + )); + } + Err(err) => { + error!("Failed to get user: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + }; + + // Get session data + let sessions_key = SessionsCacheKey::DeleteAccount(user.email.clone()).to_string(); + let session_data = match sessions_cache.get(&sessions_key) { + Some(SessionCache::DeleteAccount(session)) => session, + _ => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::InternalServerError.to_string(), + )); + } + }; + + // Validate auth code + if !check_auth_code( + &request.auth_code, + &session_data.authentication_code, + session_data.created_at, + ) { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::InvalidOrExpiredAuthCode.to_string(), + )); + } + + // Remove leftover session data + sessions_cache.remove(&sessions_key); + + // Start transaction to update users privileges + let mut tx = db.connection_pool.begin().await.map_err(|err| { + error!("Failed to start transaction: {:?}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + ) + })?; + + // Delete all invites connected to user + if let Err(err) = db + .cancel_all_team_invites_containing_email(&mut tx, &user.email, &user_id) + .await + { + error!("Failed to delete team invites: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + + // Delete all user apps + if let Err(err) = db.deactivate_user_apps(&mut tx, &user_id).await { + error!("Failed to delete user apps: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + + // Leave all teams + if let Err(err) = db.remove_inactive_user_from_teams(&mut tx, &user_id).await { + error!("Failed to leave teams: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + + // delete privileges + if let Err(err) = db + .remove_privileges_for_inactive_teams(&mut tx, &user_id) + .await + { + error!("Failed to leave teams: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + + // Delete all user teams + if let Err(err) = db.delete_all_user_teams(&mut tx, &user_id).await { + error!("Failed to delete user teams: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + + // Deactivate the user + if let Err(err) = db.deactivate_user(&user_id, &mut tx).await { + error!("Failed to delete user: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + + // Commit transaction + tx.commit().await.map_err(|err| { + error!("Failed to commit transaction: {:?}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + ) + })?; + + return Ok(Json(())); +} diff --git a/server/src/http/cloud/delete_account_start.rs b/server/src/http/cloud/delete_account_start.rs new file mode 100644 index 00000000..22c531bd --- /dev/null +++ b/server/src/http/cloud/delete_account_start.rs @@ -0,0 +1,151 @@ +use crate::{ + mailer::{ + mail_requests::{DeleteAccountNotification, SendEmailRequest}, + mailer::Mailer, + }, + middlewares::auth_middleware::UserId, + structs::{ + cloud::api_cloud_errors::CloudApiErrors, + session_cache::{ + ApiSessionsCache, DeleteAccountVerification, SessionCache, SessionsCacheKey, + }, + }, + test_env::is_test_env, + utils::get_timestamp_in_milliseconds, +}; +use axum::{extract::State, http::StatusCode, Extension, Json}; +use database::db::Db; +use garde::Validate; +use log::error; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use ts_rs::TS; + +use super::utils::generate_verification_code; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Validate)] +#[ts(export)] +pub struct HttpDeleteAccountStartRequest { + #[garde(alphanumeric)] + pub device: String, + #[garde(alphanumeric)] + pub browser: String, +} + +pub async fn delete_account_start( + State(db): State>, + State(mailer): State>, + State(sessions_cache): State>, + Extension(user_id): Extension, + Json(request): Json, +) -> Result, (StatusCode, String)> { + // Get user data + let user_data = match db.get_user_by_user_id(&user_id).await { + Ok(Some(user_data)) => user_data, + Ok(None) => { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotExist.to_string(), + )) + } + Err(err) => { + error!( + "Failed to check if user exists: {:?}, user_id: {}", + err, user_id + ); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + }; + + // Save to cache delete account challenge request + let sessions_key = SessionsCacheKey::DeleteAccount(user_data.email.clone()).to_string(); + + // Remove leftover session data + sessions_cache.remove(&sessions_key); + + // Generate verification code, if not in production use a static code + let verification_code = generate_verification_code(); + + // Save the challenge to the cache + sessions_cache.set( + sessions_key, + SessionCache::DeleteAccount(DeleteAccountVerification { + email: user_data.email.clone(), + verification_code: verification_code.clone(), + authentication_code: None, + created_at: get_timestamp_in_milliseconds(), + }), + None, + ); + + if !is_test_env() { + // Send code via email + let request = SendEmailRequest::DeleteAccount(DeleteAccountNotification { + email: user_data.email, + code: verification_code, + device: request.device.clone(), + browser: request.browser.clone(), + }); + + // It doesn't matter if this fails + mailer.handle_email_request(&request); + } + + return Ok(Json(())); +} + +#[cfg(feature = "cloud_intsegration_tests")] +#[cfg(test)] +mod tests { + use crate::{ + env::JWT_SECRET, + http::cloud::delete_account_start::HttpDeleteAccountStartRequest, + structs::cloud::cloud_http_endpoints::HttpCloudEndpoint, + test_utils::test_utils::{ + convert_response, create_test_app, 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_delete_account() { + let test_app = create_test_app(false).await; + + let (auth_token, _email, _password) = register_and_login_random_user(&test_app).await; + + let request = HttpDeleteAccountStartRequest { + device: "device".to_string(), + browser: "browser".to_string(), + }; + + 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::DeleteAccountStart.to_string() + )) + .extension(ip) + .body(Body::from(json)) + .unwrap(); + + // Send request + let response = test_app.clone().oneshot(req).await.unwrap(); + // Validate response + convert_response::<()>(response).await.unwrap(); + } +} diff --git a/server/src/http/cloud/delete_team.rs b/server/src/http/cloud/delete_team.rs index 44a68fd0..752a9a03 100644 --- a/server/src/http/cloud/delete_team.rs +++ b/server/src/http/cloud/delete_team.rs @@ -153,7 +153,17 @@ pub async fn delete_team( // Grafana, delete team // TODO, fix this by fixing methods for setting up grafana datasource if is_env_production() { - if let Err(err) = handle_grafana_delete_team(&grafana_conf, &team.grafana_id).await { + let team_grafana_id = match team.grafana_id { + Some(grafana_id) => grafana_id, + None => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::TeamWithoutGrafanaId.to_string(), + )); + } + }; + + if let Err(err) = handle_grafana_delete_team(&grafana_conf, &team_grafana_id).await { error!("Failed to delete team from grafana: {:?}", err); return Err(( StatusCode::INTERNAL_SERVER_ERROR, diff --git a/server/src/http/cloud/get_user_joined_teams.rs b/server/src/http/cloud/get_user_joined_teams.rs index 299ae389..8b3b8630 100644 --- a/server/src/http/cloud/get_user_joined_teams.rs +++ b/server/src/http/cloud/get_user_joined_teams.rs @@ -76,7 +76,6 @@ pub async fn get_user_joined_teams( // Parse joined team let joined_team = JoinedTeam { team_id: team.team_id.clone(), - grafana_id: team.grafana_id, team_name: team.team_name, created_at: team.registration_timestamp, creator_email: admin_email, diff --git a/server/src/http/cloud/leave_team.rs b/server/src/http/cloud/leave_team.rs index 7f9c7d5f..113c79df 100644 --- a/server/src/http/cloud/leave_team.rs +++ b/server/src/http/cloud/leave_team.rs @@ -106,9 +106,19 @@ pub async fn leave_team( // Grafana, remove user from the team if is_env_production() { + let team_grafana_id = match team.grafana_id { + Some(grafana_id) => grafana_id, + None => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::TeamWithoutGrafanaId.to_string(), + )); + } + }; + if let Err(err) = handle_grafana_remove_user_from_team( &grafana_conf, - &team.grafana_id, + &team_grafana_id, &user.email, ) .await diff --git a/server/src/http/cloud/mod.rs b/server/src/http/cloud/mod.rs index 52e07ded..fc593519 100644 --- a/server/src/http/cloud/mod.rs +++ b/server/src/http/cloud/mod.rs @@ -4,6 +4,8 @@ pub mod add_passkey_start; pub mod cancel_team_user_invite; pub mod cancel_user_team_invite; pub mod change_user_privileges; +pub mod delete_account_finish; +pub mod delete_account_start; pub mod delete_app; pub mod delete_passkey; pub mod delete_team; diff --git a/server/src/http/cloud/register_new_app.rs b/server/src/http/cloud/register_new_app.rs index 858926fd..78d25963 100644 --- a/server/src/http/cloud/register_new_app.rs +++ b/server/src/http/cloud/register_new_app.rs @@ -112,11 +112,21 @@ pub async fn register_new_app( // Grafana, add new app // TODO, fix this by fixing methods for setting up grafana datasource if is_env_production() { + let team_grafana_id = match team.grafana_id { + Some(grafana_id) => grafana_id, + None => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::TeamWithoutGrafanaId.to_string(), + )); + } + }; + if let Err(err) = handle_grafana_create_new_app( &grafana_conf, &request.app_name, &app_id, - &team.grafana_id, + &team_grafana_id, ) .await { diff --git a/server/src/http/cloud/register_new_team.rs b/server/src/http/cloud/register_new_team.rs index d2e460c2..1df882cf 100644 --- a/server/src/http/cloud/register_new_team.rs +++ b/server/src/http/cloud/register_new_team.rs @@ -122,7 +122,7 @@ pub async fn register_new_team( )); } }; - let mut grafana_team_id: i64 = 0; + let mut grafana_team_id: Option = None; // Grafana, add new team if is_env_production() { grafana_team_id = match handle_grafana_create_new_team( @@ -132,7 +132,7 @@ pub async fn register_new_team( ) .await { - Ok(id) => id, + Ok(id) => Some(id.to_string()), Err(err) => { error!("Failed to create team in grafana: {:?}", err); return Err(( @@ -148,7 +148,7 @@ pub async fn register_new_team( // Create a new team let team = Team { team_id: team_id.to_string(), - grafana_id: grafana_team_id.to_string(), + grafana_id: grafana_team_id, team_name: request.team_name.clone(), team_admin_id: user_id.clone(), subscription: None, diff --git a/server/src/http/cloud/remove_user_from_team.rs b/server/src/http/cloud/remove_user_from_team.rs index 51ba31d7..77170918 100644 --- a/server/src/http/cloud/remove_user_from_team.rs +++ b/server/src/http/cloud/remove_user_from_team.rs @@ -108,9 +108,19 @@ pub async fn remove_user_from_team( // Grafana, remove user from the team if is_env_production() { + let team_grafana_id = match team.grafana_id { + Some(grafana_id) => grafana_id, + None => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::TeamWithoutGrafanaId.to_string(), + )); + } + }; + if let Err(err) = handle_grafana_remove_user_from_team( &grafana_conf, - &team.grafana_id, + &team_grafana_id, &request.user_email, ) .await diff --git a/server/src/http/cloud/verify_code.rs b/server/src/http/cloud/verify_code.rs index 1eeb7e79..584d24c0 100644 --- a/server/src/http/cloud/verify_code.rs +++ b/server/src/http/cloud/verify_code.rs @@ -122,6 +122,25 @@ pub async fn verify_code( )); } } + Some(SessionCache::DeleteAccount(mut session)) => { + // Check if code is correct and not expired + if check_verification_code( + &session.verification_code, + &request.code, + session.created_at, + ) { + // Generate authentication code + let (auth_code, encrypted_auth_code) = generate_authentication_code(); + session.authentication_code = Some(encrypted_auth_code); + + (auth_code, SessionCache::DeleteAccount(session)) + } else { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::InvalidOrExpiredVerificationCode.to_string(), + )); + } + } _ => { return Err(( StatusCode::BAD_REQUEST, diff --git a/server/src/mailer/mail_requests.rs b/server/src/mailer/mail_requests.rs index 216f0126..178b9f03 100644 --- a/server/src/mailer/mail_requests.rs +++ b/server/src/mailer/mail_requests.rs @@ -7,6 +7,7 @@ pub enum SendEmailRequest { TeamInvite(TeamInviteNotification), TeamRemoval(TeamRemovalNotification), LeaveTeam(TeamLeavingNotification), + DeleteAccount(DeleteAccountNotification), } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -51,3 +52,11 @@ pub struct TeamLeavingNotification { pub device: String, pub browser: String, } + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct DeleteAccountNotification { + pub email: String, + pub code: String, + pub device: String, + pub browser: String, +} diff --git a/server/src/mailer/request_handler/handle_requests.rs b/server/src/mailer/request_handler/handle_requests.rs index 892cbddb..30af4b66 100644 --- a/server/src/mailer/request_handler/handle_requests.rs +++ b/server/src/mailer/request_handler/handle_requests.rs @@ -3,10 +3,7 @@ use database::tables::utils::get_current_datetime; use log::error; use super::processors::{ - email_confirmation::send_email_confirmation, reset_password::send_password_reset, - team_invite_notification::send_team_invite_notification, - team_leaving_notification::send_team_leaving_notification, - team_removal_notification::send_team_removal_notification, + account_removal_notification::send_account_removal_notification, email_confirmation::send_email_confirmation, reset_password::send_password_reset, team_invite_notification::send_team_invite_notification, team_leaving_notification::send_team_leaving_notification, team_removal_notification::send_team_removal_notification }; use crate::{ env::MAILER_ACTIVE, @@ -115,6 +112,21 @@ impl Mailer { error!("Failed to send email: {:?}, request: {:?}", err, request); } } + SendEmailRequest::DeleteAccount(request) => { + let date = get_date(); + if let Some(err) = send_account_removal_notification( + &templates, + mailbox.clone(), + &mail_sender, + &request, + date.0, + date.1, + ) + .error_message + { + error!("Failed to send email: {:?}, request: {:?}", err, request); + } + } } }); return; diff --git a/server/src/mailer/request_handler/processors/account_removal_notification.rs b/server/src/mailer/request_handler/processors/account_removal_notification.rs new file mode 100644 index 00000000..1e30a5f7 --- /dev/null +++ b/server/src/mailer/request_handler/processors/account_removal_notification.rs @@ -0,0 +1,63 @@ +use crate::mailer::{ + mail_requests::{DeleteAccountNotification, SendEmailResponse}, + request_handler::utils::create_message, + templates::templates::Templates, +}; +use lettre::{message::Mailbox, SmtpTransport, Transport}; +use log::{error, warn}; +use std::{collections::HashMap, sync::Arc}; + +pub fn send_account_removal_notification( + templates: &Arc>, + mailbox: Mailbox, + mail_sender: &Arc, + request: &DeleteAccountNotification, + date: String, + time: String, +) -> SendEmailResponse { + let html = match templates.get(&Templates::AccountRemovalNotification) { + Some(template) => template + .replace("EMAIL_ACTION_DEVICE", &request.device) + .replace("EMAIL_ACTION_BROWSER", &request.browser) + .replace("EMAIL_ACTION_DATE", &date) + .replace("EMAIL_ACTION_TIME", &time), + None => { + // Only possible if someone messes with the templates, print error and go along + error!( + "MAILER: Could not find account removal notification template under: {:?}", + Templates::AccountRemovalNotification + ); + return SendEmailResponse { + error_message: Some("Internal Error".to_string()), + }; + } + }; + match create_message( + html, + mailbox, + &request.email, + "Nightly Connect Cloud - Remove your account".to_string(), + ) { + Ok(message) => { + if let Err(e) = mail_sender.send(&message) { + warn!("MAILER: Failed to send account removal notification: {:?}", e); + return SendEmailResponse { + error_message: Some("Internal Error".to_string()), + }; + } else { + return SendEmailResponse { + error_message: None, + }; + } + } + Err(err) => { + warn!( + "MAILER: Failed to create account removal notification: {:?}", + err + ); + return SendEmailResponse { + error_message: Some("Internal Error".to_string()), + }; + } + } +} diff --git a/server/src/mailer/request_handler/processors/mod.rs b/server/src/mailer/request_handler/processors/mod.rs index aa57d0a3..b914bf3c 100644 --- a/server/src/mailer/request_handler/processors/mod.rs +++ b/server/src/mailer/request_handler/processors/mod.rs @@ -3,3 +3,4 @@ pub mod reset_password; pub mod team_invite_notification; pub mod team_leaving_notification; pub mod team_removal_notification; +pub mod account_removal_notification; \ No newline at end of file diff --git a/server/src/mailer/templates/templates.rs b/server/src/mailer/templates/templates.rs index 2354d46e..9ad7fed4 100644 --- a/server/src/mailer/templates/templates.rs +++ b/server/src/mailer/templates/templates.rs @@ -14,6 +14,7 @@ pub enum Templates { TeamInviteNotification, TeamRemovalNotification, TeamLeavingNotification, + AccountRemovalNotification, } pub fn get_templates() -> HashMap { @@ -39,6 +40,10 @@ pub fn get_templates() -> HashMap { Templates::TeamLeavingNotification, TEAM_LEAVING_NOTIFICATION_TEMPLATE.to_string(), ); + templates.insert( + Templates::AccountRemovalNotification, + TEAM_LEAVING_NOTIFICATION_TEMPLATE.to_string(), + ); templates } diff --git a/server/src/routes/cloud_router.rs b/server/src/routes/cloud_router.rs index d94c504e..24da0c3a 100644 --- a/server/src/routes/cloud_router.rs +++ b/server/src/routes/cloud_router.rs @@ -6,6 +6,8 @@ use crate::{ cancel_team_user_invite::cancel_team_user_invite, cancel_user_team_invite::cancel_user_team_invite, change_user_privileges::change_user_privileges, + delete_account_finish::delete_account_finish, + delete_account_start::delete_account_start, delete_app::delete_app, delete_passkey::delete_passkey, delete_team::delete_team, @@ -229,5 +231,13 @@ pub fn private_router(state: ServerState) -> Router { &HttpCloudEndpoint::DeleteTeam.to_string(), post(delete_team), ) + .route( + &HttpCloudEndpoint::DeleteAccountStart.to_string(), + post(delete_account_start), + ) + .route( + &HttpCloudEndpoint::DeleteAccountFinish.to_string(), + post(delete_account_finish), + ) .with_state(state) } diff --git a/server/src/structs/cloud/api_cloud_errors.rs b/server/src/structs/cloud/api_cloud_errors.rs index 80c730db..0689b0f8 100644 --- a/server/src/structs/cloud/api_cloud_errors.rs +++ b/server/src/structs/cloud/api_cloud_errors.rs @@ -54,4 +54,5 @@ pub enum CloudApiErrors { InvalidAction, AdminCannotLeaveTeam, GrafanaError, + TeamWithoutGrafanaId, } diff --git a/server/src/structs/cloud/cloud_http_endpoints.rs b/server/src/structs/cloud/cloud_http_endpoints.rs index 598a14b1..04c5aa4c 100644 --- a/server/src/structs/cloud/cloud_http_endpoints.rs +++ b/server/src/structs/cloud/cloud_http_endpoints.rs @@ -86,6 +86,10 @@ pub enum HttpCloudEndpoint { DeleteApp, #[serde(rename = "/delete_team")] DeleteTeam, + #[serde(rename = "/delete_account_start")] + DeleteAccountStart, + #[serde(rename = "/delete_account_finish")] + DeleteAccountFinish, } impl HttpCloudEndpoint { @@ -142,6 +146,8 @@ impl HttpCloudEndpoint { HttpCloudEndpoint::LeaveTeam => "/leave_team".to_string(), HttpCloudEndpoint::DeleteApp => "/delete_app".to_string(), HttpCloudEndpoint::DeleteTeam => "/delete_team".to_string(), + HttpCloudEndpoint::DeleteAccountStart => "/delete_account_start".to_string(), + HttpCloudEndpoint::DeleteAccountFinish => "/delete_account_finish".to_string(), } } } diff --git a/server/src/structs/cloud/joined_team.rs b/server/src/structs/cloud/joined_team.rs index c81ecc49..3b4117c7 100644 --- a/server/src/structs/cloud/joined_team.rs +++ b/server/src/structs/cloud/joined_team.rs @@ -9,7 +9,6 @@ pub type TeamId = String; #[serde(rename_all = "camelCase")] pub struct JoinedTeam { pub team_id: TeamId, - pub grafana_id: String, pub team_name: String, pub creator_email: String, pub created_at: DateTime, diff --git a/server/src/structs/session_cache.rs b/server/src/structs/session_cache.rs index 94387689..867e067a 100644 --- a/server/src/structs/session_cache.rs +++ b/server/src/structs/session_cache.rs @@ -13,6 +13,7 @@ pub enum SessionCache { Passkey2FA(Passkey2FAVerification), VerifyAddPasskey(AddPasskeyVerification), PasskeyLogin(PasskeyLoginVerification), + DeleteAccount(DeleteAccountVerification), } pub enum SessionsCacheKey { @@ -23,6 +24,7 @@ pub enum SessionsCacheKey { Passkey2FA(String), // user id AddPasskey(String), // user id PasskeyLogin(String), // user email + DeleteAccount(String), // user email } impl SessionsCacheKey { @@ -35,6 +37,7 @@ impl SessionsCacheKey { SessionsCacheKey::Passkey2FA(user_id) => format!("pass_chal_{}", user_id), SessionsCacheKey::AddPasskey(email) => format!("add_pass_{}", email), SessionsCacheKey::PasskeyLogin(email) => format!("pass_login_{}", email), + SessionsCacheKey::DeleteAccount(email) => format!("del_acc_{}", email), } } } @@ -47,6 +50,7 @@ pub enum VerificationAction { RegisterPasskey, ResetPassword, ResetPasskey, + DeleteAccount, } impl VerificationAction { @@ -62,6 +66,7 @@ impl VerificationAction { VerificationAction::ResetPasskey => { SessionsCacheKey::ResetPasskeyVerification(user_data) } + VerificationAction::DeleteAccount => SessionsCacheKey::DeleteAccount(user_data), } } } @@ -120,3 +125,11 @@ pub struct PasskeyLoginVerification { pub passkey_verification_state: webauthn_rs::prelude::PasskeyAuthentication, pub created_at: u64, } + +#[derive(Debug, Clone)] +pub struct DeleteAccountVerification { + pub email: String, + pub verification_code: String, + pub authentication_code: Option, + pub created_at: u64, +}