265 lines
7.4 KiB
Rust
265 lines
7.4 KiB
Rust
use crate::AppState;
|
|
use axum::{
|
|
extract::State,
|
|
http::StatusCode,
|
|
response::IntoResponse,
|
|
routing::{get, patch},
|
|
Json, Router,
|
|
};
|
|
use contracts::auth_middleware::AuthUser;
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
pub fn router() -> Router<AppState> {
|
|
Router::new()
|
|
.route("/", get(get_settings))
|
|
.route("/notifications", patch(update_notifications))
|
|
.route("/delete-account-request", get(get_delete_account_request).post(create_delete_account_request))
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct SettingsResponse {
|
|
email_notifications: bool,
|
|
in_app_notifications: bool,
|
|
sms_notifications: bool,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct UpdateNotificationsPayload {
|
|
email_notifications: bool,
|
|
in_app_notifications: bool,
|
|
sms_notifications: bool,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct CreateDeleteAccountPayload {
|
|
reason: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct DeleteRequestResponse {
|
|
status: String,
|
|
deleted_at: Option<String>,
|
|
}
|
|
|
|
async fn ensure_settings_row(pool: &sqlx::PgPool, user_id: Uuid) -> Result<(), sqlx::Error> {
|
|
sqlx::query(
|
|
r#"
|
|
INSERT INTO user_settings (user_id)
|
|
VALUES ($1)
|
|
ON CONFLICT (user_id) DO NOTHING
|
|
"#,
|
|
)
|
|
.bind(user_id)
|
|
.execute(pool)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_settings(
|
|
auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
) -> impl IntoResponse {
|
|
if let Err(e) = ensure_settings_row(&state.pool, auth.user_id).await {
|
|
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response();
|
|
}
|
|
|
|
let row = sqlx::query_as::<_, (bool, bool, bool)>(
|
|
r#"
|
|
SELECT email_notifications, in_app_notifications, sms_notifications
|
|
FROM user_settings
|
|
WHERE user_id = $1
|
|
"#,
|
|
)
|
|
.bind(auth.user_id)
|
|
.fetch_one(&state.pool)
|
|
.await;
|
|
|
|
match row {
|
|
Ok((email_notifications, in_app_notifications, sms_notifications)) => (
|
|
StatusCode::OK,
|
|
Json(SettingsResponse {
|
|
email_notifications,
|
|
in_app_notifications,
|
|
sms_notifications,
|
|
}),
|
|
)
|
|
.into_response(),
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
}
|
|
}
|
|
|
|
async fn update_notifications(
|
|
auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
Json(payload): Json<UpdateNotificationsPayload>,
|
|
) -> impl IntoResponse {
|
|
let result = sqlx::query(
|
|
r#"
|
|
INSERT INTO user_settings (user_id, email_notifications, in_app_notifications, sms_notifications, updated_at)
|
|
VALUES ($1, $2, $3, $4, NOW())
|
|
ON CONFLICT (user_id)
|
|
DO UPDATE SET
|
|
email_notifications = EXCLUDED.email_notifications,
|
|
in_app_notifications = EXCLUDED.in_app_notifications,
|
|
sms_notifications = EXCLUDED.sms_notifications,
|
|
updated_at = NOW()
|
|
"#,
|
|
)
|
|
.bind(auth.user_id)
|
|
.bind(payload.email_notifications)
|
|
.bind(payload.in_app_notifications)
|
|
.bind(payload.sms_notifications)
|
|
.execute(&state.pool)
|
|
.await;
|
|
|
|
match result {
|
|
Ok(_) => (
|
|
StatusCode::OK,
|
|
Json(serde_json::json!({
|
|
"message": "Notification preferences updated"
|
|
})),
|
|
)
|
|
.into_response(),
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
}
|
|
}
|
|
|
|
async fn get_delete_account_request(
|
|
auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
) -> impl IntoResponse {
|
|
let row = sqlx::query_as::<_, (Option<chrono::DateTime<chrono::Utc>>, )>(
|
|
r#"
|
|
SELECT deleted_at
|
|
FROM users
|
|
WHERE id = $1
|
|
"#,
|
|
)
|
|
.bind(auth.user_id)
|
|
.fetch_optional(&state.pool)
|
|
.await;
|
|
|
|
match row {
|
|
Ok(Some((deleted_at,))) if deleted_at.is_some() => (
|
|
StatusCode::OK,
|
|
Json(DeleteRequestResponse {
|
|
status: "DELETED".to_string(),
|
|
deleted_at: deleted_at.map(|v| v.to_rfc3339()),
|
|
}),
|
|
)
|
|
.into_response(),
|
|
Ok(Some((_deleted_at,))) => (
|
|
StatusCode::OK,
|
|
Json(DeleteRequestResponse {
|
|
status: "NONE".to_string(),
|
|
deleted_at: None,
|
|
}),
|
|
)
|
|
.into_response(),
|
|
Ok(None) => (
|
|
StatusCode::NOT_FOUND,
|
|
Json(serde_json::json!({ "error": "User not found" })),
|
|
)
|
|
.into_response(),
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
}
|
|
}
|
|
|
|
async fn create_delete_account_request(
|
|
auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
Json(payload): Json<CreateDeleteAccountPayload>,
|
|
) -> impl IntoResponse {
|
|
let user = match db::models::user::UserRepository::get_by_id(&state.pool, auth.user_id).await {
|
|
Ok(u) => u,
|
|
Err(_) => {
|
|
return (
|
|
StatusCode::UNAUTHORIZED,
|
|
Json(serde_json::json!({ "error": "User not found" })),
|
|
)
|
|
.into_response()
|
|
}
|
|
};
|
|
|
|
let already_deleted = sqlx::query_scalar::<_, i64>(
|
|
r#"
|
|
SELECT COUNT(*)::BIGINT
|
|
FROM users
|
|
WHERE id = $1 AND deleted_at IS NOT NULL
|
|
"#,
|
|
)
|
|
.bind(auth.user_id)
|
|
.fetch_one(&state.pool)
|
|
.await;
|
|
|
|
if let Ok(count) = already_deleted {
|
|
if count > 0 {
|
|
return (
|
|
StatusCode::CONFLICT,
|
|
Json(serde_json::json!({
|
|
"error": "Account is already deleted."
|
|
})),
|
|
)
|
|
.into_response();
|
|
}
|
|
}
|
|
|
|
let deleted_at = chrono::Utc::now();
|
|
let soft_deleted = sqlx::query(
|
|
r#"
|
|
UPDATE users
|
|
SET deleted_at = $2, status = 'SUSPENDED', updated_at = NOW()
|
|
WHERE id = $1 AND deleted_at IS NULL
|
|
"#,
|
|
)
|
|
.bind(auth.user_id)
|
|
.bind(deleted_at)
|
|
.execute(&state.pool)
|
|
.await;
|
|
|
|
match soft_deleted {
|
|
Ok(result) if result.rows_affected() > 0 => {
|
|
let _ = db::models::user::UserRepository::revoke_all_for_user(&state.pool, auth.user_id).await;
|
|
let _ = state
|
|
.mail
|
|
.send_account_deleted_email(
|
|
&user.email,
|
|
&format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default()),
|
|
)
|
|
.await;
|
|
let _ = sqlx::query(
|
|
r#"INSERT INTO notifications (user_id, title, body, type)
|
|
VALUES ($1, $2, $3, $4)"#,
|
|
)
|
|
.bind(auth.user_id)
|
|
.bind("Account Deleted")
|
|
.bind(format!(
|
|
"Your account was deleted{}.",
|
|
payload
|
|
.reason
|
|
.as_deref()
|
|
.map(|r| format!(" Reason: {r}"))
|
|
.unwrap_or_default()
|
|
))
|
|
.bind("ACCOUNT")
|
|
.execute(&state.pool)
|
|
.await;
|
|
|
|
(
|
|
StatusCode::OK,
|
|
Json(DeleteRequestResponse {
|
|
status: "DELETED".to_string(),
|
|
deleted_at: Some(deleted_at.to_rfc3339()),
|
|
}),
|
|
)
|
|
.into_response()
|
|
}
|
|
Ok(_) => (
|
|
StatusCode::CONFLICT,
|
|
Json(serde_json::json!({ "error": "Account is already deleted." })),
|
|
)
|
|
.into_response(),
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
}
|
|
}
|