feat(api): expand API endpoints for repo, PR, user, workspace management
- Add git operation endpoints: archive, compare branches, diff, tree, repository extras - Add repo endpoints: contributors, delete fork, get branch/commit status/deploy key/invitation/member/release/tag/webhook, topics, release assets, webhook deliveries/retry - Add PR endpoints: review requests, templates - Add user endpoints: block/unblock, follow/unfollow, presence, personal access tokens, account restore - Add workspace endpoints: billing history, approvals, domains, integrations, invitations, members, webhooks, restore - Add internal API, notification API, IM API modules - Update route configuration and OpenAPI spec
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
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::UserBlock;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// User ID to block
|
||||
pub target_user_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams, utoipa::ToSchema)]
|
||||
pub struct BlockBody {
|
||||
/// Optional reason for blocking
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Block a user
|
||||
///
|
||||
/// Blocks the specified user. Once blocked, neither user can see the other's
|
||||
/// content or interact in shared channels. Requires authentication.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/user/blocks/{target_user_id}",
|
||||
tag = "User",
|
||||
operation_id = "userBlockUser",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 201, description = "User blocked successfully.", body = ApiResponse<UserBlock>),
|
||||
(status = 400, description = "Invalid request (e.g., blocking yourself)", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 409, description = "User is already blocked", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn block_user(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
body: Option<web::Json<BlockBody>>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let reason = body.and_then(|b| b.reason.clone());
|
||||
let block = service
|
||||
.user
|
||||
.user_block_create(&session, path.target_user_id, reason)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(block)))
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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::{
|
||||
CreatePersonalAccessTokenParams, CreatePersonalAccessTokenResponse,
|
||||
};
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams, utoipa::ToSchema)]
|
||||
pub struct CreateTokenBody {
|
||||
/// Display name for the token (e.g., "My CLI Token")
|
||||
pub name: String,
|
||||
/// List of permission scopes assigned to the token
|
||||
pub scopes: Vec<crate::models::common::Scope>,
|
||||
/// Optional expiration date (UTC). If not set, the token never expires.
|
||||
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
/// Create a personal access token
|
||||
///
|
||||
/// Creates a new personal access token (PAT) for the authenticated user.
|
||||
/// The full token value is returned in the response — this is the ONLY time it will be shown.
|
||||
/// Store it securely; it cannot be retrieved again.
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/user/security/tokens",
|
||||
tag = "User",
|
||||
operation_id = "userCreateToken",
|
||||
params(CreateTokenBody),
|
||||
responses(
|
||||
(status = 201, description = "Personal access token created successfully. The raw token value is included in the response and will never be shown again.", body = ApiResponse<CreatePersonalAccessTokenResponse>),
|
||||
(status = 400, description = "Invalid request body (e.g., missing name or scopes)", 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 create_token(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
body: web::Json<CreateTokenBody>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let params = CreatePersonalAccessTokenParams {
|
||||
name: body.name.clone(),
|
||||
scopes: body.scopes.clone(),
|
||||
expires_at: body.expires_at,
|
||||
};
|
||||
let token = service
|
||||
.user
|
||||
.user_create_personal_access_token(&session, params)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(token)))
|
||||
}
|
||||
@@ -7,18 +7,21 @@ use crate::session::Session;
|
||||
|
||||
/// Delete user account
|
||||
///
|
||||
/// Permanently deletes the authenticated user's account and all associated data.
|
||||
/// Requires authentication.
|
||||
/// Marks the authenticated user's account and all associated data for deletion.
|
||||
/// The user's data is soft-deleted (marked as deleted, not physically removed).
|
||||
/// A restore link is sent to the user's verified email, valid for 30 days.
|
||||
/// Requires authentication and a verified email address.
|
||||
///
|
||||
/// Preconditions:
|
||||
/// - User must have at least one verified email address
|
||||
/// - 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)
|
||||
/// - All user data is soft-deleted (SSH keys, GPG keys, sessions, devices, etc.)
|
||||
/// - Current session is cleared
|
||||
/// - Account cannot be recovered
|
||||
/// - A restore token is generated and sent via email
|
||||
/// - Account can be restored within 30 days using the restore link
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
@@ -27,8 +30,8 @@ use crate::session::Session;
|
||||
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 = 200, description = "Account marked for deletion. A restore link has been sent to your email.", body = ApiResponse<String>),
|
||||
(status = 400, description = "Cannot delete: user still owns workspaces or repositories, or no verified email", 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),
|
||||
@@ -42,5 +45,7 @@ pub async fn delete_account(
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service.user.user_delete_account(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Account deleted successfully".to_string())))
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(
|
||||
"Account deletion scheduled. A restore link has been sent to your email.".to_string(),
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
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::UserFollow;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// User ID to follow
|
||||
pub target_user_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Follow a user
|
||||
///
|
||||
/// Starts following the specified user. Requires authentication.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/user/follows/{target_user_id}",
|
||||
tag = "User",
|
||||
operation_id = "userFollowUser",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 201, description = "User followed successfully.", body = ApiResponse<UserFollow>),
|
||||
(status = 400, description = "Invalid request (e.g., following yourself)", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 409, description = "Already following this user", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn follow_user(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let follow = service
|
||||
.user
|
||||
.user_follow_create(&session, path.target_user_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(follow)))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::UserPresence;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Get user presence
|
||||
///
|
||||
/// Returns the current presence status for the authenticated user,
|
||||
/// including online/offline status, custom status text, and device information.
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/user/presence",
|
||||
tag = "User",
|
||||
operation_id = "userGetPresence",
|
||||
responses(
|
||||
(status = 200, description = "Presence retrieved successfully.", body = ApiResponse<UserPresence>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Presence not found for this user", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_presence(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let presence = service.user.user_presence_get(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(presence)))
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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::UserBlock;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of blocks to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of blocks to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List blocked users
|
||||
///
|
||||
/// Returns a paginated list of users blocked by the authenticated user.
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/user/blocks",
|
||||
tag = "User",
|
||||
operation_id = "userListBlocks",
|
||||
params(QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Blocked users listed successfully.", body = ApiResponse<Vec<UserBlock>>),
|
||||
(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_blocks(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let blocks = service
|
||||
.user
|
||||
.user_blocks_list(
|
||||
&session,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(blocks)))
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
@@ -6,6 +8,12 @@ use crate::models::users::UserDevice;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List user devices
|
||||
///
|
||||
/// Returns all registered devices for the authenticated user.
|
||||
@@ -17,6 +25,7 @@ use crate::session::Session;
|
||||
path = "/api/v1/user/security/devices",
|
||||
tag = "User",
|
||||
operation_id = "userListDevices",
|
||||
params(QueryParams),
|
||||
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),
|
||||
@@ -29,7 +38,15 @@ use crate::session::Session;
|
||||
pub async fn list_devices(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let devices = service.user.user_devices(&session).await?;
|
||||
let devices = service
|
||||
.user
|
||||
.user_devices(
|
||||
&session,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(devices)))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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::UserFollow;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of follows to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of follows to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List followed users
|
||||
///
|
||||
/// Returns a paginated list of users followed by the authenticated user.
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/user/follows",
|
||||
tag = "User",
|
||||
operation_id = "userListFollows",
|
||||
params(QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Follows listed successfully.", body = ApiResponse<Vec<UserFollow>>),
|
||||
(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_follows(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let follows = service
|
||||
.user
|
||||
.user_follows_list(
|
||||
&session,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(follows)))
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
@@ -6,6 +8,12 @@ use crate::models::users::UserGpgKey;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List user GPG keys
|
||||
///
|
||||
/// Returns all GPG public keys registered by the authenticated user.
|
||||
@@ -17,6 +25,7 @@ use crate::session::Session;
|
||||
path = "/api/v1/user/keys/gpg",
|
||||
tag = "User",
|
||||
operation_id = "userListGpgKeys",
|
||||
params(QueryParams),
|
||||
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),
|
||||
@@ -29,7 +38,15 @@ use crate::session::Session;
|
||||
pub async fn list_gpg_keys(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let keys = service.user.user_gpg_keys(&session).await?;
|
||||
let keys = service
|
||||
.user
|
||||
.user_gpg_keys(
|
||||
&session,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(keys)))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
@@ -6,6 +8,12 @@ use crate::service::AppService;
|
||||
use crate::service::user::security::UserOAuthInfo;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List OAuth accounts
|
||||
///
|
||||
/// Returns all linked OAuth/third-party login accounts for the authenticated user.
|
||||
@@ -17,6 +25,7 @@ use crate::session::Session;
|
||||
path = "/api/v1/user/security/oauth",
|
||||
tag = "User",
|
||||
operation_id = "userListOAuthAccounts",
|
||||
params(QueryParams),
|
||||
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),
|
||||
@@ -29,7 +38,15 @@ use crate::session::Session;
|
||||
pub async fn list_oauth_accounts(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let accounts = service.user.user_oauth_accounts(&session).await?;
|
||||
let accounts = service
|
||||
.user
|
||||
.user_oauth_accounts(
|
||||
&session,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(accounts)))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
@@ -6,6 +8,12 @@ use crate::models::users::UserSshKey;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List user SSH keys
|
||||
///
|
||||
/// Returns all SSH public keys registered by the authenticated user.
|
||||
@@ -17,6 +25,7 @@ use crate::session::Session;
|
||||
path = "/api/v1/user/keys/ssh",
|
||||
tag = "User",
|
||||
operation_id = "userListSshKeys",
|
||||
params(QueryParams),
|
||||
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),
|
||||
@@ -29,7 +38,15 @@ use crate::session::Session;
|
||||
pub async fn list_ssh_keys(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let keys = service.user.user_ssh_keys(&session).await?;
|
||||
let keys = service
|
||||
.user
|
||||
.user_ssh_keys(
|
||||
&session,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(keys)))
|
||||
}
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
pub mod add_gpg_key;
|
||||
pub mod add_ssh_key;
|
||||
pub mod block_user;
|
||||
pub mod create_personal_access_token;
|
||||
pub mod delete_account;
|
||||
pub mod delete_device;
|
||||
pub mod delete_gpg_key;
|
||||
pub mod delete_ssh_key;
|
||||
pub mod follow_user;
|
||||
pub mod get_account;
|
||||
pub mod get_appearance;
|
||||
pub mod get_notifications;
|
||||
pub mod get_presence;
|
||||
pub mod get_profile;
|
||||
pub mod list_blocks;
|
||||
pub mod list_devices;
|
||||
pub mod list_follows;
|
||||
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 restore_account;
|
||||
pub mod revoke_personal_access_token;
|
||||
pub mod revoke_session;
|
||||
pub mod unblock_user;
|
||||
pub mod unfollow_user;
|
||||
pub mod unlink_oauth;
|
||||
pub mod update_account;
|
||||
pub mod update_appearance;
|
||||
pub mod update_notifications;
|
||||
pub mod update_presence;
|
||||
pub mod update_profile;
|
||||
pub mod upload_avatar;
|
||||
|
||||
@@ -37,6 +47,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
web::post().to(upload_avatar::upload_avatar),
|
||||
)
|
||||
.route("/account", web::delete().to(delete_account::delete_account))
|
||||
.route(
|
||||
"/account/restore",
|
||||
web::post().to(restore_account::restore_account),
|
||||
)
|
||||
// Appearance
|
||||
.route("/appearance", web::get().to(get_appearance::get_appearance))
|
||||
.route(
|
||||
@@ -106,9 +120,36 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
"/security/tokens",
|
||||
web::get().to(list_personal_access_tokens::list_tokens),
|
||||
)
|
||||
.route(
|
||||
"/security/tokens",
|
||||
web::post().to(create_personal_access_token::create_token),
|
||||
)
|
||||
.route(
|
||||
"/security/tokens/{token_id}",
|
||||
web::delete().to(revoke_personal_access_token::revoke_token),
|
||||
)
|
||||
// Presence
|
||||
.route("/presence", web::get().to(get_presence::get_presence))
|
||||
.route("/presence", web::put().to(update_presence::update_presence))
|
||||
// Blocks
|
||||
.route("/blocks", web::get().to(list_blocks::list_blocks))
|
||||
.route(
|
||||
"/blocks/{target_user_id}",
|
||||
web::post().to(block_user::block_user),
|
||||
)
|
||||
.route(
|
||||
"/blocks/{target_user_id}",
|
||||
web::delete().to(unblock_user::unblock_user),
|
||||
)
|
||||
// Follows
|
||||
.route("/follows", web::get().to(list_follows::list_follows))
|
||||
.route(
|
||||
"/follows/{target_user_id}",
|
||||
web::post().to(follow_user::follow_user),
|
||||
)
|
||||
.route(
|
||||
"/follows/{target_user_id}",
|
||||
web::delete().to(unfollow_user::unfollow_user),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct RestoreAccountParams {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
/// Restore a deleted user account
|
||||
///
|
||||
/// Restores a user account that was marked for deletion.
|
||||
/// The restore token is sent to the user's verified email when the account is deleted.
|
||||
/// Tokens are valid for 30 days. After expiry, the data is preserved but the user
|
||||
/// must contact support to restore.
|
||||
///
|
||||
/// This endpoint does not require authentication (the restore token serves as proof of identity).
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/user/account/restore",
|
||||
tag = "User",
|
||||
operation_id = "userRestoreAccount",
|
||||
request_body(
|
||||
content = RestoreAccountParams,
|
||||
description = "Restore token received via email."
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Account restored successfully.", body = ApiResponse<String>),
|
||||
(status = 404, description = "Invalid or expired restore link.", body = ApiErrorResponse),
|
||||
)
|
||||
)]
|
||||
pub async fn restore_account(
|
||||
service: web::Data<AppService>,
|
||||
body: web::Json<RestoreAccountParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service.user.user_restore(&body.token).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(
|
||||
"account restored successfully".to_string(),
|
||||
)))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// User ID to unblock
|
||||
pub target_user_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Unblock a user
|
||||
///
|
||||
/// Removes a previously applied block on the specified user. Requires authentication.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/user/blocks/{target_user_id}",
|
||||
tag = "User",
|
||||
operation_id = "userUnblockUser",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "User unblocked successfully.", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Block not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn unblock_user(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.user
|
||||
.user_block_delete(&session, path.target_user_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("User unblocked successfully")))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// User ID to unfollow
|
||||
pub target_user_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Unfollow a user
|
||||
///
|
||||
/// Stops following the specified user. Requires authentication.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/user/follows/{target_user_id}",
|
||||
tag = "User",
|
||||
operation_id = "userUnfollowUser",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "User unfollowed successfully.", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Follow not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn unfollow_user(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.user
|
||||
.user_follow_delete(&session, path.target_user_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("User unfollowed successfully")))
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::common::{DeviceType, PresenceStatus};
|
||||
use crate::models::users::UserPresence;
|
||||
use crate::service::AppService;
|
||||
use crate::service::user::social::UpdatePresenceParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams, utoipa::ToSchema)]
|
||||
pub struct UpdatePresenceBody {
|
||||
/// New presence status
|
||||
pub status: PresenceStatus,
|
||||
/// Optional custom status text (e.g., "In a meeting")
|
||||
pub custom_status_text: Option<String>,
|
||||
/// Optional custom status emoji (e.g., ":palm_tree:")
|
||||
pub custom_status_emoji: Option<String>,
|
||||
/// Device type the user is currently using
|
||||
pub device_type: Option<DeviceType>,
|
||||
/// IP address of the current session
|
||||
pub ip_address: Option<String>,
|
||||
}
|
||||
|
||||
/// Update user presence
|
||||
///
|
||||
/// Updates the presence status for the authenticated user.
|
||||
/// Supports custom status text, emoji, device type, and IP address.
|
||||
/// Creates a new presence record if one does not exist.
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/user/presence",
|
||||
tag = "User",
|
||||
operation_id = "userUpdatePresence",
|
||||
params(UpdatePresenceBody),
|
||||
responses(
|
||||
(status = 200, description = "Presence updated successfully.", body = ApiResponse<UserPresence>),
|
||||
(status = 400, description = "Invalid request body", 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_presence(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
body: web::Json<UpdatePresenceBody>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let params = UpdatePresenceParams {
|
||||
status: body.status,
|
||||
custom_status_text: body.custom_status_text.clone(),
|
||||
custom_status_emoji: body.custom_status_emoji.clone(),
|
||||
device_type: body.device_type,
|
||||
ip_address: body.ip_address.clone(),
|
||||
};
|
||||
let presence = service.user.user_presence_update(&session, params).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(presence)))
|
||||
}
|
||||
+43
-14
@@ -1,20 +1,46 @@
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{HttpResponse, web};
|
||||
use futures_util::StreamExt;
|
||||
use serde::Serialize;
|
||||
|
||||
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;
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
pub struct AvatarData {
|
||||
pub avatar_url: String,
|
||||
pub storage_key: String,
|
||||
}
|
||||
|
||||
pub async fn parse_avatar_field(
|
||||
mut payload: Multipart,
|
||||
) -> Result<(Vec<u8>, Option<String>, Option<String>), AppError> {
|
||||
while let Some(Ok(mut field)) = payload.next().await {
|
||||
if field.name() == Some("avatar") {
|
||||
let content_type = field.content_type().map(|m| m.to_string());
|
||||
let file_name = field
|
||||
.content_disposition()
|
||||
.and_then(|cd| cd.get_filename().map(|s| s.to_string()));
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
while let Some(Ok(chunk)) = field.next().await {
|
||||
data.extend_from_slice(&chunk);
|
||||
}
|
||||
return Ok((data, content_type, file_name));
|
||||
}
|
||||
}
|
||||
Err(AppError::BadRequest(
|
||||
"missing 'avatar' field in multipart form".into(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Upload user avatar
|
||||
///
|
||||
/// Uploads a new avatar image for the authenticated user.
|
||||
/// Requires authentication.
|
||||
/// Requires authentication. Accepts multipart/form-data with a single "avatar" field.
|
||||
///
|
||||
/// 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)
|
||||
/// Supported formats: PNG, JPEG, WebP, GIF (max 5MB).
|
||||
///
|
||||
/// Effects:
|
||||
/// - Avatar image is stored in S3-compatible object storage
|
||||
@@ -28,12 +54,11 @@ use crate::session::Session;
|
||||
tag = "User",
|
||||
operation_id = "userUploadAvatar",
|
||||
request_body(
|
||||
content = UploadUserAvatarParams,
|
||||
description = "Avatar upload parameters",
|
||||
content_type = "application/json"
|
||||
content_type = "multipart/form-data",
|
||||
description = "Avatar image file in a multipart form field named 'avatar'."
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Avatar uploaded successfully. Returns the new avatar URL and storage key.", body = ApiResponse<UserAvatarResponse>),
|
||||
(status = 200, description = "Avatar uploaded successfully", body = ApiResponse<AvatarData>),
|
||||
(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),
|
||||
@@ -46,11 +71,15 @@ use crate::session::Session;
|
||||
pub async fn upload_avatar(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<UploadUserAvatarParams>,
|
||||
payload: Multipart,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let response = service
|
||||
let (data, content_type, file_name) = parse_avatar_field(payload).await?;
|
||||
let (avatar_url, storage_key) = service
|
||||
.user
|
||||
.user_upload_avatar(&session, params.into_inner())
|
||||
.user_upload_avatar(&session, data, content_type, file_name)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(response)))
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(AvatarData {
|
||||
avatar_url,
|
||||
storage_key,
|
||||
})))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user