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:
zhenyi
2026-06-07 18:44:01 +08:00
parent 297a54f312
commit dca717be10
75 changed files with 2306 additions and 212 deletions
+45
View File
@@ -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, &params.token)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
}
+44
View File
@@ -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)))
}
+45
View File
@@ -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)))
}
+33
View File
@@ -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")))
}
+50
View File
@@ -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)))
}
+39
View File
@@ -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)))
}
+44
View File
@@ -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)))
}
+43
View File
@@ -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)))
}
+44
View File
@@ -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)))
}
+33
View File
@@ -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")))
}
+40
View File
@@ -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")))
}
+39
View File
@@ -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")))
}
+39
View File
@@ -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")))
}
+34
View File
@@ -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)))
}
+34
View File
@@ -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)))
}
+34
View File
@@ -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)))
}
+33
View File
@@ -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)))
}
+33
View File
@@ -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)))
}
+33
View File
@@ -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")))
}
+44
View File
@@ -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)))
}
+50
View File
@@ -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)))
}
+50
View File
@@ -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)))
}
+50
View File
@@ -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)))
}
+50
View File
@@ -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)))
}
+50
View File
@@ -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)))
}
+50
View File
@@ -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)))
}
+211
View File
@@ -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),
),
);
}
+36
View File
@@ -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)))
}
+40
View File
@@ -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")))
}
+44
View File
@@ -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)))
}
+52
View File
@@ -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")))
}
+39
View File
@@ -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")))
}
+40
View File
@@ -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")))
}
+51
View File
@@ -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)))
}
+33
View File
@@ -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")))
}
+45
View File
@@ -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)))
}
+44
View File
@@ -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)))
}
+44
View File
@@ -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)))
}
+47
View File
@@ -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)))
}
+48
View File
@@ -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)))
}
+44
View File
@@ -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)))
}
+48
View File
@@ -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)))
}
+60
View File
@@ -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)))
}
+39
View File
@@ -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")))
}