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:
@@ -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