feat(api): expand API endpoints for repo, PR, user, workspace management
- Add git operation endpoints: archive, compare branches, diff, tree, repository extras - Add repo endpoints: contributors, delete fork, get branch/commit status/deploy key/invitation/member/release/tag/webhook, topics, release assets, webhook deliveries/retry - Add PR endpoints: review requests, templates - Add user endpoints: block/unblock, follow/unfollow, presence, personal access tokens, account restore - Add workspace endpoints: billing history, approvals, domains, integrations, invitations, members, webhooks, restore - Add internal API, notification API, IM API modules - Update route configuration and OpenAPI spec
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiListResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Deserialize, utoipa::IntoParams)]
|
||||
pub struct BillingHistoryQuery {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/billing/history",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceBillingHistory",
|
||||
summary = "List billing history",
|
||||
description = "Return billing history for a workspace. Requires owner role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
BillingHistoryQuery
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List of billing history entries (currently empty).", body = ApiListResponse<serde_json::Value>),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn billing_history(
|
||||
_service: web::Data<AppService>,
|
||||
_session: Session,
|
||||
_path: web::Path<String>,
|
||||
_query: web::Query<BillingHistoryQuery>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
Ok(HttpResponse::Ok().json(ApiListResponse::<serde_json::Value>::empty()))
|
||||
}
|
||||
+10
-3
@@ -2,7 +2,8 @@ use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::Workspace;
|
||||
use crate::models::base_info::resolve_users;
|
||||
use crate::models::workspaces::WorkspaceDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::core::CreateWorkspaceParams;
|
||||
use crate::session::Session;
|
||||
@@ -20,7 +21,7 @@ use crate::session::Session;
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Workspace created.", body = ApiResponse<Workspace>),
|
||||
(status = 200, description = "Workspace created.", body = ApiResponse<WorkspaceDetail>),
|
||||
(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)
|
||||
@@ -35,5 +36,11 @@ pub async fn handle(
|
||||
.workspace
|
||||
.workspace_create(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
|
||||
let db = &service.ctx.db;
|
||||
let users = resolve_users(db, &[data.owner_id]).await?;
|
||||
let owner = users.get(&data.owner_id).cloned().unwrap_or_default();
|
||||
let detail = data.into_detail(owner);
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(detail)))
|
||||
}
|
||||
|
||||
+10
-3
@@ -2,7 +2,8 @@ use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::Workspace;
|
||||
use crate::models::base_info::resolve_users;
|
||||
use crate::models::workspaces::WorkspaceDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
@@ -17,7 +18,7 @@ use crate::session::Session;
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Workspace data.", body = ApiResponse<Workspace>),
|
||||
(status = 200, description = "Workspace data.", body = ApiResponse<WorkspaceDetail>),
|
||||
(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)
|
||||
@@ -30,5 +31,11 @@ pub async fn handle(
|
||||
) -> 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)))
|
||||
|
||||
let db = &service.ctx.db;
|
||||
let users = resolve_users(db, &[data.owner_id]).await?;
|
||||
let owner = users.get(&data.owner_id).cloned().unwrap_or_default();
|
||||
let detail = data.into_detail(owner);
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(detail)))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspacePendingApproval;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub approval_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/approvals/{approval_id}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceGetApproval",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Approval retrieved successfully", body = ApiResponse<WorkspacePendingApproval>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Approval not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service
|
||||
.workspace
|
||||
.find_workspace_by_name(&path.workspace_name)
|
||||
.await?;
|
||||
let approvals = service
|
||||
.workspace
|
||||
.workspace_pending_approvals(&session, &ws, 1000, 0)
|
||||
.await?;
|
||||
|
||||
let approval = approvals
|
||||
.into_iter()
|
||||
.find(|a| a.id == path.approval_id)
|
||||
.ok_or(AppError::NotFound("approval not found".into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(approval)))
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceDomain;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub domain_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/domains/{domain_id}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceGetDomain",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Domain retrieved successfully", body = ApiResponse<WorkspaceDomain>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Domain not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service
|
||||
.workspace
|
||||
.find_workspace_by_name(&path.workspace_name)
|
||||
.await?;
|
||||
let domains = service
|
||||
.workspace
|
||||
.workspace_domains(&session, &ws, 1000, 0)
|
||||
.await?;
|
||||
|
||||
let domain = domains
|
||||
.into_iter()
|
||||
.find(|d| d.id == path.domain_id)
|
||||
.ok_or(AppError::NotFound("domain not found".into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(domain)))
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceIntegration;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub integration_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/integrations/{integration_id}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceGetIntegration",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Integration retrieved successfully", body = ApiResponse<WorkspaceIntegration>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Integration not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service
|
||||
.workspace
|
||||
.find_workspace_by_name(&path.workspace_name)
|
||||
.await?;
|
||||
let integrations = service
|
||||
.workspace
|
||||
.workspace_integrations(&session, &ws, 1000, 0)
|
||||
.await?;
|
||||
|
||||
let integration = integrations
|
||||
.into_iter()
|
||||
.find(|i| i.id == path.integration_id)
|
||||
.ok_or(AppError::NotFound("integration not found".into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(integration)))
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceInvitation;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub invitation_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/invitations/{invitation_id}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceGetInvitation",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Invitation retrieved successfully", body = ApiResponse<WorkspaceInvitation>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Invitation not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service
|
||||
.workspace
|
||||
.find_workspace_by_name(&path.workspace_name)
|
||||
.await?;
|
||||
let invitations = service
|
||||
.workspace
|
||||
.workspace_invitations(&session, &ws, 1000, 0)
|
||||
.await?;
|
||||
|
||||
let invitation = invitations
|
||||
.into_iter()
|
||||
.find(|i| i.id == path.invitation_id)
|
||||
.ok_or(AppError::NotFound("invitation not found".into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(invitation)))
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceMember;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub member_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/members/{member_id}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceGetMember",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Member retrieved successfully", body = ApiResponse<WorkspaceMember>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Member not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service
|
||||
.workspace
|
||||
.find_workspace_by_name(&path.workspace_name)
|
||||
.await?;
|
||||
let members = service
|
||||
.workspace
|
||||
.workspace_members(&session, &ws, 1000, 0)
|
||||
.await?;
|
||||
|
||||
let member = members
|
||||
.into_iter()
|
||||
.find(|m| m.id == path.member_id)
|
||||
.ok_or(AppError::NotFound("member not found".into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(member)))
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::WorkspaceWebhook;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub webhook_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/webhooks/{webhook_id}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceGetWebhook",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Webhook retrieved successfully", body = ApiResponse<WorkspaceWebhook>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Webhook not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service
|
||||
.workspace
|
||||
.find_workspace_by_name(&path.workspace_name)
|
||||
.await?;
|
||||
let webhooks = service
|
||||
.workspace
|
||||
.workspace_webhooks(&session, &ws, 1000, 0)
|
||||
.await?;
|
||||
|
||||
let webhook = webhooks
|
||||
.into_iter()
|
||||
.find(|w| w.id == path.webhook_id)
|
||||
.ok_or(AppError::NotFound("webhook not found".into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(webhook)))
|
||||
}
|
||||
+17
-3
@@ -1,9 +1,11 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::Workspace;
|
||||
use crate::models::base_info::resolve_users;
|
||||
use crate::models::workspaces::WorkspaceDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
@@ -22,7 +24,7 @@ pub struct ListQuery {
|
||||
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 = 200, description = "List of workspaces.", body = ApiResponse<Vec<WorkspaceDetail>>),
|
||||
(status = 401, description = "Unauthenticated.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
@@ -40,5 +42,17 @@ pub async fn handle(
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
|
||||
let db = &service.ctx.db;
|
||||
let owner_ids: Vec<Uuid> = data.iter().map(|w| w.owner_id).collect();
|
||||
let users = resolve_users(db, &owner_ids).await?;
|
||||
let details: Vec<WorkspaceDetail> = data
|
||||
.into_iter()
|
||||
.map(|w| {
|
||||
let owner = users.get(&w.owner_id).cloned().unwrap_or_default();
|
||||
w.into_detail(owner)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(details)))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiListResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Deserialize, utoipa::IntoParams)]
|
||||
pub struct DeliveriesQuery {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/webhooks/{webhook_id}/deliveries",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceListWebhookDeliveries",
|
||||
summary = "List webhook deliveries",
|
||||
description = "Return delivery logs for a webhook. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
("webhook_id" = Uuid, Path, description = "Webhook ID."),
|
||||
DeliveriesQuery
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List of deliveries (currently empty).", body = ApiListResponse<serde_json::Value>),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
_service: web::Data<AppService>,
|
||||
_session: Session,
|
||||
_path: web::Path<(String, Uuid)>,
|
||||
_query: web::Query<DeliveriesQuery>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
Ok(HttpResponse::Ok().json(ApiListResponse::<serde_json::Value>::empty()))
|
||||
}
|
||||
+67
-1
@@ -3,6 +3,7 @@ pub mod add_domain;
|
||||
pub mod add_member;
|
||||
pub mod archive;
|
||||
pub mod audit_logs;
|
||||
pub mod billing_history;
|
||||
pub mod create;
|
||||
pub mod create_integration;
|
||||
pub mod create_invitation;
|
||||
@@ -12,10 +13,16 @@ pub mod delete_domain;
|
||||
pub mod delete_integration;
|
||||
pub mod delete_webhook;
|
||||
pub mod get;
|
||||
pub mod get_approval;
|
||||
pub mod get_billing;
|
||||
pub mod get_branding;
|
||||
pub mod get_domain;
|
||||
pub mod get_integration;
|
||||
pub mod get_invitation;
|
||||
pub mod get_member;
|
||||
pub mod get_settings;
|
||||
pub mod get_stats;
|
||||
pub mod get_webhook;
|
||||
pub mod leave;
|
||||
pub mod list;
|
||||
pub mod list_approvals;
|
||||
@@ -23,10 +30,13 @@ pub mod list_domains;
|
||||
pub mod list_integrations;
|
||||
pub mod list_invitations;
|
||||
pub mod list_members;
|
||||
pub mod list_webhook_deliveries;
|
||||
pub mod list_webhooks;
|
||||
pub mod refresh_stats;
|
||||
pub mod remove_member;
|
||||
pub mod request_approval;
|
||||
pub mod restore;
|
||||
pub mod retry_webhook_delivery;
|
||||
pub mod review_approval;
|
||||
pub mod revoke_invitation;
|
||||
pub mod set_primary_domain;
|
||||
@@ -35,6 +45,7 @@ pub mod unarchive;
|
||||
pub mod update;
|
||||
pub mod update_billing;
|
||||
pub mod update_branding;
|
||||
pub mod update_domain;
|
||||
pub mod update_integration;
|
||||
pub mod update_member_role;
|
||||
pub mod update_settings;
|
||||
@@ -47,7 +58,6 @@ 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(
|
||||
@@ -57,6 +67,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.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}/restore",
|
||||
web::post().to(restore::restore_workspace),
|
||||
)
|
||||
.route("/{workspace_name}/archive", web::post().to(archive::handle))
|
||||
.route(
|
||||
"/{workspace_name}/unarchive",
|
||||
@@ -83,6 +97,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
"/{workspace_name}/members/{member_id}/role",
|
||||
web::put().to(update_member_role::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/members/{member_id}",
|
||||
web::get().to(get_member::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/members/{member_id}",
|
||||
web::delete().to(remove_member::handle),
|
||||
@@ -97,6 +115,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
"/{workspace_name}/invitations",
|
||||
web::post().to(create_invitation::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/invitations/{invitation_id}",
|
||||
web::get().to(get_invitation::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/invitations/{invitation_id}",
|
||||
web::delete().to(revoke_invitation::handle),
|
||||
@@ -110,6 +132,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
"/{workspace_name}/billing",
|
||||
web::put().to(update_billing::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/billing/history",
|
||||
web::get().to(billing_history::billing_history),
|
||||
)
|
||||
// Branding
|
||||
.route(
|
||||
"/{workspace_name}/branding",
|
||||
@@ -143,6 +169,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
"/{workspace_name}/integrations",
|
||||
web::post().to(create_integration::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/integrations/{integration_id}",
|
||||
web::get().to(get_integration::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/integrations/{integration_id}",
|
||||
web::put().to(update_integration::handle),
|
||||
@@ -160,6 +190,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
"/{workspace_name}/webhooks",
|
||||
web::post().to(create_webhook::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/webhooks/{webhook_id}",
|
||||
web::get().to(get_webhook::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/webhooks/{webhook_id}",
|
||||
web::put().to(update_webhook::handle),
|
||||
@@ -168,6 +202,14 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
"/{workspace_name}/webhooks/{webhook_id}",
|
||||
web::delete().to(delete_webhook::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/webhooks/{webhook_id}/deliveries",
|
||||
web::get().to(list_webhook_deliveries::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/webhooks/{webhook_id}/deliveries/{delivery_id}/retry",
|
||||
web::post().to(retry_webhook_delivery::handle),
|
||||
)
|
||||
// Domains
|
||||
.route(
|
||||
"/{workspace_name}/domains",
|
||||
@@ -185,6 +227,14 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
"/{workspace_name}/domains/{domain_id}/primary",
|
||||
web::put().to(set_primary_domain::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/domains/{domain_id}",
|
||||
web::get().to(get_domain::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/domains/{domain_id}",
|
||||
web::put().to(update_domain::update_domain),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/domains/{domain_id}",
|
||||
web::delete().to(delete_domain::handle),
|
||||
@@ -198,6 +248,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
"/{workspace_name}/approvals",
|
||||
web::post().to(request_approval::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/approvals/{approval_id}",
|
||||
web::get().to(get_approval::handle),
|
||||
)
|
||||
.route(
|
||||
"/{workspace_name}/approvals/{approval_id}",
|
||||
web::put().to(review_approval::handle),
|
||||
@@ -206,6 +260,18 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.route(
|
||||
"/{workspace_name}/audit-logs",
|
||||
web::get().to(audit_logs::handle),
|
||||
)
|
||||
// Issues
|
||||
.service(web::scope("/{workspace_name}/issues").configure(crate::api::issue::configure))
|
||||
// Repos
|
||||
.service(web::scope("/{workspace_name}/repos").configure(crate::api::repo::configure))
|
||||
// Repo-level: PRs, Wiki, Issue labels/milestones/templates, Git
|
||||
.service(
|
||||
web::scope("/{workspace_name}/repos/{repo_name}")
|
||||
.configure(crate::api::issue::configure_repo_level)
|
||||
.configure(crate::api::pr::configure)
|
||||
.configure(crate::api::wiki::configure)
|
||||
.configure(crate::api::repo::git::configure),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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}/restore",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceRestore",
|
||||
summary = "Restore a soft-deleted workspace",
|
||||
description = "Restore a workspace that was previously soft-deleted. Requires owner role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name.")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Workspace restored.", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found or not deleted.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn restore_workspace(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws_name = path.into_inner();
|
||||
service
|
||||
.workspace
|
||||
.workspace_restore(&session, &ws_name)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("workspace restored")))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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}/webhooks/{webhook_id}/deliveries/{delivery_id}/retry",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceRetryWebhookDelivery",
|
||||
summary = "Retry a webhook delivery",
|
||||
description = "Retry a failed webhook delivery. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
("webhook_id" = Uuid, Path, description = "Webhook ID."),
|
||||
("delivery_id" = Uuid, Path, description = "Delivery ID.")
|
||||
),
|
||||
responses(
|
||||
(status = 202, description = "Retry scheduled.", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Unauthenticated or insufficient role.", body = ApiErrorResponse),
|
||||
(status = 404, description = "Delivery not found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
_service: web::Data<AppService>,
|
||||
_session: Session,
|
||||
_path: web::Path<(String, Uuid, Uuid)>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
Ok(HttpResponse::Accepted().json(ApiEmptyResponse::ok("retry scheduled")))
|
||||
}
|
||||
@@ -4,7 +4,8 @@ use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::Workspace;
|
||||
use crate::models::base_info::resolve_users;
|
||||
use crate::models::workspaces::WorkspaceDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
@@ -30,7 +31,7 @@ pub struct TransferOwnerRequest {
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Ownership transferred.", body = ApiResponse<Workspace>),
|
||||
(status = 200, description = "Ownership transferred.", body = ApiResponse<WorkspaceDetail>),
|
||||
(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)
|
||||
@@ -47,5 +48,11 @@ pub async fn handle(
|
||||
.workspace
|
||||
.workspace_transfer_owner(&session, &ws, params.new_owner_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
|
||||
let db = &service.ctx.db;
|
||||
let users = resolve_users(db, &[data.owner_id]).await?;
|
||||
let owner = users.get(&data.owner_id).cloned().unwrap_or_default();
|
||||
let detail = data.into_detail(owner);
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(detail)))
|
||||
}
|
||||
|
||||
+10
-3
@@ -2,7 +2,8 @@ use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::workspaces::Workspace;
|
||||
use crate::models::base_info::resolve_users;
|
||||
use crate::models::workspaces::WorkspaceDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::core::UpdateWorkspaceParams;
|
||||
use crate::session::Session;
|
||||
@@ -23,7 +24,7 @@ use crate::session::Session;
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Workspace updated.", body = ApiResponse<Workspace>),
|
||||
(status = 200, description = "Workspace updated.", body = ApiResponse<WorkspaceDetail>),
|
||||
(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),
|
||||
@@ -41,5 +42,11 @@ pub async fn handle(
|
||||
.workspace
|
||||
.workspace_update(&session, &ws, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
|
||||
let db = &service.ctx.db;
|
||||
let users = resolve_users(db, &[data.owner_id]).await?;
|
||||
let owner = users.get(&data.owner_id).cloned().unwrap_or_default();
|
||||
let detail = data.into_detail(owner);
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(detail)))
|
||||
}
|
||||
|
||||
@@ -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::WorkspaceDomain;
|
||||
use crate::service::AppService;
|
||||
use crate::service::workspace::domains::UpdateDomainParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/domains/{domain_id}",
|
||||
tag = "Workspaces",
|
||||
operation_id = "workspaceUpdateDomain",
|
||||
summary = "Update a domain",
|
||||
description = "Update a domain name. Requires admin role.",
|
||||
params(
|
||||
("workspace_name" = String, Path, description = "Workspace name."),
|
||||
("domain_id" = Uuid, Path, description = "Domain record ID.")
|
||||
),
|
||||
request_body(
|
||||
content = UpdateDomainParams,
|
||||
description = "Updated domain name.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Domain updated.", body = ApiResponse<WorkspaceDomain>),
|
||||
(status = 400, description = "Domain is empty.", 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 update_domain(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<(String, Uuid)>,
|
||||
params: web::Json<UpdateDomainParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let (ws_name, domain_id) = path.into_inner();
|
||||
let ws = service.workspace.find_workspace_by_name(&ws_name).await?;
|
||||
let data = service
|
||||
.workspace
|
||||
.workspace_update_domain(&session, &ws, domain_id, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -1,34 +1,26 @@
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::api::user::upload_avatar::parse_avatar_field;
|
||||
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.",
|
||||
description = "Upload an avatar image for a workspace. Requires admin role. Maximum size 5 MB. Supported: png, jpg, gif, webp. Accepts multipart/form-data with a single 'avatar' field.",
|
||||
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"
|
||||
content_type = "multipart/form-data",
|
||||
description = "Avatar image file in a multipart form field named 'avatar'."
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Avatar uploaded.", body = ApiResponse<Workspace>),
|
||||
@@ -42,19 +34,13 @@ pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
query: web::Query<UploadAvatarQuery>,
|
||||
body: web::Bytes,
|
||||
payload: Multipart,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let ws = service.workspace.find_workspace_by_name(&path).await?;
|
||||
let data = service
|
||||
let (data, content_type, file_name) = parse_avatar_field(payload).await?;
|
||||
let ws = service
|
||||
.workspace
|
||||
.workspace_upload_avatar(
|
||||
&session,
|
||||
&ws,
|
||||
body.to_vec(),
|
||||
query.content_type.clone(),
|
||||
query.file_name.clone(),
|
||||
)
|
||||
.workspace_upload_avatar(&session, &ws, data, content_type, file_name)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(ws)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user