refactor(api): reorder imports and update code formatting across repository endpoints
- Reordered actix-web imports to standardize import order - Reordered crate module imports to follow alphabetical ordering - Updated function calls to use multi-line formatting for better readability - Standardized blank lines around documentation comments - Applied consistent formatting to response handling methods - Normalized import organization across all repository-related API files - Improved code consistency and maintainability through standardized formatting - Applied formatting updates to all repository endpoint implementations
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::UserGpgKey;
|
||||
use crate::service::AppService;
|
||||
use crate::service::user::keys::AddGpgKeyParams;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Add a GPG key
|
||||
///
|
||||
/// Registers a new GPG public key for the authenticated user.
|
||||
/// Requires authentication.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - public_key: ASCII-armored PGP public key block
|
||||
/// - key_id: Short key ID or full fingerprint
|
||||
/// - primary_email: Primary email associated with the key (optional)
|
||||
/// - expires_at: Optional expiration date for the key
|
||||
///
|
||||
/// Effects:
|
||||
/// - Key fingerprint is computed and stored
|
||||
/// - Key is immediately usable for verifying signed commits/tags
|
||||
/// - Duplicate keys are rejected
|
||||
///
|
||||
/// Returns the created GPG key with fingerprint and metadata.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/user/keys/gpg",
|
||||
tag = "User",
|
||||
operation_id = "userAddGpgKey",
|
||||
request_body(
|
||||
content = AddGpgKeyParams,
|
||||
description = "GPG key creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "GPG key added successfully. Returns the created key with fingerprint and metadata.", body = ApiResponse<UserGpgKey>),
|
||||
(status = 400, description = "Invalid parameters: invalid PGP key format", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 409, description = "GPG key with this fingerprint already exists", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn add_gpg_key(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<AddGpgKeyParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let key = service
|
||||
.user
|
||||
.user_add_gpg_key(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(key)))
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::UserSshKey;
|
||||
use crate::service::AppService;
|
||||
use crate::service::user::keys::AddSshKeyParams;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Add an SSH key
|
||||
///
|
||||
/// Registers a new SSH public key for the authenticated user.
|
||||
/// Requires authentication.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - title: Human-readable label for the key (e.g., "Work Laptop")
|
||||
/// - public_key: SSH public key string (supports RSA, Ed25519, ECDSA, DSA)
|
||||
/// - key_type: Key algorithm type ("rsa", "ed25519", "ecdsa", "dsa")
|
||||
/// - expires_at: Optional expiration date for the key
|
||||
///
|
||||
/// Effects:
|
||||
/// - Key fingerprint is computed and stored
|
||||
/// - Key is immediately usable for Git operations
|
||||
/// - Duplicate keys are rejected
|
||||
///
|
||||
/// Returns the created SSH key with fingerprint and metadata.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/user/keys/ssh",
|
||||
tag = "User",
|
||||
operation_id = "userAddSshKey",
|
||||
request_body(
|
||||
content = AddSshKeyParams,
|
||||
description = "SSH key creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "SSH key added successfully. Returns the created key with fingerprint and metadata.", body = ApiResponse<UserSshKey>),
|
||||
(status = 400, description = "Invalid parameters: invalid key format, unsupported key type, or type mismatch", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 409, description = "SSH key with this fingerprint already exists", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn add_ssh_key(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<AddSshKeyParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let key = service
|
||||
.user
|
||||
.user_add_ssh_key(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(key)))
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Delete user account
|
||||
///
|
||||
/// Permanently deletes the authenticated user's account and all associated data.
|
||||
/// Requires authentication.
|
||||
///
|
||||
/// Preconditions:
|
||||
/// - User must transfer or delete all owned workspaces
|
||||
/// - User must transfer or delete all owned repositories
|
||||
///
|
||||
/// Effects:
|
||||
/// - All user data is removed (SSH keys, GPG keys, sessions, devices, OAuth links, etc.)
|
||||
/// - User is soft-deleted (marked as deleted, not physically removed)
|
||||
/// - Current session is cleared
|
||||
/// - Account cannot be recovered
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/user/account",
|
||||
tag = "User",
|
||||
operation_id = "userDeleteAccount",
|
||||
responses(
|
||||
(status = 200, description = "Account deleted successfully. All user data has been removed.", body = ApiResponse<String>),
|
||||
(status = 400, description = "Cannot delete: user still owns workspaces or repositories", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "User not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn delete_account(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service.user.user_delete_account(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Account deleted successfully".to_string())))
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Device ID (UUID)
|
||||
pub device_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Delete a user device
|
||||
///
|
||||
/// Removes a registered device from the authenticated user's trusted devices.
|
||||
/// Requires authentication.
|
||||
///
|
||||
/// Effects:
|
||||
/// - Device is permanently removed
|
||||
/// - Device can no longer be used for 2FA bypass
|
||||
/// - Device will need to be re-registered and verified if needed
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/user/security/devices/{device_id}",
|
||||
tag = "User",
|
||||
operation_id = "userDeleteDevice",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Device deleted successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Device not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn delete_device(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.user
|
||||
.user_delete_device(&session, path.device_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Device deleted successfully".to_string())))
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// GPG key ID (UUID)
|
||||
pub key_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Delete a GPG key
|
||||
///
|
||||
/// Revokes a GPG key belonging to the authenticated user.
|
||||
/// Requires authentication.
|
||||
///
|
||||
/// Effects:
|
||||
/// - Key is marked as revoked (soft-deleted)
|
||||
/// - Key can no longer be used for verifying commits/tags
|
||||
/// - Revoked keys remain visible in key history
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/user/keys/gpg/{key_id}",
|
||||
tag = "User",
|
||||
operation_id = "userDeleteGpgKey",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "GPG key revoked successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "GPG key not found or already revoked", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn delete_gpg_key(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.user
|
||||
.user_delete_gpg_key(&session, path.key_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("GPG key revoked successfully".to_string())))
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// SSH key ID (UUID)
|
||||
pub key_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Delete an SSH key
|
||||
///
|
||||
/// Revokes an SSH key belonging to the authenticated user.
|
||||
/// Requires authentication.
|
||||
///
|
||||
/// Effects:
|
||||
/// - Key is marked as revoked (soft-deleted)
|
||||
/// - Key can no longer be used for Git operations
|
||||
/// - Revoked keys remain visible in key history
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/user/keys/ssh/{key_id}",
|
||||
tag = "User",
|
||||
operation_id = "userDeleteSshKey",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "SSH key revoked successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "SSH key not found or already revoked", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn delete_ssh_key(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.user
|
||||
.user_delete_ssh_key(&session, path.key_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("SSH key revoked successfully".to_string())))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::User;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Get current user account
|
||||
///
|
||||
/// Returns the authenticated user's account information including:
|
||||
/// - Username, display name, and bio
|
||||
/// - Avatar URL
|
||||
/// - Account status and role
|
||||
/// - Last login and creation timestamps
|
||||
///
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/user/account",
|
||||
tag = "User",
|
||||
operation_id = "userGetAccount",
|
||||
responses(
|
||||
(status = 200, description = "Account retrieved successfully. Returns user account with all metadata.", body = ApiResponse<User>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "User not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_account(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user = service.user.user_account(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(user)))
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::UserAppearance;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Get user appearance settings
|
||||
///
|
||||
/// Returns the authenticated user's UI appearance preferences including:
|
||||
/// - Theme (system, light, dark)
|
||||
/// - Color scheme (system, light, dark)
|
||||
/// - Density (compact, comfortable)
|
||||
/// - Font size (small, medium, large)
|
||||
/// - Editor theme
|
||||
/// - Markdown preview toggle
|
||||
/// - Reduced motion toggle
|
||||
///
|
||||
/// If no settings exist, defaults are created automatically.
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/user/appearance",
|
||||
tag = "User",
|
||||
operation_id = "userGetAppearance",
|
||||
responses(
|
||||
(status = 200, description = "Appearance settings retrieved successfully. Returns all UI preference settings.", body = ApiResponse<UserAppearance>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_appearance(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let appearance = service.user.user_appearance(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(appearance)))
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::UserNotifySetting;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Get user notification settings
|
||||
///
|
||||
/// Returns the authenticated user's notification preferences including:
|
||||
/// - Email notification toggle
|
||||
/// - Web push notification toggle
|
||||
/// - Mention notification toggle
|
||||
/// - Review notification toggle
|
||||
/// - Security notification toggle
|
||||
/// - Marketing email toggle
|
||||
/// - Digest frequency (realtime, daily, weekly, off)
|
||||
///
|
||||
/// If no settings exist, defaults are created automatically.
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/user/notifications",
|
||||
tag = "User",
|
||||
operation_id = "userGetNotifications",
|
||||
responses(
|
||||
(status = 200, description = "Notification settings retrieved successfully. Returns all notification preferences.", body = ApiResponse<UserNotifySetting>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_notifications(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let settings = service.user.user_notify_setting(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(settings)))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::UserProfile;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Get user profile
|
||||
///
|
||||
/// Returns the authenticated user's public profile information including:
|
||||
/// - Full name and company
|
||||
/// - Location and website URL
|
||||
/// - Twitter username
|
||||
/// - Timezone and language
|
||||
/// - Profile README
|
||||
///
|
||||
/// If no profile exists, an empty profile is created automatically.
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/user/profile",
|
||||
tag = "User",
|
||||
operation_id = "userGetProfile",
|
||||
responses(
|
||||
(status = 200, description = "Profile retrieved successfully. Returns all profile fields.", body = ApiResponse<UserProfile>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_profile(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let profile = service.user.user_profile(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(profile)))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::UserDevice;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
/// List user devices
|
||||
///
|
||||
/// Returns all registered devices for the authenticated user.
|
||||
/// Devices are sorted by last seen time (most recent first).
|
||||
/// Includes device metadata such as name, type, fingerprint, and trust status.
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/user/security/devices",
|
||||
tag = "User",
|
||||
operation_id = "userListDevices",
|
||||
responses(
|
||||
(status = 200, description = "Devices listed successfully. Returns array of device objects with metadata.", body = ApiResponse<Vec<UserDevice>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_devices(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let devices = service.user.user_devices(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(devices)))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::UserGpgKey;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
/// List user GPG keys
|
||||
///
|
||||
/// Returns all GPG public keys registered by the authenticated user.
|
||||
/// Keys are sorted by creation date (newest first).
|
||||
/// Only non-revoked keys are included.
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/user/keys/gpg",
|
||||
tag = "User",
|
||||
operation_id = "userListGpgKeys",
|
||||
responses(
|
||||
(status = 200, description = "GPG keys listed successfully. Returns array of GPG key objects with fingerprints and metadata.", body = ApiResponse<Vec<UserGpgKey>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_gpg_keys(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let keys = service.user.user_gpg_keys(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(keys)))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::user::security::UserOAuthInfo;
|
||||
use crate::session::Session;
|
||||
|
||||
/// List OAuth accounts
|
||||
///
|
||||
/// Returns all linked OAuth/third-party login accounts for the authenticated user.
|
||||
/// Accounts are sorted by link date (most recent first).
|
||||
/// Includes provider information, usernames, and token expiry status.
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/user/security/oauth",
|
||||
tag = "User",
|
||||
operation_id = "userListOAuthAccounts",
|
||||
responses(
|
||||
(status = 200, description = "OAuth accounts listed successfully. Returns array of linked OAuth accounts with provider details.", body = ApiResponse<Vec<UserOAuthInfo>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_oauth_accounts(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let accounts = service.user.user_oauth_accounts(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(accounts)))
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::user::security::UserPersonalAccessTokenInfo;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of tokens to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of tokens to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List personal access tokens
|
||||
///
|
||||
/// Returns a paginated list of all personal access tokens (PATs) for the authenticated user.
|
||||
/// Tokens are sorted by creation date (newest first).
|
||||
/// Includes token names, scopes, last used timestamps, and expiry status.
|
||||
/// Note: Token values are never returned after creation for security reasons.
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/user/security/tokens",
|
||||
tag = "User",
|
||||
operation_id = "userListTokens",
|
||||
params(QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Personal access tokens listed successfully. Returns array of token metadata objects (token values are never exposed).", body = ApiResponse<Vec<UserPersonalAccessTokenInfo>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_tokens(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let tokens = service
|
||||
.user
|
||||
.user_personal_access_tokens(
|
||||
&session,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(tokens)))
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::UserSecurityLog;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of log entries to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of log entries to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List security logs
|
||||
///
|
||||
/// Returns a paginated list of security events for the authenticated user.
|
||||
/// Entries are sorted by creation date (newest first).
|
||||
/// Includes event types, descriptions, IP addresses, and user agents.
|
||||
/// Useful for auditing account activity and detecting suspicious behavior.
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/user/security/logs",
|
||||
tag = "User",
|
||||
operation_id = "userListSecurityLogs",
|
||||
params(QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Security logs listed successfully. Returns array of security event entries with metadata.", body = ApiResponse<Vec<UserSecurityLog>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_security_logs(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let logs = service
|
||||
.user
|
||||
.user_security_logs(
|
||||
&session,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(logs)))
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::user::security::UserSessionInfo;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of sessions to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of sessions to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List user sessions
|
||||
///
|
||||
/// Returns a paginated list of all active and recently-expired sessions for the authenticated user.
|
||||
/// Sessions are sorted by last activity (most recent first).
|
||||
/// Includes session metadata such as IP address, user agent, and expiration time.
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/user/security/sessions",
|
||||
tag = "User",
|
||||
operation_id = "userListSessions",
|
||||
params(QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Sessions listed successfully. Returns array of session objects with metadata.", body = ApiResponse<Vec<UserSessionInfo>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_sessions(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let sessions = service
|
||||
.user
|
||||
.user_sessions(
|
||||
&session,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(sessions)))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::UserSshKey;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
/// List user SSH keys
|
||||
///
|
||||
/// Returns all SSH public keys registered by the authenticated user.
|
||||
/// Keys are sorted by creation date (newest first).
|
||||
/// Only non-revoked keys are included.
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/user/keys/ssh",
|
||||
tag = "User",
|
||||
operation_id = "userListSshKeys",
|
||||
responses(
|
||||
(status = 200, description = "SSH keys listed successfully. Returns array of SSH key objects with fingerprints and metadata.", body = ApiResponse<Vec<UserSshKey>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_ssh_keys(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let keys = service.user.user_ssh_keys(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(keys)))
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
pub mod add_gpg_key;
|
||||
pub mod add_ssh_key;
|
||||
pub mod delete_account;
|
||||
pub mod delete_device;
|
||||
pub mod delete_gpg_key;
|
||||
pub mod delete_ssh_key;
|
||||
pub mod get_account;
|
||||
pub mod get_appearance;
|
||||
pub mod get_notifications;
|
||||
pub mod get_profile;
|
||||
pub mod list_devices;
|
||||
pub mod list_gpg_keys;
|
||||
pub mod list_oauth_accounts;
|
||||
pub mod list_personal_access_tokens;
|
||||
pub mod list_security_logs;
|
||||
pub mod list_sessions;
|
||||
pub mod list_ssh_keys;
|
||||
pub mod revoke_personal_access_token;
|
||||
pub mod revoke_session;
|
||||
pub mod unlink_oauth;
|
||||
pub mod update_account;
|
||||
pub mod update_appearance;
|
||||
pub mod update_notifications;
|
||||
pub mod update_profile;
|
||||
pub mod upload_avatar;
|
||||
|
||||
use actix_web::web;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/user")
|
||||
// Account
|
||||
.route("/account", web::get().to(get_account::get_account))
|
||||
.route("/account", web::put().to(update_account::update_account))
|
||||
.route(
|
||||
"/account/avatar",
|
||||
web::post().to(upload_avatar::upload_avatar),
|
||||
)
|
||||
.route("/account", web::delete().to(delete_account::delete_account))
|
||||
// Appearance
|
||||
.route("/appearance", web::get().to(get_appearance::get_appearance))
|
||||
.route(
|
||||
"/appearance",
|
||||
web::put().to(update_appearance::update_appearance),
|
||||
)
|
||||
// Profile
|
||||
.route("/profile", web::get().to(get_profile::get_profile))
|
||||
.route("/profile", web::put().to(update_profile::update_profile))
|
||||
// Notifications
|
||||
.route(
|
||||
"/notifications",
|
||||
web::get().to(get_notifications::get_notifications),
|
||||
)
|
||||
.route(
|
||||
"/notifications",
|
||||
web::put().to(update_notifications::update_notifications),
|
||||
)
|
||||
// SSH Keys
|
||||
.route("/keys/ssh", web::get().to(list_ssh_keys::list_ssh_keys))
|
||||
.route("/keys/ssh", web::post().to(add_ssh_key::add_ssh_key))
|
||||
.route(
|
||||
"/keys/ssh/{key_id}",
|
||||
web::delete().to(delete_ssh_key::delete_ssh_key),
|
||||
)
|
||||
// GPG Keys
|
||||
.route("/keys/gpg", web::get().to(list_gpg_keys::list_gpg_keys))
|
||||
.route("/keys/gpg", web::post().to(add_gpg_key::add_gpg_key))
|
||||
.route(
|
||||
"/keys/gpg/{key_id}",
|
||||
web::delete().to(delete_gpg_key::delete_gpg_key),
|
||||
)
|
||||
// Security - Sessions
|
||||
.route(
|
||||
"/security/sessions",
|
||||
web::get().to(list_sessions::list_sessions),
|
||||
)
|
||||
.route(
|
||||
"/security/sessions/{session_id}",
|
||||
web::delete().to(revoke_session::revoke_session),
|
||||
)
|
||||
// Security - Devices
|
||||
.route(
|
||||
"/security/devices",
|
||||
web::get().to(list_devices::list_devices),
|
||||
)
|
||||
.route(
|
||||
"/security/devices/{device_id}",
|
||||
web::delete().to(delete_device::delete_device),
|
||||
)
|
||||
// Security - OAuth
|
||||
.route(
|
||||
"/security/oauth",
|
||||
web::get().to(list_oauth_accounts::list_oauth_accounts),
|
||||
)
|
||||
.route(
|
||||
"/security/oauth/{oauth_id}",
|
||||
web::delete().to(unlink_oauth::unlink_oauth),
|
||||
)
|
||||
// Security - Logs
|
||||
.route(
|
||||
"/security/logs",
|
||||
web::get().to(list_security_logs::list_security_logs),
|
||||
)
|
||||
// Security - Personal Access Tokens
|
||||
.route(
|
||||
"/security/tokens",
|
||||
web::get().to(list_personal_access_tokens::list_tokens),
|
||||
)
|
||||
.route(
|
||||
"/security/tokens/{token_id}",
|
||||
web::delete().to(revoke_personal_access_token::revoke_token),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Token ID (UUID)
|
||||
pub token_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Revoke a personal access token
|
||||
///
|
||||
/// Immediately revokes a personal access token belonging to the authenticated user.
|
||||
/// Requires authentication.
|
||||
///
|
||||
/// Effects:
|
||||
/// - Token is marked as revoked and can no longer be used
|
||||
/// - All API calls using this token will fail with 401 Unauthorized
|
||||
/// - Revoked tokens remain visible in token list for audit purposes
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/user/security/tokens/{token_id}",
|
||||
tag = "User",
|
||||
operation_id = "userRevokeToken",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Personal access token revoked successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Token not found or already revoked", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn revoke_token(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.user
|
||||
.user_revoke_personal_access_token(&session, path.token_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(
|
||||
"Personal access token revoked successfully".to_string(),
|
||||
)))
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Session ID (UUID)
|
||||
pub session_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Revoke a user session
|
||||
///
|
||||
/// Immediately terminates a specific session belonging to the authenticated user.
|
||||
/// Requires authentication.
|
||||
///
|
||||
/// Effects:
|
||||
/// - Session is marked as revoked
|
||||
/// - Session can no longer be used for authentication
|
||||
/// - Active connections using this session are disconnected
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/user/security/sessions/{session_id}",
|
||||
tag = "User",
|
||||
operation_id = "userRevokeSession",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Session revoked successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Session not found or already revoked", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn revoke_session(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.user
|
||||
.user_revoke_session(&session, path.session_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Session revoked successfully".to_string())))
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// OAuth account ID (UUID)
|
||||
pub oauth_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Unlink an OAuth account
|
||||
///
|
||||
/// Removes a linked OAuth/third-party login account from the authenticated user.
|
||||
/// Requires authentication.
|
||||
///
|
||||
/// Preconditions:
|
||||
/// - User must have at least one remaining login method (password or another OAuth account)
|
||||
///
|
||||
/// Effects:
|
||||
/// - OAuth account link is permanently removed
|
||||
/// - User can no longer log in with this OAuth provider unless re-linked
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/user/security/oauth/{oauth_id}",
|
||||
tag = "User",
|
||||
operation_id = "userUnlinkOAuth",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "OAuth account unlinked successfully.", body = ApiResponse<String>),
|
||||
(status = 400, description = "Cannot unlink: this is the last login method (set a password first)", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "OAuth account not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn unlink_oauth(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.user
|
||||
.user_unlink_oauth(&session, path.oauth_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(
|
||||
"OAuth account unlinked successfully".to_string(),
|
||||
)))
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::User;
|
||||
use crate::service::AppService;
|
||||
use crate::service::user::account::UpdateUserAccountParams;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Update user account
|
||||
///
|
||||
/// Updates the authenticated user's account settings.
|
||||
/// Requires authentication.
|
||||
///
|
||||
/// Updatable fields:
|
||||
/// - username: New username (must be unique across the platform)
|
||||
/// - display_name: Human-readable display name
|
||||
/// - bio: Short biography text
|
||||
/// - visibility: Profile visibility ("public", "private", or "internal")
|
||||
///
|
||||
/// All fields are optional; only provided fields are updated.
|
||||
/// Returns the updated user account with all metadata.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/user/account",
|
||||
tag = "User",
|
||||
operation_id = "userUpdateAccount",
|
||||
request_body(
|
||||
content = UpdateUserAccountParams,
|
||||
description = "Account update parameters (all fields optional)",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Account updated successfully. Returns updated user account with all metadata.", body = ApiResponse<User>),
|
||||
(status = 400, description = "Invalid parameters: empty username or unsupported visibility value", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "User not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "Username already taken", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn update_account(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<UpdateUserAccountParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user = service
|
||||
.user
|
||||
.user_update_account(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(user)))
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::UserAppearance;
|
||||
use crate::service::AppService;
|
||||
use crate::service::user::appearance::UpdateUserAppearanceParams;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Update user appearance settings
|
||||
///
|
||||
/// Updates the authenticated user's UI appearance preferences.
|
||||
/// Requires authentication.
|
||||
///
|
||||
/// Updatable fields:
|
||||
/// - theme: UI theme ("system", "light", "dark")
|
||||
/// - color_scheme: Color scheme ("system", "light", "dark")
|
||||
/// - density: UI density ("compact", "comfortable")
|
||||
/// - font_size: Font size ("small", "medium", "large")
|
||||
/// - editor_theme: Code editor theme name
|
||||
/// - markdown_preview: Enable/disable markdown live preview
|
||||
/// - reduced_motion: Enable/disable reduced motion
|
||||
///
|
||||
/// All fields are optional; only provided fields are updated.
|
||||
/// Returns the updated appearance settings.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/user/appearance",
|
||||
tag = "User",
|
||||
operation_id = "userUpdateAppearance",
|
||||
request_body(
|
||||
content = UpdateUserAppearanceParams,
|
||||
description = "Appearance update parameters (all fields optional)",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Appearance settings updated successfully. Returns all updated UI preferences.", body = ApiResponse<UserAppearance>),
|
||||
(status = 400, description = "Invalid parameters: unsupported theme, color scheme, density, or font size", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn update_appearance(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<UpdateUserAppearanceParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let appearance = service
|
||||
.user
|
||||
.user_update_appearance(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(appearance)))
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::UserNotifySetting;
|
||||
use crate::service::AppService;
|
||||
use crate::service::user::notify::UpdateUserNotifySettingParams;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Update user notification settings
|
||||
///
|
||||
/// Updates the authenticated user's notification preferences.
|
||||
/// Requires authentication.
|
||||
///
|
||||
/// Updatable fields:
|
||||
/// - email_notifications: Enable/disable email notifications
|
||||
/// - web_notifications: Enable/disable web push notifications
|
||||
/// - mention_notifications: Enable/disable @mention notifications
|
||||
/// - review_notifications: Enable/disable code review notifications
|
||||
/// - security_notifications: Enable/disable security notifications
|
||||
/// - marketing_emails: Enable/disable marketing emails
|
||||
/// - digest_frequency: Digest email frequency ("realtime", "daily", "weekly", "off")
|
||||
///
|
||||
/// All fields are optional; only provided fields are updated.
|
||||
/// Returns the updated notification settings.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/user/notifications",
|
||||
tag = "User",
|
||||
operation_id = "userUpdateNotifications",
|
||||
request_body(
|
||||
content = UpdateUserNotifySettingParams,
|
||||
description = "Notification settings update parameters (all fields optional)",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Notification settings updated successfully. Returns all updated preferences.", body = ApiResponse<UserNotifySetting>),
|
||||
(status = 400, description = "Invalid parameters: unsupported digest frequency", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn update_notifications(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<UpdateUserNotifySettingParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let settings = service
|
||||
.user
|
||||
.user_update_notify_setting(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(settings)))
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::UserProfile;
|
||||
use crate::service::AppService;
|
||||
use crate::service::user::profile::UpdateUserProfileParams;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Update user profile
|
||||
///
|
||||
/// Updates the authenticated user's public profile information.
|
||||
/// Requires authentication.
|
||||
///
|
||||
/// Updatable fields:
|
||||
/// - full_name: Full legal name or display name
|
||||
/// - company: Organization or company name
|
||||
/// - location: Geographic location (e.g., "San Francisco, CA")
|
||||
/// - website_url: Personal or company website URL
|
||||
/// - twitter_username: Twitter/X handle
|
||||
/// - timezone: IANA timezone identifier (e.g., "America/New_York")
|
||||
/// - language: Preferred language code (e.g., "en", "zh-CN")
|
||||
/// - profile_readme: Markdown content for profile README
|
||||
///
|
||||
/// All fields are optional; only provided fields are updated.
|
||||
/// Returns the updated profile with all fields.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/user/profile",
|
||||
tag = "User",
|
||||
operation_id = "userUpdateProfile",
|
||||
request_body(
|
||||
content = UpdateUserProfileParams,
|
||||
description = "Profile update parameters (all fields optional)",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Profile updated successfully. Returns all updated profile fields.", body = ApiResponse<UserProfile>),
|
||||
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn update_profile(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<UpdateUserProfileParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let profile = service
|
||||
.user
|
||||
.user_update_profile(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(profile)))
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::user::account::{UploadUserAvatarParams, UserAvatarResponse};
|
||||
use crate::session::Session;
|
||||
|
||||
/// Upload user avatar
|
||||
///
|
||||
/// Uploads a new avatar image for the authenticated user.
|
||||
/// Requires authentication.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - data: Raw avatar image bytes (PNG, JPEG, or WebP, max 5MB)
|
||||
/// - content_type: MIME type of the image (e.g., "image/png")
|
||||
/// - file_name: Original file name (used to infer file extension)
|
||||
///
|
||||
/// Effects:
|
||||
/// - Avatar image is stored in S3-compatible object storage
|
||||
/// - Previous avatar is deleted from storage
|
||||
/// - User's avatar URL is updated
|
||||
///
|
||||
/// Returns the new avatar URL and storage key.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/user/account/avatar",
|
||||
tag = "User",
|
||||
operation_id = "userUploadAvatar",
|
||||
request_body(
|
||||
content = UploadUserAvatarParams,
|
||||
description = "Avatar upload parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Avatar uploaded successfully. Returns the new avatar URL and storage key.", body = ApiResponse<UserAvatarResponse>),
|
||||
(status = 400, description = "Invalid parameters: unsupported file type or image too large", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "User not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error or S3 storage failure", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn upload_avatar(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<UploadUserAvatarParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let response = service
|
||||
.user
|
||||
.user_upload_avatar(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(response)))
|
||||
}
|
||||
Reference in New Issue
Block a user