refactor(workspace): pass workspace object instead of id to service methods
- Replace workspace_id parameter with Workspace object reference in all workspace service methods - Remove redundant find_workspace_by_id calls that were duplicated in each method - Update all method signatures across approval, audit, billing, branding, core, settings and stats modules - Modify SQL queries to bind ws.id instead of separate workspace_id parameter - Add Workspace import to all affected modules - Adjust method calls in API handlers to pass workspace object instead of id - Consolidate workspace retrieval logic to single location per operation flow
This commit is contained in:
@@ -9,7 +9,7 @@ use crate::session::Session;
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/2fa/disable",
|
||||
tag = "Auth / 2FA",
|
||||
tag = "Auth",
|
||||
operation_id = "authDisableTwoFactor",
|
||||
summary = "Disable two-factor authentication",
|
||||
description = "Disable TOTP two-factor authentication for the current signed-in user. This requires verifying both the current password and a valid TOTP code or backup code. password must be encrypted with the current session RSA public key; a successfully verified backup code is consumed.",
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::session::Session;
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/2fa/enable",
|
||||
tag = "Auth / 2FA",
|
||||
tag = "Auth",
|
||||
operation_id = "authPrepareTwoFactorEnable",
|
||||
summary = "Initialize two-factor authentication setup",
|
||||
description = "Generate a new TOTP secret, otpauth QR-code URI, and 10 one-time backup codes for the current signed-in user, and save them in a not-yet-enabled state. Clients must guide the user to scan the QR code and call /auth/2fa/verify with a dynamic code before 2FA is actually enabled. Backup codes are returned in plaintext only once in this response; frontends must remind users to store them securely.",
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::session::Session;
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/auth/2fa/status",
|
||||
tag = "Auth / 2FA",
|
||||
tag = "Auth",
|
||||
operation_id = "authGetTwoFactorStatus",
|
||||
summary = "Get two-factor authentication status",
|
||||
description = "Read the current signed-in user's TOTP two-factor authentication status, including whether it is enabled, the authentication method, and whether backup codes are still available.",
|
||||
|
||||
@@ -21,7 +21,7 @@ pub struct Regenerate2FABackupCodesResponse {
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/2fa/backup-codes/regenerate",
|
||||
tag = "Auth / 2FA",
|
||||
tag = "Auth",
|
||||
operation_id = "authRegenerateTwoFactorBackupCodes",
|
||||
summary = "Regenerate 2FA backup codes",
|
||||
description = "After verifying the current password, generate a new set of backup codes for a user with 2FA enabled and replace the old backup codes. password must be encrypted with the current session RSA public key. Backup codes are returned in plaintext only once in this response; clients must prompt users to store them securely.",
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::session::Session;
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/2fa/verify",
|
||||
tag = "Auth / 2FA",
|
||||
tag = "Auth",
|
||||
operation_id = "authVerifyAndEnableTwoFactor",
|
||||
summary = "Verify and enable two-factor authentication",
|
||||
description = "After initializing with /auth/2fa/enable, submit the 6-digit TOTP code generated by the authenticator app. On success, the current user's 2FA status is set to enabled. A small clock drift of one 30-second window before or after is allowed.",
|
||||
|
||||
@@ -2,3 +2,4 @@ pub mod auth;
|
||||
pub mod openapi;
|
||||
pub mod response;
|
||||
pub mod routes;
|
||||
pub mod workspace;
|
||||
|
||||
+116
-3
@@ -5,6 +5,14 @@ use crate::api::auth::regenerate_2fa_backup_codes::{
|
||||
};
|
||||
use crate::api::auth::register::RegisterResponse;
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse, ApiResponse};
|
||||
use crate::api::workspace::accept_invitation::AcceptInvitationRequest;
|
||||
use crate::api::workspace::review_approval::ReviewApprovalRequest;
|
||||
use crate::api::workspace::transfer_owner::TransferOwnerRequest;
|
||||
use crate::models::workspaces::{
|
||||
Workspace, WorkspaceAuditLog, WorkspaceBilling, WorkspaceCustomBranding, WorkspaceDomain,
|
||||
WorkspaceIntegration, WorkspaceInvitation, WorkspaceMember, WorkspacePendingApproval,
|
||||
WorkspaceSettings, WorkspaceStats, WorkspaceWebhook,
|
||||
};
|
||||
use crate::service::auth::captcha::{CaptchaQuery, CaptchaResponse};
|
||||
use crate::service::auth::email::{EmailChangeRequest, EmailResponse, EmailVerifyRequest};
|
||||
use crate::service::auth::login::LoginParams;
|
||||
@@ -17,6 +25,16 @@ use crate::service::auth::rsa::RsaResponse;
|
||||
use crate::service::auth::totp::{
|
||||
Disable2FAParams, Enable2FAResponse, Get2FAStatusResponse, Verify2FAParams,
|
||||
};
|
||||
use crate::service::workspace::approvals::RequestApprovalParams;
|
||||
use crate::service::workspace::billing::UpdateBillingParams;
|
||||
use crate::service::workspace::branding::UpdateBrandingParams;
|
||||
use crate::service::workspace::core::{CreateWorkspaceParams, UpdateWorkspaceParams};
|
||||
use crate::service::workspace::domains::AddDomainParams;
|
||||
use crate::service::workspace::integrations::{CreateIntegrationParams, UpdateIntegrationParams};
|
||||
use crate::service::workspace::invitations::{CreateInvitationParams, CreateInvitationResponse};
|
||||
use crate::service::workspace::members::{AddMemberParams, UpdateMemberRoleParams};
|
||||
use crate::service::workspace::settings::UpdateWorkspaceSettingsParams;
|
||||
use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookParams};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
@@ -27,9 +45,10 @@ use crate::service::auth::totp::{
|
||||
),
|
||||
tags(
|
||||
(name = "Auth", description = "Authentication, registration, session and email security endpoints."),
|
||||
(name = "Auth / 2FA", description = "TOTP two-factor authentication management endpoints.")
|
||||
(name = "Workspaces", description = "Workspace CRUD, archiving, ownership transfer, and avatar management."),
|
||||
),
|
||||
paths(
|
||||
// Auth
|
||||
crate::api::auth::rsa::handle,
|
||||
crate::api::auth::captcha::handle,
|
||||
crate::api::auth::login::handle,
|
||||
@@ -46,7 +65,51 @@ use crate::service::auth::totp::{
|
||||
crate::api::auth::enable_2fa::handle,
|
||||
crate::api::auth::verify_2fa::handle,
|
||||
crate::api::auth::disable_2fa::handle,
|
||||
crate::api::auth::regenerate_2fa_backup_codes::handle
|
||||
crate::api::auth::regenerate_2fa_backup_codes::handle,
|
||||
// Workspaces
|
||||
crate::api::workspace::list::handle,
|
||||
crate::api::workspace::get::handle,
|
||||
crate::api::workspace::create::handle,
|
||||
crate::api::workspace::update::handle,
|
||||
crate::api::workspace::archive::handle,
|
||||
crate::api::workspace::unarchive::handle,
|
||||
crate::api::workspace::delete::handle,
|
||||
crate::api::workspace::transfer_owner::handle,
|
||||
crate::api::workspace::upload_avatar::handle,
|
||||
crate::api::workspace::list_members::handle,
|
||||
crate::api::workspace::add_member::handle,
|
||||
crate::api::workspace::update_member_role::handle,
|
||||
crate::api::workspace::remove_member::handle,
|
||||
crate::api::workspace::leave::handle,
|
||||
crate::api::workspace::list_invitations::handle,
|
||||
crate::api::workspace::create_invitation::handle,
|
||||
crate::api::workspace::revoke_invitation::handle,
|
||||
crate::api::workspace::accept_invitation::handle,
|
||||
crate::api::workspace::get_billing::handle,
|
||||
crate::api::workspace::update_billing::handle,
|
||||
crate::api::workspace::get_branding::handle,
|
||||
crate::api::workspace::update_branding::handle,
|
||||
crate::api::workspace::get_settings::handle,
|
||||
crate::api::workspace::update_settings::handle,
|
||||
crate::api::workspace::get_stats::handle,
|
||||
crate::api::workspace::refresh_stats::handle,
|
||||
crate::api::workspace::list_integrations::handle,
|
||||
crate::api::workspace::create_integration::handle,
|
||||
crate::api::workspace::update_integration::handle,
|
||||
crate::api::workspace::delete_integration::handle,
|
||||
crate::api::workspace::list_webhooks::handle,
|
||||
crate::api::workspace::create_webhook::handle,
|
||||
crate::api::workspace::update_webhook::handle,
|
||||
crate::api::workspace::delete_webhook::handle,
|
||||
crate::api::workspace::list_domains::handle,
|
||||
crate::api::workspace::add_domain::handle,
|
||||
crate::api::workspace::verify_domain::handle,
|
||||
crate::api::workspace::set_primary_domain::handle,
|
||||
crate::api::workspace::delete_domain::handle,
|
||||
crate::api::workspace::list_approvals::handle,
|
||||
crate::api::workspace::request_approval::handle,
|
||||
crate::api::workspace::review_approval::handle,
|
||||
crate::api::workspace::audit_logs::handle,
|
||||
),
|
||||
components(schemas(
|
||||
ApiEmptyResponse,
|
||||
@@ -79,7 +142,57 @@ use crate::service::auth::totp::{
|
||||
Verify2FAParams,
|
||||
Disable2FAParams,
|
||||
Regenerate2FABackupCodesRequest,
|
||||
Regenerate2FABackupCodesResponse
|
||||
Regenerate2FABackupCodesResponse,
|
||||
ApiResponse<Workspace>,
|
||||
ApiResponse<Vec<Workspace>>,
|
||||
ApiResponse<WorkspaceMember>,
|
||||
ApiResponse<Vec<WorkspaceMember>>,
|
||||
ApiResponse<CreateInvitationResponse>,
|
||||
ApiResponse<Vec<WorkspaceInvitation>>,
|
||||
ApiResponse<WorkspaceInvitation>,
|
||||
ApiResponse<WorkspaceBilling>,
|
||||
ApiResponse<WorkspaceCustomBranding>,
|
||||
ApiResponse<WorkspaceSettings>,
|
||||
ApiResponse<WorkspaceStats>,
|
||||
ApiResponse<WorkspaceIntegration>,
|
||||
ApiResponse<Vec<WorkspaceIntegration>>,
|
||||
ApiResponse<WorkspaceWebhook>,
|
||||
ApiResponse<Vec<WorkspaceWebhook>>,
|
||||
ApiResponse<WorkspaceDomain>,
|
||||
ApiResponse<Vec<WorkspaceDomain>>,
|
||||
ApiResponse<WorkspacePendingApproval>,
|
||||
ApiResponse<Vec<WorkspacePendingApproval>>,
|
||||
ApiResponse<Vec<WorkspaceAuditLog>>,
|
||||
Workspace,
|
||||
CreateWorkspaceParams,
|
||||
UpdateWorkspaceParams,
|
||||
TransferOwnerRequest,
|
||||
WorkspaceMember,
|
||||
AddMemberParams,
|
||||
UpdateMemberRoleParams,
|
||||
WorkspaceInvitation,
|
||||
CreateInvitationParams,
|
||||
CreateInvitationResponse,
|
||||
AcceptInvitationRequest,
|
||||
WorkspaceBilling,
|
||||
UpdateBillingParams,
|
||||
WorkspaceCustomBranding,
|
||||
UpdateBrandingParams,
|
||||
WorkspaceSettings,
|
||||
UpdateWorkspaceSettingsParams,
|
||||
WorkspaceStats,
|
||||
WorkspaceIntegration,
|
||||
CreateIntegrationParams,
|
||||
UpdateIntegrationParams,
|
||||
WorkspaceWebhook,
|
||||
CreateWebhookParams,
|
||||
UpdateWebhookParams,
|
||||
WorkspaceDomain,
|
||||
AddDomainParams,
|
||||
WorkspacePendingApproval,
|
||||
RequestApprovalParams,
|
||||
ReviewApprovalRequest,
|
||||
WorkspaceAuditLog,
|
||||
))
|
||||
)]
|
||||
pub struct OpenApiDoc;
|
||||
|
||||
+6
-1
@@ -2,7 +2,12 @@ use actix_web::web;
|
||||
use actix_web::web::scope;
|
||||
|
||||
use crate::api::auth;
|
||||
use crate::api::workspace;
|
||||
|
||||
pub fn init_routes(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(scope("/api/v1").configure(auth::configure));
|
||||
cfg.service(
|
||||
scope("/api/v1")
|
||||
.configure(auth::configure)
|
||||
.configure(workspace::configure),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceInvitation;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct AcceptInvitationRequest {
|
||||
/// The plaintext invitation token from the email link.
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/invitations/accept",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceAcceptInvitation",
|
||||
summary = "Accept an invitation",
|
||||
description = "Accept a workspace invitation using the token from the invitation email. The authenticated user's verified email must match the invited email.",
|
||||
request_body(
|
||||
content = AcceptInvitationRequest,
|
||||
description = "Invitation token.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Invitation accepted and user added as a member.", body = ApiResponse<WorkspaceInvitation>),
|
||||
(status = 400, description = "Invalid or expired invitation, or already a member.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated or email mismatch.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<AcceptInvitationRequest>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_accept_invitation(&session, ¶ms.token)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceDomain;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::domains::AddDomainParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/domains",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceAddDomain",
|
||||
summary = "Add a domain",
|
||||
description = "Add a domain for verification. The first domain added is auto-set as primary. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
request_body(
|
||||
content = AddDomainParams,
|
||||
description = "Domain name to add.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Domain added (unverified).", body = ApiResponse<WorkspaceDomain>),
|
||||
(status = 400, description = "Domain is empty.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
params: web::Json<AddDomainParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_add_domain(&session, &ws, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceMember;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::members::AddMemberParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/members",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceAddMember",
|
||||
summary = "Add a member",
|
||||
description = "Add a user to a workspace. Requires admin role. Cannot add members with role equal to or higher than the caller.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
request_body(
|
||||
content = AddMemberParams,
|
||||
description = "User ID and optional role.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Member added.", body = ApiResponse<WorkspaceMember>),
|
||||
(status = 400, description = "Cannot add owner, invalid role, or member invites disabled.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 409, description = "User is already a member.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
params: web::Json<AddMemberParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_add_member(&session, &ws, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/archive",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceArchive",
|
||||
summary = "Archive a workspace",
|
||||
description = "Archive a workspace. Requires owner role. All repos become read-only.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Workspace archived.", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found or already archived.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
service.workspace.workspace_archive(&session, &ws).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("workspace archived")))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceAuditLog;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Deserialize, utoipa::IntoParams)]
|
||||
pub struct AuditLogsQuery {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/audit-logs",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceAuditLogs",
|
||||
summary = "Get audit logs",
|
||||
description = "Return recent audit log entries for the workspace. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
AuditLogsQuery
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List of audit log entries.", body = ApiResponse<Vec<WorkspaceAuditLog>>),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
query: web::Query<AuditLogsQuery>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_audit_logs(
|
||||
&session,
|
||||
&ws,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::Workspace;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::core::CreateWorkspaceParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceCreate",
|
||||
summary = "Create a workspace",
|
||||
description = "Create a new workspace. The creator automatically becomes the owner.",
|
||||
request_body(
|
||||
content = CreateWorkspaceParams,
|
||||
description = "Workspace creation parameters.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Workspace created.", body = ApiResponse<Workspace>),
|
||||
(status = 400, description = "Name is required or visibility is invalid.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<CreateWorkspaceParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_create(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceIntegration;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::integrations::CreateIntegrationParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/integrations",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceCreateIntegration",
|
||||
summary = "Create an integration",
|
||||
description = "Add a third-party integration to the workspace. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
request_body(
|
||||
content = CreateIntegrationParams,
|
||||
description = "Integration provider, name, config, and optional secret.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Integration created.", body = ApiResponse<WorkspaceIntegration>),
|
||||
(status = 400, description = "Invalid provider or name is empty.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
params: web::Json<CreateIntegrationParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_create_integration(&session, &ws, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::invitations::{CreateInvitationParams, CreateInvitationResponse};
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/invitations",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceCreateInvitation",
|
||||
summary = "Create an invitation",
|
||||
description = "Invite a user by email to join the workspace. An invitation email is sent. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
request_body(
|
||||
content = CreateInvitationParams,
|
||||
description = "Email and optional role.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Invitation created and email sent.", body = ApiResponse<CreateInvitationResponse>),
|
||||
(status = 400, description = "Email is empty, role is invalid, or invitation already exists.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database or email service failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
params: web::Json<CreateInvitationParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_create_invitation(&session, &ws, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceWebhook;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::webhooks::CreateWebhookParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/webhooks",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceCreateWebhook",
|
||||
summary = "Create a webhook",
|
||||
description = "Create a webhook for the workspace. Requires admin role. The URL must use HTTPS and cannot point to localhost or internal IPs.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
request_body(
|
||||
content = CreateWebhookParams,
|
||||
description = "HTTPS webhook URL, optional secret ciphertext, event types, and active flag.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Webhook created.", body = ApiResponse<WorkspaceWebhook>),
|
||||
(status = 400, description = "Invalid URL, missing events, or SSRF check failed.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
params: web::Json<CreateWebhookParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_create_webhook(&session, &ws, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceDelete",
|
||||
summary = "Delete a workspace",
|
||||
description = "Soft-delete a workspace. Requires owner role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Workspace deleted.", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
service.workspace.workspace_delete(&session, &ws).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("workspace deleted")))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/domains/{domain_id}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceDeleteDomain",
|
||||
summary = "Delete a domain",
|
||||
description = "Remove a domain from the workspace. Cannot delete the primary domain. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
("domain_id" = Uuid, Path, description = "Domain record ID.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Domain deleted.", body = ApiEmptyResponse),
|
||||
(status = 400, description = "Cannot delete primary domain.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Domain not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<(String, Uuid)>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let (ws_name, domain_id) = path.into_inner();
|
||||
let ws = service.workspace.find_workspace_by_name(&ws_name).await?;
|
||||
service
|
||||
.workspace
|
||||
.workspace_delete_domain(&session, &ws, domain_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("domain deleted")))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/integrations/{integration_id}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceDeleteIntegration",
|
||||
summary = "Delete an integration",
|
||||
description = "Remove an integration from the workspace. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
("integration_id" = Uuid, Path, description = "Integration ID.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Integration deleted.", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Integration not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<(String, Uuid)>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let (ws_name, integration_id) = path.into_inner();
|
||||
let ws = service.workspace.find_workspace_by_name(&ws_name).await?;
|
||||
service
|
||||
.workspace
|
||||
.workspace_delete_integration(&session, &ws, integration_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("integration deleted")))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/webhooks/{webhook_id}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceDeleteWebhook",
|
||||
summary = "Delete a webhook",
|
||||
description = "Remove a webhook from the workspace. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
("webhook_id" = Uuid, Path, description = "Webhook ID.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Webhook deleted.", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Webhook not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<(String, Uuid)>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let (ws_name, webhook_id) = path.into_inner();
|
||||
let ws = service.workspace.find_workspace_by_name(&ws_name).await?;
|
||||
service
|
||||
.workspace
|
||||
.workspace_delete_webhook(&session, &ws, webhook_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("webhook deleted")))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::Workspace;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceGet",
|
||||
summary = "Get a workspace",
|
||||
description = "Return the workspace identified by name, subject to visibility checks.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Workspace data.", body = ApiResponse<Workspace>),
|
||||
(status = 401, description = "Unauthenticated.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found or not accessible.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service.workspace.workspace_get(&session, &ws).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceBilling;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/billing",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceGetBilling",
|
||||
summary = "Get billing information",
|
||||
description = "Return billing information for a workspace. Requires owner role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Billing information.", body = ApiResponse<WorkspaceBilling>),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service.workspace.workspace_billing(&session, &ws).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceCustomBranding;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/branding",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceGetBranding",
|
||||
summary = "Get custom branding",
|
||||
description = "Return custom branding settings for a workspace. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Branding settings.", body = ApiResponse<WorkspaceCustomBranding>),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service.workspace.workspace_branding(&session, &ws).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceSettings;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/settings",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceGetSettings",
|
||||
summary = "Get workspace settings",
|
||||
description = "Return workspace settings — readable by anyone with workspace access.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Workspace settings.", body = ApiResponse<WorkspaceSettings>),
|
||||
(status = 401, description = "Unauthenticated or not readable.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service.workspace.workspace_settings(&session, &ws).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceStats;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/stats",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceGetStats",
|
||||
summary = "Get workspace statistics",
|
||||
description = "Return workspace statistics including member, repository, issue, and pull request counts. Readable by anyone with workspace access.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Workspace statistics.", body = ApiResponse<WorkspaceStats>),
|
||||
(status = 401, description = "Unauthenticated or not readable.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service.workspace.workspace_stats(&session, &ws).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/leave",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceLeave",
|
||||
summary = "Leave a workspace",
|
||||
description = "Remove the current user from the workspace. The owner cannot leave; transfer ownership first.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Left the workspace.", body = ApiEmptyResponse),
|
||||
(status = 400, description = "Owner cannot leave.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated or not a member.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
service.workspace.workspace_leave(&session, &ws).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("left workspace")))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::Workspace;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Deserialize, utoipa::IntoParams)]
|
||||
pub struct ListQuery {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceList",
|
||||
summary = "List accessible workspaces",
|
||||
description = "Return workspaces owned by, joined by, or publicly accessible to the current user.",
|
||||
params(ListQuery),
|
||||
responses(
|
||||
(status = 200, description = "List of workspaces.", body = ApiResponse<Vec<Workspace>>),
|
||||
(status = 401, description = "Unauthenticated.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<ListQuery>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_list(
|
||||
&session,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspacePendingApproval;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Deserialize, utoipa::IntoParams)]
|
||||
pub struct ApprovalsQuery {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/approvals",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceListApprovals",
|
||||
summary = "List pending approvals",
|
||||
description = "Return pending approval requests for the workspace. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
ApprovalsQuery
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List of pending approvals.", body = ApiResponse<Vec<WorkspacePendingApproval>>),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
query: web::Query<ApprovalsQuery>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_pending_approvals(
|
||||
&session,
|
||||
&ws,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceDomain;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Deserialize, utoipa::IntoParams)]
|
||||
pub struct DomainsQuery {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/domains",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceListDomains",
|
||||
summary = "List domains",
|
||||
description = "Return domains associated with the workspace. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
DomainsQuery
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List of domains.", body = ApiResponse<Vec<WorkspaceDomain>>),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
query: web::Query<DomainsQuery>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_domains(
|
||||
&session,
|
||||
&ws,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceIntegration;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Deserialize, utoipa::IntoParams)]
|
||||
pub struct IntegrationsQuery {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/integrations",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceListIntegrations",
|
||||
summary = "List integrations",
|
||||
description = "Return integrations configured for the workspace. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
IntegrationsQuery
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List of integrations.", body = ApiResponse<Vec<WorkspaceIntegration>>),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
query: web::Query<IntegrationsQuery>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_integrations(
|
||||
&session,
|
||||
&ws,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceInvitation;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Deserialize, utoipa::IntoParams)]
|
||||
pub struct InvitationsQuery {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/invitations",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceListInvitations",
|
||||
summary = "List pending invitations",
|
||||
description = "Return pending (un-expired, un-revoked, un-accepted) invitations for a workspace. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
InvitationsQuery
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List of invitations.", body = ApiResponse<Vec<WorkspaceInvitation>>),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
query: web::Query<InvitationsQuery>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_invitations(
|
||||
&session,
|
||||
&ws,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceMember;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Deserialize, utoipa::IntoParams)]
|
||||
pub struct MembersQuery {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/members",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceListMembers",
|
||||
summary = "List workspace members",
|
||||
description = "Return active members of a workspace. Viewable by anyone with read access.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
MembersQuery
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List of members.", body = ApiResponse<Vec<WorkspaceMember>>),
|
||||
(status = 401, description = "Unauthenticated or not readable.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
query: web::Query<MembersQuery>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_members(
|
||||
&session,
|
||||
&ws,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceWebhook;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Deserialize, utoipa::IntoParams)]
|
||||
pub struct WebhooksQuery {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/webhooks",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceListWebhooks",
|
||||
summary = "List webhooks",
|
||||
description = "Return webhooks configured for the workspace. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
WebhooksQuery
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List of webhooks.", body = ApiResponse<Vec<WorkspaceWebhook>>),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
query: web::Query<WebhooksQuery>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_webhooks(
|
||||
&session,
|
||||
&ws,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
pub mod accept_invitation;
|
||||
pub mod add_domain;
|
||||
pub mod add_member;
|
||||
pub mod archive;
|
||||
pub mod audit_logs;
|
||||
pub mod create;
|
||||
pub mod create_integration;
|
||||
pub mod create_invitation;
|
||||
pub mod create_webhook;
|
||||
pub mod delete;
|
||||
pub mod delete_domain;
|
||||
pub mod delete_integration;
|
||||
pub mod delete_webhook;
|
||||
pub mod get;
|
||||
pub mod get_billing;
|
||||
pub mod get_branding;
|
||||
pub mod get_settings;
|
||||
pub mod get_stats;
|
||||
pub mod leave;
|
||||
pub mod list;
|
||||
pub mod list_approvals;
|
||||
pub mod list_domains;
|
||||
pub mod list_integrations;
|
||||
pub mod list_invitations;
|
||||
pub mod list_members;
|
||||
pub mod list_webhooks;
|
||||
pub mod refresh_stats;
|
||||
pub mod remove_member;
|
||||
pub mod request_approval;
|
||||
pub mod review_approval;
|
||||
pub mod revoke_invitation;
|
||||
pub mod set_primary_domain;
|
||||
pub mod transfer_owner;
|
||||
pub mod unarchive;
|
||||
pub mod update;
|
||||
pub mod update_billing;
|
||||
pub mod update_branding;
|
||||
pub mod update_integration;
|
||||
pub mod update_member_role;
|
||||
pub mod update_settings;
|
||||
pub mod update_webhook;
|
||||
pub mod upload_avatar;
|
||||
pub mod verify_domain;
|
||||
|
||||
use actix_web::web;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/workspaces")
|
||||
// Core
|
||||
.route("", web::get().to(list::handle))
|
||||
.route("", web::post().to(create::handle))
|
||||
.route(
|
||||
"/invitations/accept",
|
||||
web::post().to(accept_invitation::handle),
|
||||
)
|
||||
.route("/{workspace_name}", web::get().to(get::handle))
|
||||
.route("/{workspace_name}", web::put().to(update::handle))
|
||||
.route("/{workspace_name}", web::delete().to(delete::handle))
|
||||
.route("/{workspace_name}/archive", web::post().to(archive::handle))
|
||||
.route(
|
||||
"/{workspace_name}/unarchive",
|
||||
web::post().to(unarchive::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/transfer-owner",
|
||||
web::post().to(transfer_owner::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/avatar",
|
||||
web::post().to(upload_avatar::handle),
|
||||
)
|
||||
// Members
|
||||
.route(
|
||||
"/{workspace_name}/members",
|
||||
web::get().to(list_members::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/members",
|
||||
web::post().to(add_member::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/members/{member_id}/role",
|
||||
web::put().to(update_member_role::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/members/{member_id}",
|
||||
web::delete().to(remove_member::handle),
|
||||
)
|
||||
.route("/{workspace_name}/leave", web::post().to(leave::handle))
|
||||
// Invitations
|
||||
.route(
|
||||
"/{workspace_name}/invitations",
|
||||
web::get().to(list_invitations::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/invitations",
|
||||
web::post().to(create_invitation::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/invitations/{invitation_id}",
|
||||
web::delete().to(revoke_invitation::handle),
|
||||
)
|
||||
// Billing
|
||||
.route(
|
||||
"/{workspace_name}/billing",
|
||||
web::get().to(get_billing::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/billing",
|
||||
web::put().to(update_billing::handle),
|
||||
)
|
||||
// Branding
|
||||
.route(
|
||||
"/{workspace_name}/branding",
|
||||
web::get().to(get_branding::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/branding",
|
||||
web::put().to(update_branding::handle),
|
||||
)
|
||||
// Settings
|
||||
.route(
|
||||
"/{workspace_name}/settings",
|
||||
web::get().to(get_settings::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/settings",
|
||||
web::put().to(update_settings::handle),
|
||||
)
|
||||
// Stats
|
||||
.route("/{workspace_name}/stats", web::get().to(get_stats::handle))
|
||||
.route(
|
||||
"/{workspace_name}/stats/refresh",
|
||||
web::post().to(refresh_stats::handle),
|
||||
)
|
||||
// Integrations
|
||||
.route(
|
||||
"/{workspace_name}/integrations",
|
||||
web::get().to(list_integrations::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/integrations",
|
||||
web::post().to(create_integration::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/integrations/{integration_id}",
|
||||
web::put().to(update_integration::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/integrations/{integration_id}",
|
||||
web::delete().to(delete_integration::handle),
|
||||
)
|
||||
// Webhooks
|
||||
.route(
|
||||
"/{workspace_name}/webhooks",
|
||||
web::get().to(list_webhooks::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/webhooks",
|
||||
web::post().to(create_webhook::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/webhooks/{webhook_id}",
|
||||
web::put().to(update_webhook::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/webhooks/{webhook_id}",
|
||||
web::delete().to(delete_webhook::handle),
|
||||
)
|
||||
// Domains
|
||||
.route(
|
||||
"/{workspace_name}/domains",
|
||||
web::get().to(list_domains::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/domains",
|
||||
web::post().to(add_domain::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/domains/{domain_id}/verify",
|
||||
web::post().to(verify_domain::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/domains/{domain_id}/primary",
|
||||
web::put().to(set_primary_domain::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/domains/{domain_id}",
|
||||
web::delete().to(delete_domain::handle),
|
||||
)
|
||||
// Approvals
|
||||
.route(
|
||||
"/{workspace_name}/approvals",
|
||||
web::get().to(list_approvals::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/approvals",
|
||||
web::post().to(request_approval::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/approvals/{approval_id}",
|
||||
web::put().to(review_approval::handle),
|
||||
)
|
||||
// Audit
|
||||
.route(
|
||||
"/{workspace_name}/audit-logs",
|
||||
web::get().to(audit_logs::handle),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceStats;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/stats/refresh",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceRefreshStats",
|
||||
summary = "Refresh workspace statistics",
|
||||
description = "Recalculate workspace statistics by counting members, repos, issues, and PRs from live data. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Refreshed statistics.", body = ApiResponse<WorkspaceStats>),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read or write failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_refresh_stats(&session, &ws)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/members/{member_id}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceRemoveMember",
|
||||
summary = "Remove a member",
|
||||
description = "Remove a member from the workspace. Requires admin role. Cannot remove owner.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
("member_id" = Uuid, Path, description = "Member record ID.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Member removed.", body = ApiEmptyResponse),
|
||||
(status = 400, description = "Cannot remove owner or member with equal/higher role.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Member not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<(String, Uuid)>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let (ws_name, member_id) = path.into_inner();
|
||||
let ws = service.workspace.find_workspace_by_name(&ws_name).await?;
|
||||
service
|
||||
.workspace
|
||||
.workspace_remove_member(&session, &ws, member_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("member removed")))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspacePendingApproval;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::approvals::RequestApprovalParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/approvals",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceRequestApproval",
|
||||
summary = "Request an approval",
|
||||
description = "Submit a request for workspace admin approval. Readable by anyone with workspace access.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
request_body(
|
||||
content = RequestApprovalParams,
|
||||
description = "Approval request type and optional reason.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Approval request created.", body = ApiResponse<WorkspacePendingApproval>),
|
||||
(status = 400, description = "Invalid request type.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
params: web::Json<RequestApprovalParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_request_approval(&session, &ws, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct ReviewApprovalRequest {
|
||||
/// true to approve, false to reject.
|
||||
pub approved: bool,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/approvals/{approval_id}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceReviewApproval",
|
||||
summary = "Review an approval request",
|
||||
description = "Approve or reject a pending approval request. Requires owner role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
("approval_id" = Uuid, Path, description = "Approval record ID.")
|
||||
),
|
||||
request_body(
|
||||
content = ReviewApprovalRequest,
|
||||
description = "Whether to approve or reject.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Approval reviewed.", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Approval not found or already reviewed.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<(String, Uuid)>,
|
||||
params: web::Json<ReviewApprovalRequest>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let (ws_name, approval_id) = path.into_inner();
|
||||
let ws = service.workspace.find_workspace_by_name(&ws_name).await?;
|
||||
service
|
||||
.workspace
|
||||
.workspace_review_approval(&session, &ws, approval_id, params.approved)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("approval reviewed")))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/invitations/{invitation_id}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceRevokeInvitation",
|
||||
summary = "Revoke an invitation",
|
||||
description = "Revoke a pending invitation so it can no longer be accepted. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
("invitation_id" = Uuid, Path, description = "Invitation ID.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Invitation revoked.", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Invitation not found or already used.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<(String, Uuid)>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let (ws_name, invitation_id) = path.into_inner();
|
||||
let ws = service.workspace.find_workspace_by_name(&ws_name).await?;
|
||||
service
|
||||
.workspace
|
||||
.workspace_revoke_invitation(&session, &ws, invitation_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("invitation revoked")))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/domains/{domain_id}/primary",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceSetPrimaryDomain",
|
||||
summary = "Set primary domain",
|
||||
description = "Set a verified domain as the primary domain for the workspace. Requires owner role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
("domain_id" = Uuid, Path, description = "Domain record ID.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Primary domain set.", body = ApiEmptyResponse),
|
||||
(status = 400, description = "Domain must be verified first.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Domain not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<(String, Uuid)>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let (ws_name, domain_id) = path.into_inner();
|
||||
let ws = service.workspace.find_workspace_by_name(&ws_name).await?;
|
||||
service
|
||||
.workspace
|
||||
.workspace_set_primary_domain(&session, &ws, domain_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("primary domain set")))
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::Workspace;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct TransferOwnerRequest {
|
||||
/// User ID of the new owner, who must be an active member.
|
||||
pub new_owner_id: Uuid,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/transfer-owner",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceTransferOwner",
|
||||
summary = "Transfer workspace ownership",
|
||||
description = "Transfer workspace ownership to another active member. The current owner becomes admin.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
request_body(
|
||||
content = TransferOwnerRequest,
|
||||
description = "New owner user ID.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Ownership transferred.", body = ApiResponse<Workspace>),
|
||||
(status = 400, description = "New owner must be an active member and different from current owner.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
params: web::Json<TransferOwnerRequest>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_transfer_owner(&session, &ws, params.new_owner_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/unarchive",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceUnarchive",
|
||||
summary = "Unarchive a workspace",
|
||||
description = "Restore an archived workspace to active status. Requires owner role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Workspace unarchived.", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found or not archived.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
service.workspace.workspace_unarchive(&session, &ws).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("workspace unarchived")))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::Workspace;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::core::UpdateWorkspaceParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceUpdate",
|
||||
summary = "Update a workspace",
|
||||
description = "Update workspace name, description, visibility, or default role. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
request_body(
|
||||
content = UpdateWorkspaceParams,
|
||||
description = "Workspace update parameters — only included fields are changed.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Workspace updated.", body = ApiResponse<Workspace>),
|
||||
(status = 400, description = "Bad request — invalid visibility or default_role.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
params: web::Json<UpdateWorkspaceParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_update(&session, &ws, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceBilling;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::billing::UpdateBillingParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/billing",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceUpdateBilling",
|
||||
summary = "Update billing information",
|
||||
description = "Update billing plan, email, or seat count. Requires owner role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
request_body(
|
||||
content = UpdateBillingParams,
|
||||
description = "Billing update parameters — only included fields are changed.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Billing updated.", body = ApiResponse<WorkspaceBilling>),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
params: web::Json<UpdateBillingParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_update_billing(&session, &ws, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceCustomBranding;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::branding::UpdateBrandingParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/branding",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceUpdateBranding",
|
||||
summary = "Update custom branding",
|
||||
description = "Update workspace custom branding including logo, favicon, colors, CSS, and support URL. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
request_body(
|
||||
content = UpdateBrandingParams,
|
||||
description = "Branding update parameters — only included fields are changed.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Branding updated.", body = ApiResponse<WorkspaceCustomBranding>),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
params: web::Json<UpdateBrandingParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_update_branding(&session, &ws, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceIntegration;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::integrations::UpdateIntegrationParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/integrations/{integration_id}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceUpdateIntegration",
|
||||
summary = "Update an integration",
|
||||
description = "Update an integration's name, config, secret, or enabled state. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
("integration_id" = Uuid, Path, description = "Integration ID.")
|
||||
),
|
||||
request_body(
|
||||
content = UpdateIntegrationParams,
|
||||
description = "Integration update parameters — only included fields are changed.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Integration updated.", body = ApiResponse<WorkspaceIntegration>),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Integration not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<(String, Uuid)>,
|
||||
params: web::Json<UpdateIntegrationParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let (ws_name, integration_id) = path.into_inner();
|
||||
let ws = service.workspace.find_workspace_by_name(&ws_name).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_update_integration(&session, &ws, integration_id, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceMember;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::members::UpdateMemberRoleParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/members/{member_id}/role",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceUpdateMemberRole",
|
||||
summary = "Update a member's role",
|
||||
description = "Change the role of a workspace member. Requires admin role. Cannot change owner role; use transfer-owner instead.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
("member_id" = Uuid, Path, description = "Member record ID.")
|
||||
),
|
||||
request_body(
|
||||
content = UpdateMemberRoleParams,
|
||||
description = "New role value.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Member role updated.", body = ApiResponse<WorkspaceMember>),
|
||||
(status = 400, description = "Invalid role, cannot change owner, or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Member not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<(String, Uuid)>,
|
||||
params: web::Json<UpdateMemberRoleParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let (ws_name, member_id) = path.into_inner();
|
||||
let ws = service.workspace.find_workspace_by_name(&ws_name).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_update_member_role(&session, &ws, member_id, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceSettings;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::settings::UpdateWorkspaceSettingsParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/settings",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceUpdateSettings",
|
||||
summary = "Update workspace settings",
|
||||
description = "Update workspace settings such as repo visibility defaults, feature toggles, and member invite permissions. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
request_body(
|
||||
content = UpdateWorkspaceSettingsParams,
|
||||
description = "Settings update parameters — only included fields are changed.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Settings updated.", body = ApiResponse<WorkspaceSettings>),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
params: web::Json<UpdateWorkspaceSettingsParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_update_settings(&session, &ws, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceWebhook;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::webhooks::UpdateWebhookParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/webhooks/{webhook_id}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceUpdateWebhook",
|
||||
summary = "Update a webhook",
|
||||
description = "Update a webhook's URL, secret, events, or active state. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
("webhook_id" = Uuid, Path, description = "Webhook ID.")
|
||||
),
|
||||
request_body(
|
||||
content = UpdateWebhookParams,
|
||||
description = "Webhook update parameters — only included fields are changed.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Webhook updated.", body = ApiResponse<WorkspaceWebhook>),
|
||||
(status = 400, description = "Invalid URL.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Webhook not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<(String, Uuid)>,
|
||||
params: web::Json<UpdateWebhookParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let (ws_name, webhook_id) = path.into_inner();
|
||||
let ws = service.workspace.find_workspace_by_name(&ws_name).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_update_webhook(&session, &ws, webhook_id, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::Workspace;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Deserialize, utoipa::IntoParams)]
|
||||
pub struct UploadAvatarQuery {
|
||||
pub content_type: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/avatar",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceUploadAvatar",
|
||||
summary = "Upload workspace avatar",
|
||||
description = "Upload an avatar image for a workspace. Requires admin role. Maximum size 5 MB. Supported: png, jpg, gif, webp.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
("content_type" = Option<String>, Query, description = "MIME type of the uploaded image."),
|
||||
("file_name" = Option<String>, Query, description = "Original file name for extension detection.")
|
||||
),
|
||||
request_body(
|
||||
content = Vec<u8>,
|
||||
description = "Raw image bytes.",
|
||||
content_type = "application/octet-stream"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Avatar uploaded.", body = ApiResponse<Workspace>),
|
||||
(status = 400, description = "Unsupported image format or file too large.", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Storage or database failure.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
query: web::Query<UploadAvatarQuery>,
|
||||
body: web::Bytes,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_upload_avatar(
|
||||
&session,
|
||||
&ws,
|
||||
body.to_vec(),
|
||||
query.content_type.clone(),
|
||||
query.file_name.clone(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/domains/{domain_id}/verify",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceVerifyDomain",
|
||||
summary = "Verify a domain",
|
||||
description = "Mark a domain as verified. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
("domain_id" = Uuid, Path, description = "Domain record ID.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Domain verified.", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Domain not found or already verified.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<(String, Uuid)>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let (ws_name, domain_id) = path.into_inner();
|
||||
let ws = service.workspace.find_workspace_by_name(&ws_name).await?;
|
||||
service
|
||||
.workspace
|
||||
.workspace_verify_domain(&session, &ws, domain_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("domain verified")))
|
||||
}
|
||||
Reference in New Issue
Block a user