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:
zhenyi
2026-06-10 18:49:27 +08:00
parent 4586b79cb8
commit cec6dce955
161 changed files with 7522 additions and 349 deletions
+33
View File
@@ -0,0 +1,33 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::auth::change_password::ChangePasswordParams;
use crate::session::Session;
#[utoipa::path(
post,
path = "/api/v1/auth/password/change",
tag = "Auth",
operation_id = "authChangePassword",
request_body(content = ChangePasswordParams, description = "Password change parameters (passwords encrypted with session RSA public key)", content_type = "application/json"),
responses(
(status = 200, description = "Password changed successfully", body = ApiEmptyResponse),
(status = 400, description = "Invalid password", body = ApiErrorResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn change_password(
service: web::Data<AppService>,
session: Session,
params: web::Json<ChangePasswordParams>,
) -> Result<HttpResponse, AppError> {
service
.auth
.auth_change_password(&session, params.into_inner())
.await?;
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("password changed successfully")))
}
+5
View File
@@ -1,4 +1,5 @@
pub mod captcha; pub mod captcha;
pub mod change_password;
pub mod disable_2fa; pub mod disable_2fa;
pub mod enable_2fa; pub mod enable_2fa;
pub mod get_2fa_status; pub mod get_2fa_status;
@@ -52,6 +53,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.route( .route(
"/2fa/backup-codes/regenerate", "/2fa/backup-codes/regenerate",
web::post().to(regenerate_2fa_backup_codes::handle), web::post().to(regenerate_2fa_backup_codes::handle),
)
.route(
"/password/change",
web::post().to(change_password::change_password),
), ),
); );
} }
+51
View File
@@ -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::channels::ChannelCategory;
use crate::service::AppService;
use crate::service::im::ImSession;
use crate::service::im::categories::CreateCategoryParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
}
#[utoipa::path(
post,
path = "/api/v1/im/workspaces/{workspace_name}/categories",
tag = "IM",
operation_id = "imCategoryCreate",
params(PathParams),
request_body(
content = CreateCategoryParams,
description = "Category creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Category created successfully", body = ApiResponse<ChannelCategory>),
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Workspace not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn category_create(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<CreateCategoryParams>,
) -> Result<HttpResponse, AppError> {
let user_id = session.user().ok_or(AppError::Unauthorized)?;
let im_session = ImSession::new(user_id);
let result = service
.im
.category_create(&im_session, &path.workspace_name, params.into_inner())
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
+44
View File
@@ -0,0 +1,44 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::im::ImSession;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub category_id: uuid::Uuid,
}
/// Delete a category
#[utoipa::path(
delete,
path = "/api/v1/im/workspaces/{workspace_name}/categories/{category_id}",
tag = "IM",
operation_id = "imCategoryDelete",
params(PathParams),
responses(
(status = 200, description = "Category deleted successfully", body = ApiEmptyResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Workspace or category not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn category_delete(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let user_id = session.user().ok_or(AppError::Unauthorized)?;
let im_session = ImSession::new(user_id);
service
.im
.category_delete(&im_session, &path.workspace_name, path.category_id)
.await?;
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("Category deleted")))
}
+44
View File
@@ -0,0 +1,44 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::channels::ChannelCategory;
use crate::service::AppService;
use crate::service::im::ImSession;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
}
/// List categories
#[utoipa::path(
get,
path = "/api/v1/im/workspaces/{workspace_name}/categories",
tag = "IM",
operation_id = "imCategoryList",
params(PathParams),
responses(
(status = 200, description = "Categories listed successfully", body = ApiResponse<Vec<ChannelCategory>>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Workspace not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn category_list(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let user_id = session.user().ok_or(AppError::Unauthorized)?;
let im_session = ImSession::new(user_id);
let result = service
.im
.category_list(&im_session, &path.workspace_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+58
View File
@@ -0,0 +1,58 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::channels::ChannelCategory;
use crate::service::AppService;
use crate::service::im::ImSession;
use crate::service::im::categories::UpdateCategoryParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub category_id: uuid::Uuid,
}
/// Update a category
#[utoipa::path(
put,
path = "/api/v1/im/workspaces/{workspace_name}/categories/{category_id}",
tag = "IM",
operation_id = "imCategoryUpdate",
params(PathParams),
request_body(
content = UpdateCategoryParams,
description = "Category update parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Category updated successfully", body = ApiResponse<ChannelCategory>),
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Workspace or category not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn category_update(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<UpdateCategoryParams>,
) -> Result<HttpResponse, AppError> {
let user_id = session.user().ok_or(AppError::Unauthorized)?;
let im_session = ImSession::new(user_id);
let result = service
.im
.category_update(
&im_session,
&path.workspace_name,
path.category_id,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+65
View File
@@ -0,0 +1,65 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::base_info::resolve_users;
use crate::models::channels::ChannelDetail;
use crate::service::AppService;
use crate::service::im::ImSession;
use crate::service::im::channels::CreateChannelParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
}
/// Create a channel
#[utoipa::path(
post,
path = "/api/v1/im/workspaces/{workspace_name}/channels",
tag = "IM",
operation_id = "imChannelCreate",
params(PathParams),
request_body(
content = CreateChannelParams,
description = "Channel creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Channel created successfully", body = ApiResponse<ChannelDetail>),
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Workspace not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn channel_create(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<CreateChannelParams>,
) -> Result<HttpResponse, AppError> {
let user_id = session.user().ok_or(AppError::Unauthorized)?;
let im_session = ImSession::new(user_id);
let request_id = uuid::Uuid::now_v7();
let channel = service
.im
.channel_create(
&im_session,
&path.workspace_name,
params.into_inner(),
request_id,
)
.await?;
let db = &service.ctx.db;
let users = resolve_users(db, &[channel.created_by]).await?;
let creator = users.get(&channel.created_by).cloned().unwrap_or_default();
let detail = channel.into_detail(creator);
Ok(HttpResponse::Created().json(ApiResponse::new(detail)))
}
+50
View File
@@ -0,0 +1,50 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::im::ImSession;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub channel_id: uuid::Uuid,
}
/// Delete a channel
#[utoipa::path(
delete,
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}",
tag = "IM",
operation_id = "imChannelDelete",
params(PathParams),
responses(
(status = 200, description = "Channel deleted successfully", body = ApiEmptyResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Workspace or channel not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn channel_delete(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let user_id = session.user().ok_or(AppError::Unauthorized)?;
let im_session = ImSession::new(user_id);
let request_id = uuid::Uuid::now_v7();
service
.im
.channel_delete(
&im_session,
&path.workspace_name,
path.channel_id,
request_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("Channel deleted")))
}
+52
View File
@@ -0,0 +1,52 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::base_info::resolve_users;
use crate::models::channels::ChannelDetail;
use crate::service::AppService;
use crate::service::im::ImSession;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub channel_id: uuid::Uuid,
}
/// Get a channel
#[utoipa::path(
get,
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}",
tag = "IM",
operation_id = "imChannelGet",
params(PathParams),
responses(
(status = 200, description = "Channel retrieved successfully", body = ApiResponse<ChannelDetail>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Workspace or channel not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn channel_get(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let user_id = session.user().ok_or(AppError::Unauthorized)?;
let im_session = ImSession::new(user_id);
let channel = service
.im
.channel_get(&im_session, &path.workspace_name, path.channel_id)
.await?;
let db = &service.ctx.db;
let users = resolve_users(db, &[channel.created_by]).await?;
let creator = users.get(&channel.created_by).cloned().unwrap_or_default();
let detail = channel.into_detail(creator);
Ok(HttpResponse::Ok().json(ApiResponse::new(detail)))
}
+82
View File
@@ -0,0 +1,82 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use uuid::Uuid;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::base_info::resolve_users;
use crate::models::channels::ChannelDetail;
use crate::service::AppService;
use crate::service::im::ImSession;
use crate::service::im::channels::ChannelListFilters;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub channel_type: Option<String>,
pub channel_kind: Option<String>,
pub category_id: Option<uuid::Uuid>,
pub archived: Option<bool>,
pub limit: Option<i64>,
pub offset: Option<i64>,
}
/// List channels
#[utoipa::path(
get,
path = "/api/v1/im/workspaces/{workspace_name}/channels",
tag = "IM",
operation_id = "imChannelList",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Channels listed successfully", body = ApiResponse<Vec<ChannelDetail>>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Workspace not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn channel_list(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let user_id = session.user().ok_or(AppError::Unauthorized)?;
let im_session = ImSession::new(user_id);
let filters = ChannelListFilters {
channel_type: query.channel_type.clone(),
channel_kind: query.channel_kind.clone(),
category_id: query.category_id,
archived: query.archived,
};
let result = service
.im
.channel_list(
&im_session,
&path.workspace_name,
filters,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
let db = &service.ctx.db;
let creator_ids: Vec<Uuid> = result.iter().map(|c| c.created_by).collect();
let users = resolve_users(db, &creator_ids).await?;
let details: Vec<ChannelDetail> = result
.into_iter()
.map(|c| {
let creator = users.get(&c.created_by).cloned().unwrap_or_default();
c.into_detail(creator)
})
.collect();
Ok(HttpResponse::Ok().json(ApiResponse::new(details)))
}
+67
View File
@@ -0,0 +1,67 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::base_info::resolve_users;
use crate::models::channels::ChannelDetail;
use crate::service::AppService;
use crate::service::im::ImSession;
use crate::service::im::channels::UpdateChannelParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub channel_id: uuid::Uuid,
}
/// Update a channel
#[utoipa::path(
put,
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}",
tag = "IM",
operation_id = "imChannelUpdate",
params(PathParams),
request_body(
content = UpdateChannelParams,
description = "Channel update parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Channel updated successfully", body = ApiResponse<ChannelDetail>),
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Workspace or channel not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn channel_update(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<UpdateChannelParams>,
) -> Result<HttpResponse, AppError> {
let user_id = session.user().ok_or(AppError::Unauthorized)?;
let im_session = ImSession::new(user_id);
let request_id = uuid::Uuid::now_v7();
let channel = service
.im
.channel_update(
&im_session,
&path.workspace_name,
path.channel_id,
params.into_inner(),
request_id,
)
.await?;
let db = &service.ctx.db;
let users = resolve_users(db, &[channel.created_by]).await?;
let creator = users.get(&channel.created_by).cloned().unwrap_or_default();
let detail = channel.into_detail(creator);
Ok(HttpResponse::Ok().json(ApiResponse::new(detail)))
}
+58
View File
@@ -0,0 +1,58 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::channels::ChannelMember;
use crate::service::AppService;
use crate::service::im::ImSession;
use crate::service::im::members::InviteMemberParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub channel_id: uuid::Uuid,
}
/// Invite a member
#[utoipa::path(
post,
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}/members",
tag = "IM",
operation_id = "imMemberInvite",
params(PathParams),
request_body(
content = InviteMemberParams,
description = "Invitation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Member invited successfully", body = ApiResponse<ChannelMember>),
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Workspace or channel not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn member_invite(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<InviteMemberParams>,
) -> Result<HttpResponse, AppError> {
let user_id = session.user().ok_or(AppError::Unauthorized)?;
let im_session = ImSession::new(user_id);
let result = service
.im
.member_invite(
&im_session,
&path.workspace_name,
path.channel_id,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
+45
View File
@@ -0,0 +1,45 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::channels::ChannelMember;
use crate::service::AppService;
use crate::service::im::ImSession;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub channel_id: uuid::Uuid,
}
/// Join a channel
#[utoipa::path(
post,
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}/join",
tag = "IM",
operation_id = "imMemberJoin",
params(PathParams),
responses(
(status = 200, description = "Joined channel successfully", body = ApiResponse<ChannelMember>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Workspace or channel not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn member_join(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let user_id = session.user().ok_or(AppError::Unauthorized)?;
let im_session = ImSession::new(user_id);
let result = service
.im
.member_join(&im_session, &path.workspace_name, path.channel_id)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+50
View File
@@ -0,0 +1,50 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::im::ImSession;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub channel_id: uuid::Uuid,
pub user_id: uuid::Uuid,
}
/// Kick a member
#[utoipa::path(
delete,
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}/members/{user_id}",
tag = "IM",
operation_id = "imMemberKick",
params(PathParams),
responses(
(status = 200, description = "Member kicked successfully", body = ApiEmptyResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Workspace, channel or member not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn member_kick(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let user_id = session.user().ok_or(AppError::Unauthorized)?;
let im_session = ImSession::new(user_id);
service
.im
.member_kick(
&im_session,
&path.workspace_name,
path.channel_id,
path.user_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("Member kicked")))
}
+44
View File
@@ -0,0 +1,44 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::im::ImSession;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub channel_id: uuid::Uuid,
}
/// Leave a channel
#[utoipa::path(
post,
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}/leave",
tag = "IM",
operation_id = "imMemberLeave",
params(PathParams),
responses(
(status = 200, description = "Left channel successfully", body = ApiEmptyResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Workspace or channel not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn member_leave(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let user_id = session.user().ok_or(AppError::Unauthorized)?;
let im_session = ImSession::new(user_id);
service
.im
.member_leave(&im_session, &path.workspace_name, path.channel_id)
.await?;
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("Left channel")))
}
+58
View File
@@ -0,0 +1,58 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::channels::ChannelMember;
use crate::service::AppService;
use crate::service::im::ImSession;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub channel_id: uuid::Uuid,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
/// List channel members
#[utoipa::path(
get,
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}/members",
tag = "IM",
operation_id = "imMemberList",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Members listed successfully", body = ApiResponse<Vec<ChannelMember>>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Workspace or channel not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn member_list(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let user_id = session.user().ok_or(AppError::Unauthorized)?;
let im_session = ImSession::new(user_id);
let result = service
.im
.member_list(
&im_session,
&path.workspace_name,
path.channel_id,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+60
View File
@@ -0,0 +1,60 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::channels::ChannelMember;
use crate::service::AppService;
use crate::service::im::ImSession;
use crate::service::im::members::UpdateMemberParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub channel_id: uuid::Uuid,
pub user_id: uuid::Uuid,
}
/// Update member role
#[utoipa::path(
put,
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}/members/{user_id}",
tag = "IM",
operation_id = "imMemberUpdate",
params(PathParams),
request_body(
content = UpdateMemberParams,
description = "Member update parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Member updated successfully", body = ApiResponse<ChannelMember>),
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Workspace, channel or member not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn member_update(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<UpdateMemberParams>,
) -> Result<HttpResponse, AppError> {
let user_id = session.user().ok_or(AppError::Unauthorized)?;
let im_session = ImSession::new(user_id);
let result = service
.im
.member_update(
&im_session,
&path.workspace_name,
path.channel_id,
path.user_id,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+77
View File
@@ -0,0 +1,77 @@
pub mod category_create;
pub mod category_delete;
pub mod category_list;
pub mod category_update;
pub mod channel_create;
pub mod channel_delete;
pub mod channel_get;
pub mod channel_list;
pub mod channel_update;
pub mod member_invite;
pub mod member_join;
pub mod member_kick;
pub mod member_leave;
pub mod member_list;
pub mod member_update;
use actix_web::web;
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/im/workspaces/{workspace_name}")
// Channels
.route("/channels", web::get().to(channel_list::channel_list))
.route("/channels", web::post().to(channel_create::channel_create))
.route(
"/channels/{channel_id}",
web::get().to(channel_get::channel_get),
)
.route(
"/channels/{channel_id}",
web::put().to(channel_update::channel_update),
)
.route(
"/channels/{channel_id}",
web::delete().to(channel_delete::channel_delete),
)
// Members
.route(
"/channels/{channel_id}/members",
web::get().to(member_list::member_list),
)
.route(
"/channels/{channel_id}/members",
web::post().to(member_invite::member_invite),
)
.route(
"/channels/{channel_id}/members/{user_id}",
web::put().to(member_update::member_update),
)
.route(
"/channels/{channel_id}/members/{user_id}",
web::delete().to(member_kick::member_kick),
)
.route(
"/channels/{channel_id}/join",
web::post().to(member_join::member_join),
)
.route(
"/channels/{channel_id}/leave",
web::post().to(member_leave::member_leave),
)
// Categories
.route("/categories", web::get().to(category_list::category_list))
.route(
"/categories",
web::post().to(category_create::category_create),
)
.route(
"/categories/{category_id}",
web::put().to(category_update::category_update),
)
.route(
"/categories/{category_id}",
web::delete().to(category_delete::category_delete),
),
);
}
+72
View File
@@ -0,0 +1,72 @@
use actix_web::{HttpResponse, web};
use serde::{Deserialize, Serialize};
use crate::api::response::ApiResponse;
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct IssueApiKeyRequest {
pub service_name: String,
pub scopes: Vec<String>,
pub ttl_hours: Option<u64>,
}
#[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct IssueApiKeyResponse {
pub api_key: String,
pub service_name: String,
pub service_id: String,
pub scopes: Vec<String>,
pub expires_at: i64,
}
#[utoipa::path(
post,
path = "/api/v1/internal/api-keys",
tag = "Internal",
operation_id = "internalIssueApiKey",
request_body = IssueApiKeyRequest,
responses(
(status = 200, description = "API key issued", body = ApiResponse<IssueApiKeyResponse>),
(status = 401, description = "Authentication required"),
(status = 403, description = "Admin permission required"),
),
security(("session_cookie" = []))
)]
pub async fn issue_api_key(
session: Session,
service: web::Data<AppService>,
body: web::Json<IssueApiKeyRequest>,
) -> Result<HttpResponse, AppError> {
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
let is_owner: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM workspace WHERE owner_id = $1 AND deleted_at IS NULL)",
)
.bind(user_uid)
.fetch_one(service.ctx.db.reader())
.await
.map_err(AppError::Database)?;
if !is_owner {
return Err(AppError::Forbidden(
"workspace owner permission required".into(),
));
}
let ttl_secs = body.ttl_hours.map(|h| h * 3600);
let (api_key, identity) = service
.internal_auth
.issue_api_key(&body.service_name, body.scopes.clone(), ttl_secs)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(IssueApiKeyResponse {
api_key,
service_name: identity.service_name,
service_id: identity.service_id,
scopes: identity.scopes,
expires_at: identity.expires_at,
})))
}
+10
View File
@@ -0,0 +1,10 @@
pub mod issue_api_key;
use actix_web::web;
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/internal")
.route("/api-keys", web::post().to(issue_api_key::issue_api_key)),
);
}
+10 -3
View File
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError; use crate::error::AppError;
use crate::models::issues::Issue; use crate::models::base_info::{self, UserBaseInfo};
use crate::models::issues::IssueDetail;
use crate::service::AppService; use crate::service::AppService;
use crate::service::issues::core::CreateIssueParams; use crate::service::issues::core::CreateIssueParams;
use crate::session::Session; use crate::session::Session;
@@ -50,7 +51,7 @@ pub struct PathParams {
content_type = "application/json" content_type = "application/json"
), ),
responses( responses(
(status = 201, description = "Issue created successfully. Returns the newly created issue with full metadata.", body = ApiResponse<Issue>), (status = 201, description = "Issue created successfully. Returns the newly created issue with full metadata.", body = ApiResponse<IssueDetail>),
(status = 400, description = "Invalid parameters: empty title, invalid repository/label/milestone references", body = ApiErrorResponse), (status = 400, description = "Invalid parameters: empty title, invalid repository/label/milestone references", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions (requires Member role or higher)", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions (requires Member role or higher)", body = ApiErrorResponse),
@@ -71,5 +72,11 @@ pub async fn create(
.issue .issue
.issue_create(&session, &path.workspace_name, params.into_inner()) .issue_create(&session, &path.workspace_name, params.into_inner())
.await?; .await?;
Ok(HttpResponse::Created().json(ApiResponse::new(issue))) let author_id = issue.author_id;
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
let author = users
.get(&author_id)
.cloned()
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
Ok(HttpResponse::Created().json(ApiResponse::new(issue.into_detail(author))))
} }
+10 -3
View File
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError; use crate::error::AppError;
use crate::models::issues::IssueComment; use crate::models::base_info::{self, UserBaseInfo};
use crate::models::issues::IssueCommentDetail;
use crate::service::AppService; use crate::service::AppService;
use crate::service::issues::comments::CreateCommentParams; use crate::service::issues::comments::CreateCommentParams;
use crate::session::Session; use crate::session::Session;
@@ -38,7 +39,7 @@ pub struct PathParams {
params(PathParams), params(PathParams),
request_body(content = CreateCommentParams, description = "Comment creation parameters", content_type = "application/json"), request_body(content = CreateCommentParams, description = "Comment creation parameters", content_type = "application/json"),
responses( responses(
(status = 201, description = "Comment created successfully.", body = ApiResponse<IssueComment>), (status = 201, description = "Comment created successfully.", body = ApiResponse<IssueCommentDetail>),
(status = 400, description = "Invalid parameters: empty body or issue is locked", body = ApiErrorResponse), (status = 400, description = "Invalid parameters: empty body or issue is locked", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions (issue locked and user lacks write access)", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions (issue locked and user lacks write access)", body = ApiErrorResponse),
@@ -62,5 +63,11 @@ pub async fn create_comment(
params.into_inner(), params.into_inner(),
) )
.await?; .await?;
Ok(HttpResponse::Created().json(ApiResponse::new(comment))) let author_id = comment.author_id;
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
let author = users
.get(&author_id)
.cloned()
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
Ok(HttpResponse::Created().json(ApiResponse::new(comment.into_detail(author))))
} }
+10 -3
View File
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError; use crate::error::AppError;
use crate::models::issues::Issue; use crate::models::base_info::{self, UserBaseInfo};
use crate::models::issues::IssueDetail;
use crate::service::AppService; use crate::service::AppService;
use crate::session::Session; use crate::session::Session;
@@ -27,7 +28,7 @@ pub struct PathParams {
operation_id = "issueGet", operation_id = "issueGet",
params(PathParams), params(PathParams),
responses( responses(
(status = 200, description = "Issue retrieved successfully. Returns complete issue with all metadata.", body = ApiResponse<Issue>), (status = 200, description = "Issue retrieved successfully. Returns complete issue with all metadata.", body = ApiResponse<IssueDetail>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse), (status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
@@ -46,5 +47,11 @@ pub async fn get(
.issue .issue
.issue_get(&session, &path.workspace_name, path.number) .issue_get(&session, &path.workspace_name, path.number)
.await?; .await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(issue))) let author_id = issue.author_id;
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
let author = users
.get(&author_id)
.cloned()
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
Ok(HttpResponse::Ok().json(ApiResponse::new(issue.into_detail(author))))
} }
+16 -3
View File
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError; use crate::error::AppError;
use crate::models::issues::Issue; use crate::models::base_info::{self, UserBaseInfo};
use crate::models::issues::IssueDetail;
use crate::service::AppService; use crate::service::AppService;
use crate::service::issues::core::IssueListFilters; use crate::service::issues::core::IssueListFilters;
use crate::session::Session; use crate::session::Session;
@@ -48,7 +49,7 @@ pub struct QueryParams {
operation_id = "issueList", operation_id = "issueList",
params(PathParams, QueryParams), params(PathParams, QueryParams),
responses( responses(
(status = 200, description = "Issues listed successfully. Returns filtered array of issue objects with metadata.", body = ApiResponse<Vec<Issue>>), (status = 200, description = "Issues listed successfully. Returns filtered array of issue objects with metadata.", body = ApiResponse<Vec<IssueDetail>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Workspace not found", body = ApiErrorResponse), (status = 404, description = "Workspace not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse), (status = 500, description = "Internal server error", body = ApiErrorResponse),
@@ -81,5 +82,17 @@ pub async fn list(
query.offset.unwrap_or(0), query.offset.unwrap_or(0),
) )
.await?; .await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(issues))) let user_ids: Vec<_> = issues.iter().map(|i| i.author_id).collect();
let users = base_info::resolve_users(&service.ctx.db, &user_ids).await?;
let details: Vec<IssueDetail> = issues
.into_iter()
.map(|i| {
let author = users
.get(&i.author_id)
.cloned()
.unwrap_or_else(|| UserBaseInfo::placeholder(i.author_id));
i.into_detail(author)
})
.collect();
Ok(HttpResponse::Ok().json(ApiResponse::new(details)))
} }
+16 -3
View File
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError; use crate::error::AppError;
use crate::models::issues::IssueComment; use crate::models::base_info::{self, UserBaseInfo};
use crate::models::issues::IssueCommentDetail;
use crate::service::AppService; use crate::service::AppService;
use crate::session::Session; use crate::session::Session;
@@ -31,7 +32,7 @@ pub struct QueryParams {
operation_id = "issueListComments", operation_id = "issueListComments",
params(PathParams, QueryParams), params(PathParams, QueryParams),
responses( responses(
(status = 200, description = "Comments listed successfully.", body = ApiResponse<Vec<IssueComment>>), (status = 200, description = "Comments listed successfully.", body = ApiResponse<Vec<IssueCommentDetail>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse), (status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
@@ -55,5 +56,17 @@ pub async fn list_comments(
query.offset.unwrap_or(0), query.offset.unwrap_or(0),
) )
.await?; .await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(comments))) let user_ids: Vec<_> = comments.iter().map(|c| c.author_id).collect();
let users = base_info::resolve_users(&service.ctx.db, &user_ids).await?;
let details: Vec<IssueCommentDetail> = comments
.into_iter()
.map(|c| {
let author = users
.get(&c.author_id)
.cloned()
.unwrap_or_else(|| UserBaseInfo::placeholder(c.author_id));
c.into_detail(author)
})
.collect();
Ok(HttpResponse::Ok().json(ApiResponse::new(details)))
} }
+2 -2
View File
@@ -36,7 +36,7 @@ use actix_web::web;
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
web::scope("/issues") web::scope("")
// Core // Core
.route("", web::get().to(list::list)) .route("", web::get().to(list::list))
.route("", web::post().to(create::create)) .route("", web::post().to(create::create))
@@ -144,7 +144,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
pub fn configure_repo_level(cfg: &mut web::ServiceConfig) { pub fn configure_repo_level(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
web::scope("/issues") web::scope("")
.route("/labels", web::get().to(list_labels::list_labels)) .route("/labels", web::get().to(list_labels::list_labels))
.route("/labels", web::post().to(create_label::create_label)) .route("/labels", web::post().to(create_label::create_label))
.route( .route(
+3
View File
@@ -1,5 +1,8 @@
pub mod auth; pub mod auth;
pub mod im;
pub mod internal;
pub mod issue; pub mod issue;
pub mod notify;
pub mod openapi; pub mod openapi;
pub mod pr; pub mod pr;
pub mod repo; pub mod repo;
+29
View File
@@ -0,0 +1,29 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
/// Clear all notifications (dismiss all)
#[utoipa::path(
delete,
path = "/api/v1/notifications",
tag = "Notifications",
operation_id = "notificationClearAll",
responses(
(status = 200, description = "All notifications cleared", body = ApiResponse<i64>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn clear_all_notifications(
service: web::Data<AppService>,
session: Session,
) -> Result<HttpResponse, AppError> {
let result = service.notify.clear_all_notifications(&session).await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+40
View File
@@ -0,0 +1,40 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationBlock;
use crate::service::AppService;
use crate::service::notify::blocks::CreateBlockParams;
use crate::session::Session;
/// Create a notification block
#[utoipa::path(
post,
path = "/api/v1/notifications/blocks",
tag = "Notifications",
operation_id = "notificationCreateBlock",
request_body(
content = CreateBlockParams,
description = "Block creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Block created", body = ApiResponse<NotificationBlock>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn create_block(
service: web::Data<AppService>,
session: Session,
params: web::Json<CreateBlockParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.create_block(&session, params.into_inner())
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
+40
View File
@@ -0,0 +1,40 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationSubscription;
use crate::service::AppService;
use crate::service::notify::subscriptions::CreateSubscriptionParams;
use crate::session::Session;
/// Create a notification subscription
#[utoipa::path(
post,
path = "/api/v1/notifications/subscriptions",
tag = "Notifications",
operation_id = "notificationCreateSubscription",
request_body(
content = CreateSubscriptionParams,
description = "Subscription creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Subscription created", body = ApiResponse<NotificationSubscription>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn create_subscription(
service: web::Data<AppService>,
session: Session,
params: web::Json<CreateSubscriptionParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.create_subscription(&session, params.into_inner())
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
+41
View File
@@ -0,0 +1,41 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationTemplate;
use crate::service::AppService;
use crate::service::notify::templates::CreateTemplateParams;
use crate::session::Session;
/// Create a notification template (requires system admin)
#[utoipa::path(
post,
path = "/api/v1/notifications/templates",
tag = "Notifications",
operation_id = "notificationCreateTemplate",
request_body(
content = CreateTemplateParams,
description = "Template creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Template created", body = ApiResponse<NotificationTemplate>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "System admin access required", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn create_template(
service: web::Data<AppService>,
session: Session,
params: web::Json<CreateTemplateParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.create_template(&session, params.into_inner())
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
+39
View File
@@ -0,0 +1,39 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub block_id: uuid::Uuid,
}
/// Delete a notification block
#[utoipa::path(
delete,
path = "/api/v1/notifications/blocks/{block_id}",
tag = "Notifications",
operation_id = "notificationDeleteBlock",
params(PathParams),
responses(
(status = 200, description = "Block deleted", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Block not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn delete_block(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service.notify.delete_block(&session, path.block_id).await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Block deleted".to_string())))
}
+42
View File
@@ -0,0 +1,42 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub notification_id: uuid::Uuid,
}
/// Delete a notification
#[utoipa::path(
delete,
path = "/api/v1/notifications/{notification_id}",
tag = "Notifications",
operation_id = "notificationDelete",
params(PathParams),
responses(
(status = 200, description = "Notification deleted", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Notification not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn delete_notification(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.notify
.delete_notification(&session, path.notification_id)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Notification deleted".to_string())))
}
+42
View File
@@ -0,0 +1,42 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub subscription_id: uuid::Uuid,
}
/// Delete a notification subscription
#[utoipa::path(
delete,
path = "/api/v1/notifications/subscriptions/{subscription_id}",
tag = "Notifications",
operation_id = "notificationDeleteSubscription",
params(PathParams),
responses(
(status = 200, description = "Subscription deleted", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Subscription not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn delete_subscription(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.notify
.delete_subscription(&session, path.subscription_id)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Subscription deleted".to_string())))
}
+43
View File
@@ -0,0 +1,43 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub template_id: uuid::Uuid,
}
/// Delete a notification template (requires system admin)
#[utoipa::path(
delete,
path = "/api/v1/notifications/templates/{template_id}",
tag = "Notifications",
operation_id = "notificationDeleteTemplate",
params(PathParams),
responses(
(status = 200, description = "Template deleted", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "System admin access required", body = ApiErrorResponse),
(status = 404, description = "Template not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn delete_template(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.notify
.delete_template(&session, path.template_id)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Template deleted".to_string())))
}
+66
View File
@@ -0,0 +1,66 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::base_info;
use crate::models::notifications::NotificationDetail;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub notification_id: uuid::Uuid,
}
/// Dismiss a notification
#[utoipa::path(
post,
path = "/api/v1/notifications/{notification_id}/dismiss",
tag = "Notifications",
operation_id = "notificationDismiss",
params(PathParams),
responses(
(status = 200, description = "Notification dismissed", body = ApiResponse<NotificationDetail>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Notification not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn dismiss_notification(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let notification = service
.notify
.dismiss_notification(&session, path.notification_id)
.await?;
let actor = match notification.actor_id {
Some(id) => base_info::resolve_users(&service.ctx.db, &[id])
.await?
.remove(&id),
None => None,
};
let workspace = match notification.workspace_id {
Some(id) => base_info::resolve_workspaces(&service.ctx.db, &[id])
.await?
.remove(&id),
None => None,
};
let repo = match notification.repo_id {
Some(id) => base_info::resolve_repos(&service.ctx.db, &[id])
.await?
.remove(&id),
None => None,
};
Ok(HttpResponse::Ok().json(ApiResponse::new(
notification.into_detail(actor, workspace, repo),
)))
}
+44
View File
@@ -0,0 +1,44 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationTemplate;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub template_id: uuid::Uuid,
}
/// Get a notification template by ID (requires system admin)
#[utoipa::path(
get,
path = "/api/v1/notifications/templates/{template_id}",
tag = "Notifications",
operation_id = "notificationGetTemplate",
params(PathParams),
responses(
(status = 200, description = "Template retrieved", body = ApiResponse<NotificationTemplate>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "System admin access required", body = ApiErrorResponse),
(status = 404, description = "Template not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn get_template(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.get_template(&session, path.template_id)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+29
View File
@@ -0,0 +1,29 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
/// Get unread notification count for the current user
#[utoipa::path(
get,
path = "/api/v1/notifications/unread-count",
tag = "Notifications",
operation_id = "notificationUnreadCount",
responses(
(status = 200, description = "Unread count returned successfully", body = ApiResponse<i64>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn get_unread_count(
service: web::Data<AppService>,
session: Session,
) -> Result<HttpResponse, AppError> {
let result = service.notify.count_unread(&session).await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+47
View File
@@ -0,0 +1,47 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationBlock;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
/// List notification blocks for the current user
#[utoipa::path(
get,
path = "/api/v1/notifications/blocks",
tag = "Notifications",
operation_id = "notificationListBlocks",
params(QueryParams),
responses(
(status = 200, description = "Blocks listed successfully", body = ApiResponse<Vec<NotificationBlock>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_blocks(
service: web::Data<AppService>,
session: Session,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.list_blocks(
&session,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+47
View File
@@ -0,0 +1,47 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationDelivery;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
/// List notification deliveries for the current user
#[utoipa::path(
get,
path = "/api/v1/notifications/deliveries",
tag = "Notifications",
operation_id = "notificationListDeliveries",
params(QueryParams),
responses(
(status = 200, description = "Deliveries listed successfully", body = ApiResponse<Vec<NotificationDelivery>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_deliveries(
service: web::Data<AppService>,
session: Session,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.list_deliveries(
&session,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
@@ -0,0 +1,55 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationDelivery;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub notification_id: uuid::Uuid,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
/// List deliveries for a specific notification
#[utoipa::path(
get,
path = "/api/v1/notifications/{notification_id}/deliveries",
tag = "Notifications",
operation_id = "notificationListDeliveriesForNotification",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Deliveries listed successfully", body = ApiResponse<Vec<NotificationDelivery>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Notification not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_deliveries_for_notification(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.list_deliveries_for_notification(
&session,
path.notification_id,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+72
View File
@@ -0,0 +1,72 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::base_info;
use crate::models::notifications::NotificationDetail;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub unread_only: Option<bool>,
pub limit: Option<i64>,
pub offset: Option<i64>,
}
/// List notifications for the current user
#[utoipa::path(
get,
path = "/api/v1/notifications",
tag = "Notifications",
operation_id = "notificationList",
params(QueryParams),
responses(
(status = 200, description = "Notifications listed successfully", body = ApiResponse<Vec<NotificationDetail>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_notifications(
service: web::Data<AppService>,
session: Session,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let notifications = service
.notify
.list_notifications(
&session,
query.unread_only.unwrap_or(false),
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
let actor_ids: Vec<_> = notifications.iter().filter_map(|n| n.actor_id).collect();
let workspace_ids: Vec<_> = notifications
.iter()
.filter_map(|n| n.workspace_id)
.collect();
let repo_ids: Vec<_> = notifications.iter().filter_map(|n| n.repo_id).collect();
let actors = base_info::resolve_users(&service.ctx.db, &actor_ids).await?;
let workspaces = base_info::resolve_workspaces(&service.ctx.db, &workspace_ids).await?;
let repos = base_info::resolve_repos(&service.ctx.db, &repo_ids).await?;
let details: Vec<NotificationDetail> = notifications
.into_iter()
.map(|n| {
let actor = n.actor_id.and_then(|id| actors.get(&id).cloned());
let workspace = n.workspace_id.and_then(|id| workspaces.get(&id).cloned());
let repo = n.repo_id.and_then(|id| repos.get(&id).cloned());
n.into_detail(actor, workspace, repo)
})
.collect();
Ok(HttpResponse::Ok().json(ApiResponse::new(details)))
}
+47
View File
@@ -0,0 +1,47 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationSubscription;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
/// List notification subscriptions for the current user
#[utoipa::path(
get,
path = "/api/v1/notifications/subscriptions",
tag = "Notifications",
operation_id = "notificationListSubscriptions",
params(QueryParams),
responses(
(status = 200, description = "Subscriptions listed successfully", body = ApiResponse<Vec<NotificationSubscription>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_subscriptions(
service: web::Data<AppService>,
session: Session,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.list_subscriptions(
&session,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationTemplate;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
/// List notification templates (requires system admin)
#[utoipa::path(
get,
path = "/api/v1/notifications/templates",
tag = "Notifications",
operation_id = "notificationListTemplates",
params(QueryParams),
responses(
(status = 200, description = "Templates listed successfully", body = ApiResponse<Vec<NotificationTemplate>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "System admin access required", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_templates(
service: web::Data<AppService>,
session: Session,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.list_templates(
&session,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+29
View File
@@ -0,0 +1,29 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
/// Mark all notifications as read
#[utoipa::path(
put,
path = "/api/v1/notifications/read-all",
tag = "Notifications",
operation_id = "notificationMarkAllAsRead",
responses(
(status = 200, description = "All notifications marked as read", body = ApiResponse<i64>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn mark_all_as_read(
service: web::Data<AppService>,
session: Session,
) -> Result<HttpResponse, AppError> {
let result = service.notify.mark_all_as_read(&session).await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+66
View File
@@ -0,0 +1,66 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::base_info;
use crate::models::notifications::NotificationDetail;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub notification_id: uuid::Uuid,
}
/// Mark a notification as read
#[utoipa::path(
put,
path = "/api/v1/notifications/{notification_id}/read",
tag = "Notifications",
operation_id = "notificationMarkAsRead",
params(PathParams),
responses(
(status = 200, description = "Notification marked as read", body = ApiResponse<NotificationDetail>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Notification not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn mark_as_read(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let notification = service
.notify
.mark_as_read(&session, path.notification_id)
.await?;
let actor = match notification.actor_id {
Some(id) => base_info::resolve_users(&service.ctx.db, &[id])
.await?
.remove(&id),
None => None,
};
let workspace = match notification.workspace_id {
Some(id) => base_info::resolve_workspaces(&service.ctx.db, &[id])
.await?
.remove(&id),
None => None,
};
let repo = match notification.repo_id {
Some(id) => base_info::resolve_repos(&service.ctx.db, &[id])
.await?
.remove(&id),
None => None,
};
Ok(HttpResponse::Ok().json(ApiResponse::new(
notification.into_detail(actor, workspace, repo),
)))
}
+108
View File
@@ -0,0 +1,108 @@
pub mod clear_all_notifications;
pub mod create_block;
pub mod create_subscription;
pub mod create_template;
pub mod delete_block;
pub mod delete_notification;
pub mod delete_subscription;
pub mod delete_template;
pub mod dismiss_notification;
pub mod get_template;
pub mod get_unread_count;
pub mod list_blocks;
pub mod list_deliveries;
pub mod list_deliveries_for_notification;
pub mod list_notifications;
pub mod list_subscriptions;
pub mod list_templates;
pub mod mark_all_as_read;
pub mod mark_as_read;
pub mod update_subscription;
pub mod update_template;
use actix_web::web;
/// Configure notification routes under `/api/v1/notifications`
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/notifications")
// Non-parameterized paths first
.route("", web::get().to(list_notifications::list_notifications))
.route(
"",
web::delete().to(clear_all_notifications::clear_all_notifications),
)
.route(
"/unread-count",
web::get().to(get_unread_count::get_unread_count),
)
.route(
"/read-all",
web::put().to(mark_all_as_read::mark_all_as_read),
)
// Parameterized notification paths
.route(
"/{notification_id}",
web::delete().to(delete_notification::delete_notification),
)
.route(
"/{notification_id}/read",
web::put().to(mark_as_read::mark_as_read),
)
.route(
"/{notification_id}/dismiss",
web::post().to(dismiss_notification::dismiss_notification),
)
.route(
"/{notification_id}/deliveries",
web::get().to(list_deliveries_for_notification::list_deliveries_for_notification),
)
// Subscriptions
.route(
"/subscriptions",
web::get().to(list_subscriptions::list_subscriptions),
)
.route(
"/subscriptions",
web::post().to(create_subscription::create_subscription),
)
.route(
"/subscriptions/{subscription_id}",
web::put().to(update_subscription::update_subscription),
)
.route(
"/subscriptions/{subscription_id}",
web::delete().to(delete_subscription::delete_subscription),
)
// Blocks
.route("/blocks", web::get().to(list_blocks::list_blocks))
.route("/blocks", web::post().to(create_block::create_block))
.route(
"/blocks/{block_id}",
web::delete().to(delete_block::delete_block),
)
// Deliveries
.route(
"/deliveries",
web::get().to(list_deliveries::list_deliveries),
)
// Templates
.route("/templates", web::get().to(list_templates::list_templates))
.route(
"/templates",
web::post().to(create_template::create_template),
)
.route(
"/templates/{template_id}",
web::get().to(get_template::get_template),
)
.route(
"/templates/{template_id}",
web::put().to(update_template::update_template),
)
.route(
"/templates/{template_id}",
web::delete().to(delete_template::delete_template),
),
);
}
+50
View File
@@ -0,0 +1,50 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationSubscription;
use crate::service::AppService;
use crate::service::notify::subscriptions::UpdateSubscriptionParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub subscription_id: uuid::Uuid,
}
/// Update a notification subscription
#[utoipa::path(
put,
path = "/api/v1/notifications/subscriptions/{subscription_id}",
tag = "Notifications",
operation_id = "notificationUpdateSubscription",
params(PathParams),
request_body(
content = UpdateSubscriptionParams,
description = "Subscription update parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Subscription updated", body = ApiResponse<NotificationSubscription>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Subscription not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn update_subscription(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<UpdateSubscriptionParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.update_subscription(&session, path.subscription_id, params.into_inner())
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+51
View File
@@ -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::notifications::NotificationTemplate;
use crate::service::AppService;
use crate::service::notify::templates::UpdateTemplateParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub template_id: uuid::Uuid,
}
/// Update a notification template (requires system admin)
#[utoipa::path(
put,
path = "/api/v1/notifications/templates/{template_id}",
tag = "Notifications",
operation_id = "notificationUpdateTemplate",
params(PathParams),
request_body(
content = UpdateTemplateParams,
description = "Template update parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Template updated", body = ApiResponse<NotificationTemplate>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "System admin access required", body = ApiErrorResponse),
(status = 404, description = "Template not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn update_template(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<UpdateTemplateParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.update_template(&session, path.template_id, params.into_inner())
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+231 -22
View File
@@ -13,34 +13,52 @@ use crate::api::repo::accept_invitation::AcceptInvitationParams;
use crate::api::repo::set_branch_protection::SetBranchProtectionParams; use crate::api::repo::set_branch_protection::SetBranchProtectionParams;
use crate::api::repo::transfer_owner::TransferOwnerParams; use crate::api::repo::transfer_owner::TransferOwnerParams;
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse, ApiResponse}; use crate::api::response::{ApiEmptyResponse, ApiErrorResponse, ApiResponse};
use crate::api::user::upload_avatar::AvatarData;
use crate::api::wiki::compare_revisions::WikiCompareResult; use crate::api::wiki::compare_revisions::WikiCompareResult;
use crate::api::workspace::accept_invitation::AcceptInvitationRequest; use crate::api::workspace::accept_invitation::AcceptInvitationRequest;
use crate::api::workspace::review_approval::ReviewApprovalRequest; use crate::api::workspace::review_approval::ReviewApprovalRequest;
use crate::api::workspace::transfer_owner::TransferOwnerRequest; use crate::api::workspace::transfer_owner::TransferOwnerRequest;
use crate::models::base_info::{
ChannelBaseInfo, IssueBaseInfo, PullRequestBaseInfo,
RepoBaseInfo, UserBaseInfo, WikiPageBaseInfo, WorkspaceBaseInfo,
};
use crate::models::channels::channel::ChannelDetail;
use crate::models::issues::issue::IssueDetail;
use crate::models::issues::issue_comments::IssueCommentDetail;
use crate::models::issues::{ use crate::models::issues::{
Issue, IssueAssignee, IssueComment, IssueEvent, IssueLabel, IssueLabelRelation, IssueMilestone, Issue, IssueAssignee, IssueComment, IssueEvent, IssueLabel, IssueLabelRelation, IssueMilestone,
IssuePrRelation, IssueReaction, IssueRepoRelation, IssueSubscriber, IssueTemplate, IssuePrRelation, IssueReaction, IssueRepoRelation, IssueSubscriber, IssueTemplate,
}; };
use crate::models::notifications::notification::NotificationDetail;
use crate::models::notifications::{
Notification, NotificationBlock, NotificationDelivery, NotificationSubscription,
NotificationTemplate,
};
use crate::models::prs::pr_review::PrReviewDetail;
use crate::models::prs::pull_request::PullRequestDetail;
use crate::models::prs::{ use crate::models::prs::{
PrAssignee, PrCheckRun, PrCommit, PrEvent, PrFile, PrLabel, PrLabelRelation, PrMergeStrategy, PrAssignee, PrCheckRun, PrCommit, PrEvent, PrFile, PrLabel, PrLabelRelation, PrMergeStrategy,
PrReaction, PrReview, PrReviewComment, PrStatus, PrSubscription, PullRequest, PrReaction, PrReview, PrReviewComment, PrStatus, PrSubscription, PullRequest,
}; };
use crate::models::repos::repo::RepoDetail;
use crate::models::repos::{ use crate::models::repos::{
BranchProtectionRule, Repo, RepoBranch, RepoCommitComment, RepoCommitStatus, RepoDeployKey, BranchProtectionRule, Repo, RepoBranch, RepoCommitComment, RepoCommitStatus, RepoDeployKey,
RepoFork, RepoInvitation, RepoMember, RepoRelease, RepoStar, RepoStats, RepoTag, RepoWatch, RepoFork, RepoInvitation, RepoMember, RepoRelease, RepoStar, RepoStats, RepoTag, RepoWatch,
RepoWebhook, RepoWebhook,
}; };
use crate::models::users::{ use crate::models::users::{
User, UserAppearance, UserDevice, UserGpgKey, UserNotifySetting, UserProfile, UserSecurityLog, User, UserAppearance, UserBlock, UserDevice, UserFollow, UserGpgKey, UserNotifySetting,
UserSshKey, UserPresence, UserProfile, UserSecurityLog, UserSshKey,
}; };
use crate::models::wiki::{WikiPage, WikiPageRevision}; use crate::models::wiki::{WikiPage, WikiPageRevision};
use crate::models::workspaces::workspace::WorkspaceDetail;
use crate::models::workspaces::{ use crate::models::workspaces::{
Workspace, WorkspaceAuditLog, WorkspaceBilling, WorkspaceCustomBranding, WorkspaceDomain, Workspace, WorkspaceAuditLog, WorkspaceBilling, WorkspaceCustomBranding, WorkspaceDomain,
WorkspaceIntegration, WorkspaceInvitation, WorkspaceMember, WorkspacePendingApproval, WorkspaceIntegration, WorkspaceInvitation, WorkspaceMember, WorkspacePendingApproval,
WorkspaceSettings, WorkspaceStats, WorkspaceWebhook, WorkspaceSettings, WorkspaceStats, WorkspaceWebhook,
}; };
use crate::service::auth::captcha::{CaptchaQuery, CaptchaResponse}; use crate::service::auth::captcha::{CaptchaQuery, CaptchaResponse};
use crate::service::auth::change_password::ChangePasswordParams;
use crate::service::auth::email::{EmailChangeRequest, EmailResponse, EmailVerifyRequest}; use crate::service::auth::email::{EmailChangeRequest, EmailResponse, EmailVerifyRequest};
use crate::service::auth::login::LoginParams; use crate::service::auth::login::LoginParams;
use crate::service::auth::me::ContextMe; use crate::service::auth::me::ContextMe;
@@ -60,6 +78,12 @@ use crate::service::issues::pr_relations::LinkPrParams;
use crate::service::issues::reactions::CreateIssueReactionParams; use crate::service::issues::reactions::CreateIssueReactionParams;
use crate::service::issues::repo_relations::LinkRepoParams; use crate::service::issues::repo_relations::LinkRepoParams;
use crate::service::issues::templates::{CreateTemplateParams, UpdateTemplateParams}; use crate::service::issues::templates::{CreateTemplateParams, UpdateTemplateParams};
use crate::service::notify::blocks::CreateBlockParams;
use crate::service::notify::subscriptions::{CreateSubscriptionParams, UpdateSubscriptionParams};
use crate::service::notify::templates::{
CreateTemplateParams as NotifyCreateTemplateParams,
UpdateTemplateParams as NotifyUpdateTemplateParams,
};
use crate::service::pr::check_runs::{CreateCheckRunParams, UpdateCheckRunParams}; use crate::service::pr::check_runs::{CreateCheckRunParams, UpdateCheckRunParams};
use crate::service::pr::core::{CreatePrParams, MergePrParams, PrListFilters, UpdatePrParams}; use crate::service::pr::core::{CreatePrParams, MergePrParams, PrListFilters, UpdatePrParams};
use crate::service::pr::labels::{CreatePrLabelParams, UpdatePrLabelParams}; use crate::service::pr::labels::{CreatePrLabelParams, UpdatePrLabelParams};
@@ -80,30 +104,36 @@ use crate::service::repo::protection::{
BranchMergeCheck, CreateProtectionRuleParams, UpdateProtectionRuleParams, BranchMergeCheck, CreateProtectionRuleParams, UpdateProtectionRuleParams,
}; };
use crate::service::repo::releases::{CreateReleaseParams, UpdateReleaseParams}; use crate::service::repo::releases::{CreateReleaseParams, UpdateReleaseParams};
use crate::service::repo::tags::CreateTagParams; use crate::service::repo::tags::{CreateTagParams, UpdateTagParams};
use crate::service::repo::watches::WatchParams; use crate::service::repo::watches::WatchParams;
use crate::service::repo::webhooks::{ use crate::service::repo::webhooks::{
CreateWebhookParams as RepoCreateWebhookParams, UpdateWebhookParams as RepoUpdateWebhookParams, CreateWebhookParams as RepoCreateWebhookParams, UpdateWebhookParams as RepoUpdateWebhookParams,
}; };
use crate::service::user::account::{ use crate::service::user::account::UpdateUserAccountParams;
UpdateUserAccountParams, UploadUserAvatarParams, UserAvatarResponse,
};
use crate::service::user::appearance::UpdateUserAppearanceParams; use crate::service::user::appearance::UpdateUserAppearanceParams;
use crate::service::user::keys::{AddGpgKeyParams, AddSshKeyParams}; use crate::service::user::keys::{AddGpgKeyParams, AddSshKeyParams};
use crate::service::user::notify::UpdateUserNotifySettingParams; use crate::service::user::notify::UpdateUserNotifySettingParams;
use crate::service::user::profile::UpdateUserProfileParams; use crate::service::user::profile::UpdateUserProfileParams;
use crate::service::user::security::{UserOAuthInfo, UserPersonalAccessTokenInfo, UserSessionInfo}; use crate::service::user::security::{
CreatePersonalAccessTokenResponse, UserOAuthInfo, UserPersonalAccessTokenInfo, UserSessionInfo,
};
use crate::service::wiki::core::{CreateWikiPageParams, UpdateWikiPageParams}; use crate::service::wiki::core::{CreateWikiPageParams, UpdateWikiPageParams};
use crate::service::workspace::approvals::RequestApprovalParams; use crate::service::workspace::approvals::RequestApprovalParams;
use crate::service::workspace::billing::UpdateBillingParams; use crate::service::workspace::billing::UpdateBillingParams;
use crate::service::workspace::branding::UpdateBrandingParams; use crate::service::workspace::branding::UpdateBrandingParams;
use crate::service::workspace::core::{CreateWorkspaceParams, UpdateWorkspaceParams}; use crate::service::workspace::core::{CreateWorkspaceParams, UpdateWorkspaceParams};
use crate::service::workspace::domains::AddDomainParams; use crate::service::workspace::domains::{AddDomainParams, UpdateDomainParams};
use crate::service::workspace::integrations::{CreateIntegrationParams, UpdateIntegrationParams}; use crate::service::workspace::integrations::{CreateIntegrationParams, UpdateIntegrationParams};
use crate::service::workspace::invitations::{CreateInvitationParams, CreateInvitationResponse}; use crate::service::workspace::invitations::{CreateInvitationParams, CreateInvitationResponse};
use crate::service::workspace::members::{AddMemberParams, UpdateMemberRoleParams}; use crate::service::workspace::members::{AddMemberParams, UpdateMemberRoleParams};
use crate::service::workspace::settings::UpdateWorkspaceSettingsParams; use crate::service::workspace::settings::UpdateWorkspaceSettingsParams;
use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookParams}; use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookParams};
// IM Channel models
use crate::models::channels::{Channel, ChannelCategory, ChannelMember};
// IM Service params
use crate::service::im::categories::{CreateCategoryParams, UpdateCategoryParams};
use crate::service::im::channels::{ChannelListFilters, CreateChannelParams, UpdateChannelParams};
use crate::service::im::members::{InviteMemberParams, UpdateMemberParams};
#[derive(OpenApi)] #[derive(OpenApi)]
#[openapi( #[openapi(
@@ -120,6 +150,9 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
(name = "Issues", description = "Issue tracking, comments, labels, milestones, assignees, events, reactions, subscribers, templates, and cross-references."), (name = "Issues", description = "Issue tracking, comments, labels, milestones, assignees, events, reactions, subscribers, templates, and cross-references."),
(name = "Pull Requests", description = "Pull request lifecycle including reviews, check runs, merge strategies, labels, assignees, events, reactions, and subscriptions."), (name = "Pull Requests", description = "Pull request lifecycle including reviews, check runs, merge strategies, labels, assignees, events, reactions, and subscriptions."),
(name = "Wiki", description = "Wiki page management including CRUD operations, revision history, version comparison, and page reversion."), (name = "Wiki", description = "Wiki page management including CRUD operations, revision history, version comparison, and page reversion."),
(name = "Notifications", description = "User notification management including listing, reading, dismissing, deleting, subscriptions, blocks, deliveries, and templates."),
(name = "Git", description = "Git-level operations including commits, branches, merges, rebase, blame, tree, blob, tags, and repository health/statistics endpoints."),
(name = "IM", description = "Channel management, member administration, and category organization."),
), ),
paths( paths(
// Auth // Auth
@@ -140,6 +173,7 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
crate::api::auth::verify_2fa::handle, crate::api::auth::verify_2fa::handle,
crate::api::auth::disable_2fa::handle, crate::api::auth::disable_2fa::handle,
crate::api::auth::regenerate_2fa_backup_codes::handle, crate::api::auth::regenerate_2fa_backup_codes::handle,
crate::api::auth::change_password::change_password,
// User // User
crate::api::user::get_account::get_account, crate::api::user::get_account::get_account,
crate::api::user::update_account::update_account, crate::api::user::update_account::update_account,
@@ -166,6 +200,15 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
crate::api::user::list_security_logs::list_security_logs, crate::api::user::list_security_logs::list_security_logs,
crate::api::user::list_personal_access_tokens::list_tokens, crate::api::user::list_personal_access_tokens::list_tokens,
crate::api::user::revoke_personal_access_token::revoke_token, crate::api::user::revoke_personal_access_token::revoke_token,
crate::api::user::create_personal_access_token::create_token,
crate::api::user::get_presence::get_presence,
crate::api::user::update_presence::update_presence,
crate::api::user::list_blocks::list_blocks,
crate::api::user::block_user::block_user,
crate::api::user::unblock_user::unblock_user,
crate::api::user::list_follows::list_follows,
crate::api::user::follow_user::follow_user,
crate::api::user::unfollow_user::unfollow_user,
// Issues // Issues
crate::api::issue::list::list, crate::api::issue::list::list,
crate::api::issue::get::get, crate::api::issue::get::get,
@@ -226,11 +269,9 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
// Pull Requests - Commits & Files // Pull Requests - Commits & Files
crate::api::pr::list_commits::list_commits, crate::api::pr::list_commits::list_commits,
crate::api::pr::list_files::list_files, crate::api::pr::list_files::list_files,
// Pull Requests - Status & Merge Strategy
crate::api::pr::get_status::get_status, crate::api::pr::get_status::get_status,
crate::api::pr::merge_strategy::get_merge_strategy, crate::api::pr::merge_strategy::get_merge_strategy,
crate::api::pr::merge_strategy::update_merge_strategy, crate::api::pr::merge_strategy::update_merge_strategy,
// Pull Requests - Labels
crate::api::pr::labels::list_labels, crate::api::pr::labels::list_labels,
crate::api::pr::labels::create_label, crate::api::pr::labels::create_label,
crate::api::pr::labels::update_label, crate::api::pr::labels::update_label,
@@ -238,11 +279,9 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
crate::api::pr::labels::list_label_relations, crate::api::pr::labels::list_label_relations,
crate::api::pr::labels::assign_label, crate::api::pr::labels::assign_label,
crate::api::pr::labels::unassign_label, crate::api::pr::labels::unassign_label,
// Pull Requests - Assignees
crate::api::pr::assignees::list_assignees, crate::api::pr::assignees::list_assignees,
crate::api::pr::assignees::assign_user, crate::api::pr::assignees::assign_user,
crate::api::pr::assignees::unassign_user, crate::api::pr::assignees::unassign_user,
// Pull Requests - Reviews
crate::api::pr::reviews::list_reviews, crate::api::pr::reviews::list_reviews,
crate::api::pr::reviews::create_review, crate::api::pr::reviews::create_review,
crate::api::pr::reviews::submit_review, crate::api::pr::reviews::submit_review,
@@ -251,23 +290,18 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
crate::api::pr::reviews::add_review_reply, crate::api::pr::reviews::add_review_reply,
crate::api::pr::reviews::update_review_comment, crate::api::pr::reviews::update_review_comment,
crate::api::pr::reviews::delete_review_comment, crate::api::pr::reviews::delete_review_comment,
// Pull Requests - Check Runs
crate::api::pr::check_runs::list_check_runs, crate::api::pr::check_runs::list_check_runs,
crate::api::pr::check_runs::create_check_run, crate::api::pr::check_runs::create_check_run,
crate::api::pr::check_runs::update_check_run, crate::api::pr::check_runs::update_check_run,
crate::api::pr::check_runs::delete_check_run, crate::api::pr::check_runs::delete_check_run,
// Pull Requests - Events
crate::api::pr::events::list_events, crate::api::pr::events::list_events,
// Pull Requests - Reactions
crate::api::pr::reactions::list_reactions, crate::api::pr::reactions::list_reactions,
crate::api::pr::reactions::add_reaction, crate::api::pr::reactions::add_reaction,
crate::api::pr::reactions::remove_reaction, crate::api::pr::reactions::remove_reaction,
// Pull Requests - Subscriptions
crate::api::pr::subscriptions::list_subscriptions, crate::api::pr::subscriptions::list_subscriptions,
crate::api::pr::subscriptions::subscribe, crate::api::pr::subscriptions::subscribe,
crate::api::pr::subscriptions::unsubscribe, crate::api::pr::subscriptions::unsubscribe,
crate::api::pr::subscriptions::mute, crate::api::pr::subscriptions::mute,
// Wiki
crate::api::wiki::list_pages::list_pages, crate::api::wiki::list_pages::list_pages,
crate::api::wiki::get_page::get_page, crate::api::wiki::get_page::get_page,
crate::api::wiki::create_page::create_page, crate::api::wiki::create_page::create_page,
@@ -277,7 +311,27 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
crate::api::wiki::list_revisions::list_revisions, crate::api::wiki::list_revisions::list_revisions,
crate::api::wiki::get_revision::get_revision, crate::api::wiki::get_revision::get_revision,
crate::api::wiki::compare_revisions::compare_revisions, crate::api::wiki::compare_revisions::compare_revisions,
// Workspaces crate::api::notify::list_notifications::list_notifications,
crate::api::notify::get_unread_count::get_unread_count,
crate::api::notify::mark_as_read::mark_as_read,
crate::api::notify::mark_all_as_read::mark_all_as_read,
crate::api::notify::dismiss_notification::dismiss_notification,
crate::api::notify::delete_notification::delete_notification,
crate::api::notify::clear_all_notifications::clear_all_notifications,
crate::api::notify::list_subscriptions::list_subscriptions,
crate::api::notify::create_subscription::create_subscription,
crate::api::notify::update_subscription::update_subscription,
crate::api::notify::delete_subscription::delete_subscription,
crate::api::notify::list_blocks::list_blocks,
crate::api::notify::create_block::create_block,
crate::api::notify::delete_block::delete_block,
crate::api::notify::list_deliveries::list_deliveries,
crate::api::notify::list_deliveries_for_notification::list_deliveries_for_notification,
crate::api::notify::list_templates::list_templates,
crate::api::notify::get_template::get_template,
crate::api::notify::create_template::create_template,
crate::api::notify::update_template::update_template,
crate::api::notify::delete_template::delete_template,
crate::api::workspace::list::handle, crate::api::workspace::list::handle,
crate::api::workspace::get::handle, crate::api::workspace::get::handle,
crate::api::workspace::create::handle, crate::api::workspace::create::handle,
@@ -321,7 +375,17 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
crate::api::workspace::request_approval::handle, crate::api::workspace::request_approval::handle,
crate::api::workspace::review_approval::handle, crate::api::workspace::review_approval::handle,
crate::api::workspace::audit_logs::handle, crate::api::workspace::audit_logs::handle,
// Repos crate::api::workspace::restore::restore_workspace,
crate::api::workspace::billing_history::billing_history,
crate::api::workspace::list_webhook_deliveries::handle,
crate::api::workspace::retry_webhook_delivery::handle,
crate::api::workspace::update_domain::update_domain,
crate::api::workspace::get_member::handle,
crate::api::workspace::get_invitation::handle,
crate::api::workspace::get_webhook::handle,
crate::api::workspace::get_integration::handle,
crate::api::workspace::get_domain::handle,
crate::api::workspace::get_approval::handle,
crate::api::repo::list::list, crate::api::repo::list::list,
crate::api::repo::get::get, crate::api::repo::get::get,
crate::api::repo::create::create, crate::api::repo::create::create,
@@ -381,6 +445,61 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
crate::api::repo::resolve_commit_comment::resolve_commit_comment, crate::api::repo::resolve_commit_comment::resolve_commit_comment,
crate::api::repo::get_stats::get_stats, crate::api::repo::get_stats::get_stats,
crate::api::repo::refresh_stats::refresh_stats, crate::api::repo::refresh_stats::refresh_stats,
crate::api::repo::get_branch::get_branch,
crate::api::repo::get_tag::get_tag,
crate::api::repo::get_release::get_release,
crate::api::repo::get_webhook::get_webhook,
crate::api::repo::get_deploy_key::get_deploy_key,
crate::api::repo::get_member::get_member,
crate::api::repo::get_invitation::get_invitation,
crate::api::repo::update_commit_comment::update_commit_comment,
crate::api::repo::update_tag::update_tag,
crate::api::repo::delete_fork::delete_fork,
crate::api::repo::get_commit_status::get_commit_status,
crate::api::repo::repo_webhook_deliveries::repo_webhook_deliveries,
crate::api::repo::repo_webhook_retry::repo_webhook_retry,
crate::api::repo::git::git_list_commits::git_list_commits,
crate::api::repo::git::git_get_commit::git_get_commit,
crate::api::repo::git::git_create_commit::git_create_commit,
crate::api::repo::git::git_diff::git_diff,
crate::api::repo::git::git_diff_stats::git_diff_stats,
crate::api::repo::git::git_compare::git_compare,
crate::api::repo::git::git_list_branches::git_list_branches,
crate::api::repo::git::git_get_branch::git_get_branch,
crate::api::repo::git::git_create_branch::git_create_branch,
crate::api::repo::git::git_delete_branch::git_delete_branch,
crate::api::repo::git::git_merge_check::git_merge_check,
crate::api::repo::git::git_merge::git_merge,
crate::api::repo::git::git_rebase::git_rebase,
crate::api::repo::git::git_cherry_pick::git_cherry_pick,
crate::api::repo::git::git_revert::git_revert,
crate::api::repo::git::git_conflicts::git_conflicts,
crate::api::repo::git::git_tree::git_tree,
crate::api::repo::git::git_blob::git_blob,
crate::api::repo::git::git_blame::git_blame,
crate::api::repo::git::git_tags::git_tags,
crate::api::repo::git::git_create_tag::git_create_tag,
crate::api::repo::git::git_delete_tag::git_delete_tag,
crate::api::repo::git::git_info::git_info,
crate::api::repo::git::git_exists::git_exists,
crate::api::repo::git::git_stats::git_stats,
crate::api::repo::git::git_health::git_health,
crate::api::repo::git::git_gc::git_gc,
crate::api::im::channel_list::channel_list,
crate::api::im::channel_create::channel_create,
crate::api::im::channel_get::channel_get,
crate::api::im::channel_update::channel_update,
crate::api::im::channel_delete::channel_delete,
crate::api::im::member_list::member_list,
crate::api::im::member_invite::member_invite,
crate::api::im::member_update::member_update,
crate::api::im::member_kick::member_kick,
crate::api::im::member_join::member_join,
crate::api::im::member_leave::member_leave,
crate::api::im::category_list::category_list,
crate::api::im::category_create::category_create,
crate::api::im::category_update::category_update,
crate::api::im::category_delete::category_delete,
), ),
components(schemas( components(schemas(
ApiEmptyResponse, ApiEmptyResponse,
@@ -416,12 +535,11 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
Regenerate2FABackupCodesResponse, Regenerate2FABackupCodesResponse,
// User // User
ApiResponse<User>, ApiResponse<User>,
ApiResponse<UserAvatarResponse>, ApiResponse<AvatarData>,
ApiResponse<String>, ApiResponse<String>,
User, User,
UpdateUserAccountParams, UpdateUserAccountParams,
UploadUserAvatarParams, AvatarData,
UserAvatarResponse,
ApiResponse<UserAppearance>, ApiResponse<UserAppearance>,
UserAppearance, UserAppearance,
UpdateUserAppearanceParams, UpdateUserAppearanceParams,
@@ -697,6 +815,97 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
RepoCommitComment, RepoCommitComment,
CreateCommitCommentParams, CreateCommitCommentParams,
RepoStats, RepoStats,
// Notifications
ApiResponse<Notification>,
ApiResponse<Vec<Notification>>,
Notification,
ApiResponse<i64>,
ApiResponse<NotificationSubscription>,
ApiResponse<Vec<NotificationSubscription>>,
NotificationSubscription,
CreateSubscriptionParams,
UpdateSubscriptionParams,
ApiResponse<NotificationBlock>,
ApiResponse<Vec<NotificationBlock>>,
NotificationBlock,
CreateBlockParams,
ApiResponse<NotificationDelivery>,
ApiResponse<Vec<NotificationDelivery>>,
NotificationDelivery,
ApiResponse<NotificationTemplate>,
ApiResponse<Vec<NotificationTemplate>>,
NotificationTemplate,
NotifyCreateTemplateParams,
NotifyUpdateTemplateParams,
// Auth additions
ChangePasswordParams,
// User additions - Presence/Block/Follow
ApiResponse<UserPresence>,
UserPresence,
ApiResponse<UserBlock>,
ApiResponse<Vec<UserBlock>>,
UserBlock,
ApiResponse<UserFollow>,
ApiResponse<Vec<UserFollow>>,
UserFollow,
ApiResponse<CreatePersonalAccessTokenResponse>,
CreatePersonalAccessTokenResponse,
// Workspace additions
UpdateDomainParams,
// Repo additions
UpdateTagParams,
// IM - Channels
ApiResponse<Channel>,
ApiResponse<Vec<Channel>>,
Channel,
CreateChannelParams,
UpdateChannelParams,
ChannelListFilters,
// IM - Members
ApiResponse<ChannelMember>,
ApiResponse<Vec<ChannelMember>>,
ChannelMember,
InviteMemberParams,
UpdateMemberParams,
// IM - Categories
ApiResponse<ChannelCategory>,
ApiResponse<Vec<ChannelCategory>>,
ChannelCategory,
CreateCategoryParams,
UpdateCategoryParams,
// BaseInfo types
UserBaseInfo,
WorkspaceBaseInfo,
RepoBaseInfo,
ChannelBaseInfo,
IssueBaseInfo,
PullRequestBaseInfo,
WikiPageBaseInfo,
// Detail types
ApiResponse<RepoDetail>,
ApiResponse<Vec<RepoDetail>>,
RepoDetail,
ApiResponse<WorkspaceDetail>,
ApiResponse<Vec<WorkspaceDetail>>,
WorkspaceDetail,
ApiResponse<ChannelDetail>,
ApiResponse<Vec<ChannelDetail>>,
ChannelDetail,
ApiResponse<IssueDetail>,
ApiResponse<Vec<IssueDetail>>,
IssueDetail,
ApiResponse<IssueCommentDetail>,
ApiResponse<Vec<IssueCommentDetail>>,
IssueCommentDetail,
ApiResponse<PullRequestDetail>,
ApiResponse<Vec<PullRequestDetail>>,
PullRequestDetail,
ApiResponse<PrReviewDetail>,
ApiResponse<Vec<PrReviewDetail>>,
PrReviewDetail,
ApiResponse<NotificationDetail>,
ApiResponse<Vec<NotificationDetail>>,
NotificationDetail,
)) ))
)] )]
pub struct OpenApiDoc; pub struct OpenApiDoc;
+10 -3
View File
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError; use crate::error::AppError;
use crate::models::prs::PullRequest; use crate::models::base_info::{self, UserBaseInfo};
use crate::models::prs::PullRequestDetail;
use crate::service::AppService; use crate::service::AppService;
use crate::service::pr::core::CreatePrParams; use crate::service::pr::core::CreatePrParams;
use crate::session::Session; use crate::session::Session;
@@ -51,7 +52,7 @@ pub struct PathParams {
content_type = "application/json" content_type = "application/json"
), ),
responses( responses(
(status = 201, description = "PR created successfully. Returns the newly created PR with full metadata.", body = ApiResponse<PullRequest>), (status = 201, description = "PR created successfully. Returns the newly created PR with full metadata.", body = ApiResponse<PullRequestDetail>),
(status = 400, description = "Invalid parameters: empty title, non-existent branch/commit, or invalid fork relationship", body = ApiErrorResponse), (status = 400, description = "Invalid parameters: empty title, non-existent branch/commit, or invalid fork relationship", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions (requires Member role or higher)", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions (requires Member role or higher)", body = ApiErrorResponse),
@@ -77,5 +78,11 @@ pub async fn create(
params.into_inner(), params.into_inner(),
) )
.await?; .await?;
Ok(HttpResponse::Created().json(ApiResponse::new(pr))) let author_id = pr.author_id;
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
let author = users
.get(&author_id)
.cloned()
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
Ok(HttpResponse::Created().json(ApiResponse::new(pr.into_detail(author))))
} }
+10 -3
View File
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError; use crate::error::AppError;
use crate::models::prs::PullRequest; use crate::models::base_info::{self, UserBaseInfo};
use crate::models::prs::PullRequestDetail;
use crate::service::AppService; use crate::service::AppService;
use crate::session::Session; use crate::session::Session;
@@ -29,7 +30,7 @@ pub struct PathParams {
operation_id = "prGet", operation_id = "prGet",
params(PathParams), params(PathParams),
responses( responses(
(status = 200, description = "PR retrieved successfully. Returns complete PR with all metadata.", body = ApiResponse<PullRequest>), (status = 200, description = "PR retrieved successfully. Returns complete PR with all metadata.", body = ApiResponse<PullRequestDetail>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to access this repository", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions to access this repository", body = ApiErrorResponse),
(status = 404, description = "Repository, workspace, or PR not found", body = ApiErrorResponse), (status = 404, description = "Repository, workspace, or PR not found", body = ApiErrorResponse),
@@ -48,5 +49,11 @@ pub async fn get(
.pr .pr
.pr_get(&session, &path.workspace_name, &path.repo_name, path.number) .pr_get(&session, &path.workspace_name, &path.repo_name, path.number)
.await?; .await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(pr))) let author_id = pr.author_id;
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
let author = users
.get(&author_id)
.cloned()
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
Ok(HttpResponse::Ok().json(ApiResponse::new(pr.into_detail(author))))
} }
+2 -2
View File
@@ -55,7 +55,7 @@ pub struct QP {
pub offset: Option<i64>, pub offset: Option<i64>,
} }
// ── Repo-level labels ── // Section: Repo-level labels
/// List PR labels in a repository /// List PR labels in a repository
#[utoipa::path( #[utoipa::path(
@@ -189,7 +189,7 @@ pub async fn delete_label(
Ok(HttpResponse::Ok().json(ApiResponse::new("Label deleted".to_string()))) Ok(HttpResponse::Ok().json(ApiResponse::new("Label deleted".to_string())))
} }
// ── PR-level label relations ── // Section: PR-level label relations
/// List labels assigned to a PR /// List labels assigned to a PR
#[utoipa::path( #[utoipa::path(
+16 -3
View File
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError; use crate::error::AppError;
use crate::models::prs::PullRequest; use crate::models::base_info::{self, UserBaseInfo};
use crate::models::prs::PullRequestDetail;
use crate::service::AppService; use crate::service::AppService;
use crate::service::pr::core::PrListFilters; use crate::service::pr::core::PrListFilters;
use crate::session::Session; use crate::session::Session;
@@ -43,7 +44,7 @@ pub struct QueryParams {
operation_id = "prList", operation_id = "prList",
params(PathParams, QueryParams), params(PathParams, QueryParams),
responses( responses(
(status = 200, description = "PRs listed successfully. Returns filtered array of PR objects.", body = ApiResponse<Vec<PullRequest>>), (status = 200, description = "PRs listed successfully. Returns filtered array of PR objects.", body = ApiResponse<Vec<PullRequestDetail>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to access this repository", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions to access this repository", body = ApiErrorResponse),
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse), (status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
@@ -75,5 +76,17 @@ pub async fn list(
query.offset.unwrap_or(0), query.offset.unwrap_or(0),
) )
.await?; .await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(prs))) let user_ids: Vec<_> = prs.iter().map(|p| p.author_id).collect();
let users = base_info::resolve_users(&service.ctx.db, &user_ids).await?;
let details: Vec<PullRequestDetail> = prs
.into_iter()
.map(|p| {
let author = users
.get(&p.author_id)
.cloned()
.unwrap_or_else(|| UserBaseInfo::placeholder(p.author_id));
p.into_detail(author)
})
.collect();
Ok(HttpResponse::Ok().json(ApiResponse::new(details)))
} }
+28 -2
View File
@@ -16,8 +16,10 @@ pub mod merge;
pub mod merge_strategy; pub mod merge_strategy;
pub mod reactions; pub mod reactions;
pub mod reopen; pub mod reopen;
pub mod review_requests;
pub mod reviews; pub mod reviews;
pub mod subscriptions; pub mod subscriptions;
pub mod templates;
pub mod update; pub mod update;
use actix_web::web; use actix_web::web;
@@ -25,12 +27,23 @@ use actix_web::web;
/// Configure PR-level routes under `/workspaces/{workspace_name}/repos/{repo_name}/prs` /// Configure PR-level routes under `/workspaces/{workspace_name}/repos/{repo_name}/prs`
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
web::scope("/prs") web::scope("")
// Repo-level labels // Repo-level labels
.route("/labels", web::get().to(labels::list_labels)) .route("/labels", web::get().to(labels::list_labels))
.route("/labels", web::post().to(labels::create_label)) .route("/labels", web::post().to(labels::create_label))
.route("/labels/{label_id}", web::put().to(labels::update_label)) .route("/labels/{label_id}", web::put().to(labels::update_label))
.route("/labels/{label_id}", web::delete().to(labels::delete_label)) .route("/labels/{label_id}", web::delete().to(labels::delete_label))
// Templates
.route("/templates", web::get().to(templates::list_templates))
.route("/templates", web::post().to(templates::create_template))
.route(
"/templates/{template_id}",
web::put().to(templates::update_template),
)
.route(
"/templates/{template_id}",
web::delete().to(templates::delete_template),
)
// Core // Core
.route("", web::get().to(list::list)) .route("", web::get().to(list::list))
.route("", web::post().to(create::create)) .route("", web::post().to(create::create))
@@ -156,6 +169,19 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
"/{number}/subscribe", "/{number}/subscribe",
web::delete().to(subscriptions::unsubscribe), web::delete().to(subscriptions::unsubscribe),
) )
.route("/{number}/mute", web::put().to(subscriptions::mute)), .route("/{number}/mute", web::put().to(subscriptions::mute))
// Review Requests
.route(
"/{number}/requested_reviewers",
web::get().to(review_requests::list_requested_reviewers),
)
.route(
"/{number}/requested_reviewers",
web::post().to(review_requests::request_reviewers),
)
.route(
"/{number}/requested_reviewers/{user_id}",
web::delete().to(review_requests::remove_requested_reviewer),
),
); );
} }
+116
View File
@@ -0,0 +1,116 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use uuid::Uuid;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::prs::PrReviewRequest;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub number: i64,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct ReviewerPathParams {
pub workspace_name: String,
pub repo_name: String,
pub number: i64,
pub user_id: Uuid,
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct RequestReviewersBody {
pub reviewer_ids: Vec<Uuid>,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/requested_reviewers",
tag = "PullRequests",
operation_id = "prListRequestedReviewers",
params(PathParams),
responses(
(status = 200, description = "List of requested reviewers", body = ApiResponse<Vec<PrReviewRequest>>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn list_requested_reviewers(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.pr
.pr_requested_reviewers(&session, &path.workspace_name, &path.repo_name, path.number)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/requested_reviewers",
tag = "PullRequests",
operation_id = "prRequestReviewers",
params(PathParams),
request_body(content = RequestReviewersBody),
responses(
(status = 201, description = "Reviewers requested", body = ApiResponse<Vec<PrReviewRequest>>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn request_reviewers(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
body: web::Json<RequestReviewersBody>,
) -> Result<HttpResponse, AppError> {
let result = service
.pr
.pr_request_reviewers(
&session,
&path.workspace_name,
&path.repo_name,
path.number,
body.reviewer_ids.clone(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/requested_reviewers/{user_id}",
tag = "PullRequests",
operation_id = "prRemoveRequestedReviewer",
params(ReviewerPathParams),
responses(
(status = 200, description = "Reviewer removed", body = ApiResponse<String>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn remove_requested_reviewer(
service: web::Data<AppService>,
session: Session,
path: web::Path<ReviewerPathParams>,
) -> Result<HttpResponse, AppError> {
service
.pr
.pr_remove_requested_reviewer(
&session,
&path.workspace_name,
&path.repo_name,
path.number,
path.user_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("reviewer removed".to_string())))
}
+41 -9
View File
@@ -4,7 +4,9 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError; use crate::error::AppError;
use crate::models::prs::{PrReview, PrReviewComment}; use crate::models::base_info;
use crate::models::base_info::UserBaseInfo;
use crate::models::prs::{PrReviewComment, PrReviewDetail};
use crate::service::AppService; use crate::service::AppService;
use crate::service::pr::reviews::{ use crate::service::pr::reviews::{
AddReplyParams, CreateReviewParams, DismissReviewParams, SubmitReviewParams, AddReplyParams, CreateReviewParams, DismissReviewParams, SubmitReviewParams,
@@ -59,7 +61,7 @@ pub struct QP {
operation_id = "prListReviews", operation_id = "prListReviews",
params(PrPath, QP), params(PrPath, QP),
responses( responses(
(status = 200, description = "Reviews listed.", body = ApiResponse<Vec<PrReview>>), (status = 200, description = "Reviews listed.", body = ApiResponse<Vec<PrReviewDetail>>),
(status = 401, description = "Authentication required", body = ApiErrorResponse), (status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "PR not found", body = ApiErrorResponse), (status = 404, description = "PR not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse), (status = 500, description = "Internal server error", body = ApiErrorResponse),
@@ -83,7 +85,19 @@ pub async fn list_reviews(
query.offset.unwrap_or(0), query.offset.unwrap_or(0),
) )
.await?; .await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(reviews))) let user_ids: Vec<_> = reviews.iter().map(|r| r.author_id).collect();
let users = base_info::resolve_users(&service.ctx.db, &user_ids).await?;
let details: Vec<PrReviewDetail> = reviews
.into_iter()
.map(|r| {
let author = users
.get(&r.author_id)
.cloned()
.unwrap_or_else(|| UserBaseInfo::placeholder(r.author_id));
r.into_detail(author)
})
.collect();
Ok(HttpResponse::Ok().json(ApiResponse::new(details)))
} }
/// Create a review. States: pending, approved, changes_requested, commented. Authors cannot approve their own PRs. /// Create a review. States: pending, approved, changes_requested, commented. Authors cannot approve their own PRs.
@@ -95,7 +109,7 @@ pub async fn list_reviews(
params(PrPath), params(PrPath),
request_body(content = CreateReviewParams, description = "Review parameters", content_type = "application/json"), request_body(content = CreateReviewParams, description = "Review parameters", content_type = "application/json"),
responses( responses(
(status = 201, description = "Review created.", body = ApiResponse<PrReview>), (status = 201, description = "Review created.", body = ApiResponse<PrReviewDetail>),
(status = 400, description = "Invalid state or self-approval", body = ApiErrorResponse), (status = 400, description = "Invalid state or self-approval", body = ApiErrorResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse), (status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
@@ -120,7 +134,13 @@ pub async fn create_review(
params.into_inner(), params.into_inner(),
) )
.await?; .await?;
Ok(HttpResponse::Created().json(ApiResponse::new(review))) let author_id = review.author_id;
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
let author = users
.get(&author_id)
.cloned()
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
Ok(HttpResponse::Created().json(ApiResponse::new(review.into_detail(author))))
} }
/// Submit a pending review. Changes its state to approved, changes_requested, or commented. /// Submit a pending review. Changes its state to approved, changes_requested, or commented.
@@ -132,7 +152,7 @@ pub async fn create_review(
params(ReviewPath), params(ReviewPath),
request_body(content = SubmitReviewParams, description = "Submit parameters", content_type = "application/json"), request_body(content = SubmitReviewParams, description = "Submit parameters", content_type = "application/json"),
responses( responses(
(status = 200, description = "Review submitted.", body = ApiResponse<PrReview>), (status = 200, description = "Review submitted.", body = ApiResponse<PrReviewDetail>),
(status = 400, description = "Invalid state or self-approval", body = ApiErrorResponse), (status = 400, description = "Invalid state or self-approval", body = ApiErrorResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse), (status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
@@ -158,7 +178,13 @@ pub async fn submit_review(
params.into_inner(), params.into_inner(),
) )
.await?; .await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(review))) let author_id = review.author_id;
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
let author = users
.get(&author_id)
.cloned()
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
Ok(HttpResponse::Ok().json(ApiResponse::new(review.into_detail(author))))
} }
/// Dismiss a submitted review. Requires Admin role. /// Dismiss a submitted review. Requires Admin role.
@@ -170,7 +196,7 @@ pub async fn submit_review(
params(ReviewPath), params(ReviewPath),
request_body(content = DismissReviewParams, description = "Dismiss parameters", content_type = "application/json"), request_body(content = DismissReviewParams, description = "Dismiss parameters", content_type = "application/json"),
responses( responses(
(status = 200, description = "Review dismissed.", body = ApiResponse<PrReview>), (status = 200, description = "Review dismissed.", body = ApiResponse<PrReviewDetail>),
(status = 401, description = "Authentication required", body = ApiErrorResponse), (status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions (Admin required)", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions (Admin required)", body = ApiErrorResponse),
(status = 404, description = "Review not found or not submitted", body = ApiErrorResponse), (status = 404, description = "Review not found or not submitted", body = ApiErrorResponse),
@@ -195,7 +221,13 @@ pub async fn dismiss_review(
params.into_inner(), params.into_inner(),
) )
.await?; .await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(review))) let author_id = review.author_id;
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
let author = users
.get(&author_id)
.cloned()
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
Ok(HttpResponse::Ok().json(ApiResponse::new(review.into_detail(author))))
} }
/// List comments for a specific review /// List comments for a specific review
+152
View File
@@ -0,0 +1,152 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::prs::PrTemplate;
use crate::service::AppService;
use crate::service::pr::templates::{CreatePrTemplateParams, UpdatePrTemplateParams};
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct TemplatePathParams {
pub workspace_name: String,
pub repo_name: String,
pub template_id: uuid::Uuid,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/templates",
tag = "PullRequests",
operation_id = "prListTemplates",
params(PathParams, QueryParams),
responses(
(status = 200, description = "List of PR templates", body = ApiResponse<Vec<PrTemplate>>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn list_templates(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.pr
.pr_templates(
&session,
&path.workspace_name,
&path.repo_name,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/templates",
tag = "PullRequests",
operation_id = "prCreateTemplate",
params(PathParams),
request_body(content = CreatePrTemplateParams),
responses(
(status = 201, description = "Template created", body = ApiResponse<PrTemplate>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn create_template(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
body: web::Json<CreatePrTemplateParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.pr
.pr_create_template(
&session,
&path.workspace_name,
&path.repo_name,
body.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
#[utoipa::path(
put,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/templates/{template_id}",
tag = "PullRequests",
operation_id = "prUpdateTemplate",
params(TemplatePathParams),
request_body(content = UpdatePrTemplateParams),
responses(
(status = 200, description = "Template updated", body = ApiResponse<PrTemplate>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn update_template(
service: web::Data<AppService>,
session: Session,
path: web::Path<TemplatePathParams>,
body: web::Json<UpdatePrTemplateParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.pr
.pr_update_template(
&session,
&path.workspace_name,
&path.repo_name,
path.template_id,
body.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/templates/{template_id}",
tag = "PullRequests",
operation_id = "prDeleteTemplate",
params(TemplatePathParams),
responses(
(status = 200, description = "Template deleted", body = ApiResponse<String>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn delete_template(
service: web::Data<AppService>,
session: Session,
path: web::Path<TemplatePathParams>,
) -> Result<HttpResponse, AppError> {
service
.pr
.pr_delete_template(
&session,
&path.workspace_name,
&path.repo_name,
path.template_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("template deleted".to_string())))
}
+53
View File
@@ -0,0 +1,53 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::repo::contributors::Contributor;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/contributors",
tag = "Repos",
operation_id = "repoListContributors",
params(PathParams, QueryParams),
responses(
(status = 200, description = "List of contributors", body = ApiResponse<Vec<Contributor>>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Repo not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn list_contributors(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.repo_contributors(
&session,
&path.workspace_name,
&path.repo_name,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+14 -3
View File
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError; use crate::error::AppError;
use crate::models::repos::Repo; use crate::models::base_info::{resolve_users, resolve_workspaces};
use crate::models::repos::RepoDetail;
use crate::service::AppService; use crate::service::AppService;
use crate::service::repo::core::CreateRepoParams; use crate::service::repo::core::CreateRepoParams;
use crate::session::Session; use crate::session::Session;
@@ -42,7 +43,7 @@ pub struct PathParams {
content_type = "application/json" content_type = "application/json"
), ),
responses( responses(
(status = 201, description = "Repository created successfully. Returns the newly created repository with full metadata.", body = ApiResponse<Repo>), (status = 201, description = "Repository created successfully. Returns the newly created repository with full metadata.", body = ApiResponse<RepoDetail>),
(status = 400, description = "Invalid parameters: name too long, invalid characters, or invalid visibility", body = ApiErrorResponse), (status = 400, description = "Invalid parameters: name too long, invalid characters, or invalid visibility", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to create repositories in this workspace", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions to create repositories in this workspace", body = ApiErrorResponse),
@@ -65,5 +66,15 @@ pub async fn create(
.repo_create(&session, &path.workspace_name, params.into_inner()) .repo_create(&session, &path.workspace_name, params.into_inner())
.await?; .await?;
Ok(HttpResponse::Created().json(ApiResponse::new(repo))) let db = &service.ctx.db;
let users = resolve_users(db, &[repo.owner_id]).await?;
let workspaces = resolve_workspaces(db, &[repo.workspace_id]).await?;
let owner = users.get(&repo.owner_id).cloned().unwrap_or_default();
let workspace = workspaces
.get(&repo.workspace_id)
.cloned()
.unwrap_or_default();
let detail = repo.into_detail(owner, workspace);
Ok(HttpResponse::Created().json(ApiResponse::new(detail)))
} }
+10 -29
View File
@@ -9,42 +9,24 @@ use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)] #[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams { pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String, pub workspace_name: String,
/// Repository name (unique within the workspace)
pub repo_name: String, pub repo_name: String,
/// Branch ID (UUID) pub branch_name: String,
pub branch_id: uuid::Uuid,
} }
/// Delete a branch
///
/// Permanently deletes a branch from the repository. The default branch cannot be deleted.
/// Requires Write role or higher in the repository.
///
/// Effects:
/// - Branch is permanently removed from the repository
/// - All commits exclusive to this branch remain accessible via their SHA
/// - Open pull requests targeting this branch will be closed
///
/// Returns success message on completion.
#[utoipa::path( #[utoipa::path(
delete, delete,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches/{branch_id}", path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches/{branch_name}",
tag = "Repos", tag = "Repos",
operation_id = "repoDeleteBranch", operation_id = "repoDeleteBranch",
params(PathParams), params(PathParams),
responses( responses(
(status = 200, description = "Branch deleted successfully.", body = ApiResponse<String>), (status = 200, description = "Branch deleted", body = ApiResponse<String>),
(status = 400, description = "Cannot delete the default branch", body = ApiErrorResponse), (status = 400, description = "Cannot delete default branch", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), (status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions (requires Write role or higher)", body = ApiErrorResponse), (status = 404, description = "Branch not found", body = ApiErrorResponse),
(status = 404, description = "Repository, workspace, or branch not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error or Git operation failed", body = ApiErrorResponse),
), ),
security( security(("session_cookie" = []))
("session_cookie" = [])
)
)] )]
pub async fn delete_branch( pub async fn delete_branch(
service: web::Data<AppService>, service: web::Data<AppService>,
@@ -53,13 +35,12 @@ pub async fn delete_branch(
) -> Result<HttpResponse, AppError> { ) -> Result<HttpResponse, AppError> {
service service
.repo .repo
.repo_delete_branch( .git_delete_branch(
&session, &session,
&path.workspace_name, &path.workspace_name,
&path.repo_name, &path.repo_name,
path.branch_id, &path.branch_name,
) )
.await?; .await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Branch deleted".to_string())))
Ok(HttpResponse::Ok().json(ApiResponse::new("Branch deleted successfully".to_string())))
} }
+41
View File
@@ -0,0 +1,41 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/fork",
tag = "Repos",
operation_id = "repoDeleteFork",
params(PathParams),
responses(
(status = 200, description = "Fork deleted successfully", body = ApiResponse<String>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Fork not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn delete_fork(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.repo
.repo_delete_fork(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Fork deleted successfully".to_string())))
}
+13 -27
View File
@@ -9,41 +9,23 @@ use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)] #[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams { pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String, pub workspace_name: String,
/// Repository name (unique within the workspace)
pub repo_name: String, pub repo_name: String,
/// Tag ID (UUID) pub tag_name: String,
pub tag_id: uuid::Uuid,
} }
/// Delete a tag
///
/// Permanently deletes a tag from the repository. The tagged commit remains accessible via its SHA.
/// Requires Write role or higher in the repository.
///
/// Effects:
/// - Tag is permanently removed from the repository
/// - The tagged commit remains in the repository history
/// - Releases associated with this tag are not automatically deleted
///
/// Returns success message on completion.
#[utoipa::path( #[utoipa::path(
delete, delete,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/tags/{tag_id}", path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/tags/{tag_name}",
tag = "Repos", tag = "Repos",
operation_id = "repoDeleteTag", operation_id = "repoDeleteTag",
params(PathParams), params(PathParams),
responses( responses(
(status = 200, description = "Tag deleted successfully.", body = ApiResponse<String>), (status = 200, description = "Tag deleted", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), (status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions (requires Write role or higher)", body = ApiErrorResponse), (status = 404, description = "Tag not found", body = ApiErrorResponse),
(status = 404, description = "Repository, workspace, or tag not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error or Git operation failed", body = ApiErrorResponse),
), ),
security( security(("session_cookie" = []))
("session_cookie" = [])
)
)] )]
pub async fn delete_tag( pub async fn delete_tag(
service: web::Data<AppService>, service: web::Data<AppService>,
@@ -52,8 +34,12 @@ pub async fn delete_tag(
) -> Result<HttpResponse, AppError> { ) -> Result<HttpResponse, AppError> {
service service
.repo .repo
.repo_delete_tag(&session, &path.workspace_name, &path.repo_name, path.tag_id) .git_delete_tag(
&session,
&path.workspace_name,
&path.repo_name,
&path.tag_name,
)
.await?; .await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Tag deleted".to_string())))
Ok(HttpResponse::Ok().json(ApiResponse::new("Tag deleted successfully".to_string())))
} }
+14 -3
View File
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError; use crate::error::AppError;
use crate::models::repos::Repo; use crate::models::base_info::{resolve_users, resolve_workspaces};
use crate::models::repos::RepoDetail;
use crate::service::AppService; use crate::service::AppService;
use crate::session::Session; use crate::session::Session;
@@ -42,7 +43,7 @@ use crate::service::repo::fork::ForkRepoParams;
content_type = "application/json" content_type = "application/json"
), ),
responses( responses(
(status = 201, description = "Repository forked successfully. Returns the newly created fork with full metadata.", body = ApiResponse<Repo>), (status = 201, description = "Repository forked successfully. Returns the newly created fork with full metadata.", body = ApiResponse<RepoDetail>),
(status = 400, description = "Invalid parameters: target name conflicts or invalid characters", body = ApiErrorResponse), (status = 400, description = "Invalid parameters: target name conflicts or invalid characters", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to fork or create in target workspace", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions to fork or create in target workspace", body = ApiErrorResponse),
@@ -70,5 +71,15 @@ pub async fn fork_repo(
) )
.await?; .await?;
Ok(HttpResponse::Created().json(ApiResponse::new(repo))) let db = &service.ctx.db;
let users = resolve_users(db, &[repo.owner_id]).await?;
let workspaces = resolve_workspaces(db, &[repo.workspace_id]).await?;
let owner = users.get(&repo.owner_id).cloned().unwrap_or_default();
let workspace = workspaces
.get(&repo.workspace_id)
.cloned()
.unwrap_or_default();
let detail = repo.into_detail(owner, workspace);
Ok(HttpResponse::Created().json(ApiResponse::new(detail)))
} }
+14 -3
View File
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError; use crate::error::AppError;
use crate::models::repos::Repo; use crate::models::base_info::{resolve_users, resolve_workspaces};
use crate::models::repos::RepoDetail;
use crate::service::AppService; use crate::service::AppService;
use crate::session::Session; use crate::session::Session;
@@ -32,7 +33,7 @@ pub struct PathParams {
operation_id = "repoGet", operation_id = "repoGet",
params(PathParams), params(PathParams),
responses( responses(
(status = 200, description = "Repository retrieved successfully. Returns complete repository metadata including visibility, default branch, creation date, and statistics.", body = ApiResponse<Repo>), (status = 200, description = "Repository retrieved successfully. Returns complete repository metadata including visibility, default branch, creation date, and statistics.", body = ApiResponse<RepoDetail>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to access this repository", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions to access this repository", body = ApiErrorResponse),
(status = 404, description = "Repository not found or access denied", body = ApiErrorResponse), (status = 404, description = "Repository not found or access denied", body = ApiErrorResponse),
@@ -52,5 +53,15 @@ pub async fn get(
.repo_get(&session, &path.workspace_name, &path.repo_name) .repo_get(&session, &path.workspace_name, &path.repo_name)
.await?; .await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(repo))) let db = &service.ctx.db;
let users = resolve_users(db, &[repo.owner_id]).await?;
let workspaces = resolve_workspaces(db, &[repo.workspace_id]).await?;
let owner = users.get(&repo.owner_id).cloned().unwrap_or_default();
let workspace = workspaces
.get(&repo.workspace_id)
.cloned()
.unwrap_or_default();
let detail = repo.into_detail(owner, workspace);
Ok(HttpResponse::Ok().json(ApiResponse::new(detail)))
} }
+45
View File
@@ -0,0 +1,45 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub branch_name: String,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches/{branch_name}",
tag = "Repos",
operation_id = "repoGetBranch",
params(PathParams),
responses(
(status = 200, description = "Branch retrieved", body = ApiResponse<crate::pb::repo::Branch>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Branch not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn get_branch(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_get_branch(
&session,
&path.workspace_name,
&path.repo_name,
&path.branch_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+56
View File
@@ -0,0 +1,56 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoCommitStatus;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub push_commit_id: uuid::Uuid,
pub status_id: uuid::Uuid,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/commits/{push_commit_id}/statuses/{status_id}",
tag = "Repos",
operation_id = "repoGetCommitStatus",
params(PathParams),
responses(
(status = 200, description = "Commit status retrieved successfully", body = ApiResponse<RepoCommitStatus>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Commit status not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn get_commit_status(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let statuses = service
.repo
.repo_commit_statuses(
&session,
&path.workspace_name,
&path.repo_name,
path.push_commit_id,
1000,
0,
)
.await?;
let status = statuses
.into_iter()
.find(|s| s.id == path.status_id)
.ok_or(AppError::NotFound("commit status not found".into()))?;
Ok(HttpResponse::Ok().json(ApiResponse::new(status)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoDeployKey;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub key_id: uuid::Uuid,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/deploy-keys/{key_id}",
tag = "Repos",
operation_id = "repoGetDeployKey",
params(PathParams),
responses(
(status = 200, description = "Deploy key retrieved successfully", body = ApiResponse<RepoDeployKey>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Deploy key not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn get_deploy_key(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let keys = service
.repo
.repo_deploy_keys(&session, &path.workspace_name, &path.repo_name, 1000, 0)
.await?;
let key = keys
.into_iter()
.find(|k| k.id == path.key_id)
.ok_or(AppError::NotFound("deploy key not found".into()))?;
Ok(HttpResponse::Ok().json(ApiResponse::new(key)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoInvitation;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub invitation_id: uuid::Uuid,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/invitations/{invitation_id}",
tag = "Repos",
operation_id = "repoGetInvitation",
params(PathParams),
responses(
(status = 200, description = "Invitation retrieved successfully", body = ApiResponse<RepoInvitation>),
(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 get_invitation(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let invitations = service
.repo
.repo_invitations(&session, &path.workspace_name, &path.repo_name, 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)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoMember;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub member_id: uuid::Uuid,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/members/{member_id}",
tag = "Repos",
operation_id = "repoGetMember",
params(PathParams),
responses(
(status = 200, description = "Member retrieved successfully", body = ApiResponse<RepoMember>),
(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 get_member(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let members = service
.repo
.repo_members(&session, &path.workspace_name, &path.repo_name, 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)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoRelease;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub release_id: uuid::Uuid,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/releases/{release_id}",
tag = "Repos",
operation_id = "repoGetRelease",
params(PathParams),
responses(
(status = 200, description = "Release retrieved successfully", body = ApiResponse<RepoRelease>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Release not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn get_release(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let releases = service
.repo
.repo_releases(&session, &path.workspace_name, &path.repo_name, 1000, 0)
.await?;
let release = releases
.into_iter()
.find(|r| r.id == path.release_id)
.ok_or(AppError::NotFound("release not found".into()))?;
Ok(HttpResponse::Ok().json(ApiResponse::new(release)))
}
+45
View File
@@ -0,0 +1,45 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub tag_name: String,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/tags/{tag_name}",
tag = "Repos",
operation_id = "repoGetTag",
params(PathParams),
responses(
(status = 200, description = "Tag retrieved", body = ApiResponse<crate::pb::repo::Tag>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Tag not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn get_tag(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_get_tag(
&session,
&path.workspace_name,
&path.repo_name,
&path.tag_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoWebhook;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub webhook_id: uuid::Uuid,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/webhooks/{webhook_id}",
tag = "Repos",
operation_id = "repoGetWebhook",
params(PathParams),
responses(
(status = 200, description = "Webhook retrieved successfully", body = ApiResponse<RepoWebhook>),
(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 get_webhook(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let webhooks = service
.repo
.repo_webhooks(&session, &path.workspace_name, &path.repo_name, 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)))
}
+56
View File
@@ -0,0 +1,56 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub revision: String,
pub path: String,
pub page_size: Option<u32>,
}
/// Blame a file
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/blame",
tag = "Git",
operation_id = "gitBlame",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Blame retrieved successfully", body = ApiResponse<crate::pb::repo::BlameResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_blame(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_blame(
&session,
&path.workspace_name,
&path.repo_name,
&query.revision,
&query.path,
query.page_size.unwrap_or(30),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+55
View File
@@ -0,0 +1,55 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub revision: String,
pub path: String,
}
/// Get blob content
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/blobs",
tag = "Git",
operation_id = "gitGetBlob",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Blob retrieved successfully", body = ApiResponse<crate::pb::repo::Blob>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Blob not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_blob(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_get_blob(
&session,
&path.workspace_name,
&path.repo_name,
&query.revision,
&query.path,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+54
View File
@@ -0,0 +1,54 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::repo::git::merge::CherryPickParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Cherry-pick a commit
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/cherry-pick",
tag = "Git",
operation_id = "gitCherryPick",
params(PathParams),
request_body(
content = CherryPickParams,
description = "Cherry-pick parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Cherry-pick completed successfully", body = ApiResponse<crate::pb::repo::CreateCommitResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 409, description = "Cherry-pick conflict", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_cherry_pick(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<CherryPickParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_cherry_pick(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+137
View File
@@ -0,0 +1,137 @@
use crate::api::response::ApiResponse;
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct RevisionPathParams {
pub workspace_name: String,
pub repo_name: String,
pub revision: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct FindCommitQuery {
pub revision: String,
}
#[utoipa::path(get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/find-commit", tag = "Git", operation_id = "gitFindCommit", params(PathParams, FindCommitQuery), responses((status=200,body=ApiResponse<crate::pb::repo::Commit>)), security(("session_cookie"=[])))]
pub async fn git_find_commit(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
q: web::Query<FindCommitQuery>,
) -> Result<HttpResponse, AppError> {
let r = service
.repo
.git_find_commit(&session, &path.workspace_name, &path.repo_name, &q.revision)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(r)))
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct ListCommitsByOidBody {
pub oids: Vec<String>,
}
#[utoipa::path(post, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits/by-oid", tag = "Git", operation_id = "gitCommitsByOid", params(PathParams), request_body(content=ListCommitsByOidBody), responses((status=200,body=ApiResponse<crate::pb::repo::ListCommitsByOidResponse>)), security(("session_cookie"=[])))]
pub async fn git_commits_by_oid(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
body: web::Json<ListCommitsByOidBody>,
) -> Result<HttpResponse, AppError> {
let oids: Vec<Vec<u8>> = body
.oids
.iter()
.map(|s| hex::decode(s).unwrap_or_default())
.collect();
let r = service
.repo
.git_list_commits_by_oid(&session, &path.workspace_name, &path.repo_name, oids)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(r)))
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct AncestorQuery {
pub ancestor: String,
pub descendant: String,
}
#[utoipa::path(get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commit-is-ancestor", tag = "Git", operation_id = "gitCommitIsAncestor", params(PathParams, AncestorQuery), responses((status=200,body=ApiResponse<bool>)), security(("session_cookie"=[])))]
pub async fn git_commit_is_ancestor(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
q: web::Query<AncestorQuery>,
) -> Result<HttpResponse, AppError> {
let r = service
.repo
.git_commit_is_ancestor(
&session,
&path.workspace_name,
&path.repo_name,
&q.ancestor,
&q.descendant,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(r)))
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct LastCommitQuery {
pub path: String,
pub revision: Option<String>,
}
#[utoipa::path(get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/last-commit", tag = "Git", operation_id = "gitLastCommitForPath", params(PathParams, LastCommitQuery), responses((status=200,body=ApiResponse<crate::pb::repo::LastCommitForPathResponse>)), security(("session_cookie"=[])))]
pub async fn git_last_commit(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
q: web::Query<LastCommitQuery>,
) -> Result<HttpResponse, AppError> {
let r = service
.repo
.git_last_commit_for_path(
&session,
&path.workspace_name,
&path.repo_name,
&q.path,
q.revision.as_deref(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(r)))
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct CommitsByMsgQuery {
pub q: String,
pub revision: Option<String>,
pub limit: Option<u32>,
}
#[utoipa::path(get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits/search", tag = "Git", operation_id = "gitCommitsByMessage", params(PathParams, CommitsByMsgQuery), responses((status=200,body=ApiResponse<crate::pb::repo::CommitsByMessageResponse>)), security(("session_cookie"=[])))]
pub async fn git_commits_by_message(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
q: web::Query<CommitsByMsgQuery>,
) -> Result<HttpResponse, AppError> {
let r = service
.repo
.git_commits_by_message(
&session,
&path.workspace_name,
&path.repo_name,
&q.q,
q.revision.as_deref(),
q.limit.unwrap_or(20),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(r)))
}
+45
View File
@@ -0,0 +1,45 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub revision: String,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits/{revision}/stats",
tag = "Git",
operation_id = "gitCommitStats",
params(PathParams),
responses(
(status = 200, description = "Commit stats", body = ApiResponse<crate::pb::repo::CommitStats>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_commit_stats(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_commit_stats(
&session,
&path.workspace_name,
&path.repo_name,
&path.revision,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+59
View File
@@ -0,0 +1,59 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::repo::git::merge::CompareParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub base: String,
pub head: String,
pub page_size: Option<u32>,
}
/// Compare two commits
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/compare",
tag = "Git",
operation_id = "gitCompare",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Comparison completed successfully", body = ApiResponse<crate::pb::repo::CompareCommitsResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_compare(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_compare_commits(
&session,
&path.workspace_name,
&path.repo_name,
CompareParams {
base: query.base.clone(),
head: query.head.clone(),
page_size: query.page_size,
},
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+56
View File
@@ -0,0 +1,56 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub target: String,
pub source: String,
pub page_size: Option<u32>,
}
/// List merge conflicts
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/conflicts",
tag = "Git",
operation_id = "gitListConflicts",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Conflicts listed successfully", body = ApiResponse<crate::pb::repo::ListMergeConflictsResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_conflicts(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_list_conflicts(
&session,
&path.workspace_name,
&path.repo_name,
&query.target,
&query.source,
query.page_size.unwrap_or(30),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+56
View File
@@ -0,0 +1,56 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub revision: Option<String>,
pub path: Option<String>,
pub since: Option<String>,
pub until: Option<String>,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits/count",
tag = "Git",
operation_id = "gitCountCommits",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Commit count", body = ApiResponse<crate::pb::repo::CountCommitsResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_count_commits(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_count_commits(
&session,
&path.workspace_name,
&path.repo_name,
query.revision.as_deref(),
query.path.as_deref(),
query.since.as_deref(),
query.until.as_deref(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+52
View File
@@ -0,0 +1,52 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub left: String,
pub right: String,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/compare/diverging",
tag = "Git",
operation_id = "gitCountDivergingCommits",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Diverging commit counts", body = ApiResponse<crate::pb::repo::CountDivergingCommitsResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_count_diverging(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_count_diverging_commits(
&session,
&path.workspace_name,
&path.repo_name,
&query.left,
&query.right,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+59
View File
@@ -0,0 +1,59 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::{IntoParams, ToSchema};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams, ToSchema)]
pub struct CreateBranchBody {
pub branch_name: String,
pub start_point: String,
}
/// Create a branch
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/branches",
tag = "Git",
operation_id = "gitCreateBranch",
params(PathParams),
request_body(
content = CreateBranchBody,
description = "Branch creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Branch created successfully", body = ApiResponse<crate::pb::repo::Branch>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_create_branch(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
body: web::Json<CreateBranchBody>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_create_branch(
&session,
&path.workspace_name,
&path.repo_name,
&body.branch_name,
&body.start_point,
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
+53
View File
@@ -0,0 +1,53 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::repo::git::merge::CreateCommitParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Create a commit
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits",
tag = "Git",
operation_id = "gitCreateCommit",
params(PathParams),
request_body(
content = CreateCommitParams,
description = "Commit creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Commit created successfully", body = ApiResponse<crate::pb::repo::CreateCommitResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_create_commit(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<CreateCommitParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_create_commit(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
+63
View File
@@ -0,0 +1,63 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::{IntoParams, ToSchema};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams, ToSchema)]
pub struct CreateTagBody {
pub tag_name: String,
pub target: String,
pub message: Option<String>,
pub annotated: Option<bool>,
}
/// Create a tag
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/tags",
tag = "Git",
operation_id = "gitCreateTag",
params(PathParams),
request_body(
content = CreateTagBody,
description = "Tag creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Tag created successfully", body = ApiResponse<crate::pb::repo::Tag>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_create_tag(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
body: web::Json<CreateTagBody>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_create_tag(
&session,
&path.workspace_name,
&path.repo_name,
&body.tag_name,
&body.target,
body.message.clone(),
body.annotated.unwrap_or(false),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
+47
View File
@@ -0,0 +1,47 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub branch_name: String,
}
/// Delete a branch
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/branches/{branch_name}",
tag = "Git",
operation_id = "gitDeleteBranch",
params(PathParams),
responses(
(status = 200, description = "Branch deleted successfully", body = ApiResponse<String>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_delete_branch(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.repo
.git_delete_branch(
&session,
&path.workspace_name,
&path.repo_name,
&path.branch_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Branch deleted successfully".to_string())))
}
+47
View File
@@ -0,0 +1,47 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub tag_name: String,
}
/// Delete a tag
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/tags/{tag_name}",
tag = "Git",
operation_id = "gitDeleteTag",
params(PathParams),
responses(
(status = 200, description = "Tag deleted successfully", body = ApiResponse<String>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_delete_tag(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.repo
.git_delete_tag(
&session,
&path.workspace_name,
&path.repo_name,
&path.tag_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Tag deleted successfully".to_string())))
}
+56
View File
@@ -0,0 +1,56 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub base: String,
pub head: String,
pub page_size: Option<u32>,
}
/// Get diff between two revisions
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/diff",
tag = "Git",
operation_id = "gitDiff",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Diff retrieved successfully", body = ApiResponse<crate::pb::repo::GetDiffResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_diff(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_diff(
&session,
&path.workspace_name,
&path.repo_name,
&query.base,
&query.head,
query.page_size.unwrap_or(30),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+54
View File
@@ -0,0 +1,54 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub base: String,
pub head: String,
}
/// Get diff statistics between two revisions
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/diff-stats",
tag = "Git",
operation_id = "gitDiffStats",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Diff stats retrieved successfully", body = ApiResponse<crate::pb::repo::DiffStats>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_diff_stats(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_diff_stats(
&session,
&path.workspace_name,
&path.repo_name,
&query.base,
&query.head,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+41
View File
@@ -0,0 +1,41 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Check if repository exists
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/exists",
tag = "Git",
operation_id = "gitRepoExists",
params(PathParams),
responses(
(status = 200, description = "Repository existence check completed", body = ApiResponse<bool>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_exists(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_repo_exists(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+41
View File
@@ -0,0 +1,41 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Run garbage collection
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/garbage-collect",
tag = "Git",
operation_id = "gitGarbageCollect",
params(PathParams),
responses(
(status = 200, description = "Garbage collection completed", body = ApiResponse<crate::pb::repo::RepositoryMaintenanceResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_gc(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_garbage_collect(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub branch_name: String,
}
/// Get a branch
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/branches/{branch_name}",
tag = "Git",
operation_id = "gitGetBranch",
params(PathParams),
responses(
(status = 200, description = "Branch retrieved successfully", body = ApiResponse<crate::pb::repo::Branch>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Branch not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_get_branch(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_get_branch(
&session,
&path.workspace_name,
&path.repo_name,
&path.branch_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub revision: String,
}
/// Get a single commit
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits/{revision}",
tag = "Git",
operation_id = "gitGetCommit",
params(PathParams),
responses(
(status = 200, description = "Commit retrieved successfully", body = ApiResponse<crate::pb::repo::Commit>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Commit not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_get_commit(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_get_commit(
&session,
&path.workspace_name,
&path.repo_name,
&path.revision,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+45
View File
@@ -0,0 +1,45 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub tag_name: String,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/tags/{tag_name}",
tag = "Git",
operation_id = "gitGetTag",
params(PathParams),
responses(
(status = 200, description = "Tag details", body = ApiResponse<crate::pb::repo::Tag>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_get_tag(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_get_tag(
&session,
&path.workspace_name,
&path.repo_name,
&path.tag_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+41
View File
@@ -0,0 +1,41 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Check repository health
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/health",
tag = "Git",
operation_id = "gitRepoHealth",
params(PathParams),
responses(
(status = 200, description = "Repository health check completed", body = ApiResponse<crate::pb::repo::RepositoryHealthResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_health(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_repo_health(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+41
View File
@@ -0,0 +1,41 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Get repository info
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/info",
tag = "Git",
operation_id = "gitRepoInfo",
params(PathParams),
responses(
(status = 200, description = "Repository info retrieved successfully", body = ApiResponse<crate::pb::repo::Repository>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_info(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_repo_info(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+39
View File
@@ -0,0 +1,39 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/license",
tag = "Git",
operation_id = "gitLicense",
params(PathParams),
responses(
(status = 200, description = "License detection result", body = ApiResponse<crate::pb::repo::FindLicenseResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_license(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_find_license(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+56
View File
@@ -0,0 +1,56 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub pattern: Option<String>,
pub page_size: Option<u32>,
pub page_token: Option<String>,
}
/// List branches
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/branches",
tag = "Git",
operation_id = "gitListBranches",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Branches listed successfully", body = ApiResponse<crate::pb::repo::ListBranchesResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_list_branches(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_list_branches(
&session,
&path.workspace_name,
&path.repo_name,
query.pattern.clone(),
query.page_size.unwrap_or(30),
query.page_token.clone(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+56
View File
@@ -0,0 +1,56 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub revision: String,
pub path: Option<String>,
pub page_size: Option<u32>,
}
/// List commits
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits",
tag = "Git",
operation_id = "gitListCommits",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Commits listed successfully", body = ApiResponse<crate::pb::repo::ListCommitsResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_list_commits(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_list_commits(
&session,
&path.workspace_name,
&path.repo_name,
&query.revision,
query.path.clone(),
query.page_size.unwrap_or(30),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+54
View File
@@ -0,0 +1,54 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::repo::git::merge::MergeParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Merge branches
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/merge",
tag = "Git",
operation_id = "gitMerge",
params(PathParams),
request_body(
content = MergeParams,
description = "Merge parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Merge completed successfully", body = ApiResponse<crate::pb::repo::MergeResult>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 409, description = "Merge conflict", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_merge(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<MergeParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_merge(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}

Some files were not shown because too many files have changed in this diff Show More