refactor(api): reorder imports and update code formatting across repository endpoints

- Reordered actix-web imports to standardize import order
- Reordered crate module imports to follow alphabetical ordering
- Updated function calls to use multi-line formatting for better readability
- Standardized blank lines around documentation comments
- Applied consistent formatting to response handling methods
- Normalized import organization across all repository-related API files
- Improved code consistency and maintainability through standardized formatting
- Applied formatting updates to all repository endpoint implementations
This commit is contained in:
zhenyi
2026-06-07 19:41:33 +08:00
parent 7368ba676c
commit 4028f0d943
149 changed files with 4962 additions and 369 deletions
+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::issues::IssueAssignee;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
/// User ID (UUID) to assign
pub user_id: uuid::Uuid,
}
/// Assign a user to an issue
///
/// Assigns a workspace member to the given issue.
/// Requires write access to the issue (author or workspace member).
///
/// Effects:
/// - User is assigned to the issue
/// - Assignee is automatically subscribed to the issue
/// - Issue assignee count is incremented
///
/// Returns the created assignment record.
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/assignees/{user_id}",
tag = "Issues",
operation_id = "issueAssign",
params(PathParams),
responses(
(status = 201, description = "User assigned successfully. Returns the created assignment record.", body = ApiResponse<IssueAssignee>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
(status = 404, description = "Issue or user not found", body = ApiErrorResponse),
(status = 409, description = "User is already assigned to this issue", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn assign_issue(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let assignee = service
.issue
.issue_assign(&session, &path.workspace_name, path.number, path.user_id)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(assignee)))
}
+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::models::issues::IssueLabelRelation;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
/// Label ID (UUID) to assign
pub label_id: uuid::Uuid,
}
/// Assign a label to an issue
///
/// Attaches a label to the given issue. The label must belong to a repository in the same workspace.
/// Requires write access to the issue (author or workspace member).
///
/// Effects:
/// - Label is attached to the issue
/// - Issue label count is incremented
///
/// Returns the created label relation.
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/labels/{label_id}",
tag = "Issues",
operation_id = "issueAssignLabel",
params(PathParams),
responses(
(status = 200, description = "Label assigned successfully. Returns the created label relation.", body = ApiResponse<IssueLabelRelation>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
(status = 404, description = "Issue or label not found", body = ApiErrorResponse),
(status = 409, description = "Label is already assigned to this issue", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn assign_label(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let rel = service
.issue
.issue_assign_label(&session, &path.workspace_name, path.number, path.label_id)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(rel)))
}
+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::issues::Issue;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub number: i64,
}
/// Close an issue
///
/// Closes an open issue. The issue is marked as closed and the closing user is recorded.
/// Requires write access to the issue (author or workspace member).
///
/// Effects:
/// - Issue state changes to "closed"
/// - Closed by and closed at are recorded
/// - A "Closed" event is logged
///
/// Returns the closed issue with updated metadata.
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/close",
tag = "Issues",
operation_id = "issueClose",
params(PathParams),
responses(
(status = 200, description = "Issue closed successfully. Returns the closed issue with updated metadata.", body = ApiResponse<Issue>),
(status = 400, description = "Issue is already closed", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to close this issue", body = ApiErrorResponse),
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn close(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let issue = service
.issue
.issue_close(&session, &path.workspace_name, path.number)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(issue)))
}
+75
View File
@@ -0,0 +1,75 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::issues::Issue;
use crate::service::AppService;
use crate::service::issues::core::CreateIssueParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
}
/// Create an issue
///
/// Creates a new issue in the specified workspace.
/// Requires at least Member role in the workspace.
///
/// Parameters:
/// - title: Issue title (required)
/// - body: Issue body in markdown (optional)
/// - priority: Priority level (optional, defaults to "none")
/// - visibility: Visibility setting (optional, defaults to "public")
/// - due_at: Due date (optional)
/// - repo_ids: Related repository IDs
/// - label_ids: Label IDs to apply
/// - assignee_ids: User IDs to assign
/// - milestone_id: Milestone ID to attach
///
/// Effects:
/// - Issue is created with auto-incrementing number
/// - Author is automatically subscribed
/// - Relations, labels, and assignees are attached
/// - Workspace stats are updated
///
/// Returns the created issue with full metadata.
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/issues",
tag = "Issues",
operation_id = "issueCreate",
params(PathParams),
request_body(
content = CreateIssueParams,
description = "Issue creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Issue created successfully. Returns the newly created issue with full metadata.", body = ApiResponse<Issue>),
(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 = 403, description = "Insufficient permissions (requires Member role or higher)", body = ApiErrorResponse),
(status = 404, description = "Workspace or referenced resource not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn create(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<CreateIssueParams>,
) -> Result<HttpResponse, AppError> {
let issue = service
.issue
.issue_create(&session, &path.workspace_name, params.into_inner())
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(issue)))
}
+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::issues::IssueComment;
use crate::service::AppService;
use crate::service::issues::comments::CreateCommentParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub number: i64,
}
/// Create a comment on an issue
///
/// Adds a new comment to an issue. Users with read access can comment unless the issue is locked
/// (in which case only users with write access can comment).
///
/// Parameters:
/// - body: Comment body in markdown format (required)
/// - reply_to_comment_id: ID of parent comment for threaded replies (optional)
///
/// Effects:
/// - Comment is created and attached to the issue
/// - Commenter is automatically subscribed to the issue
/// - Issue comment count is incremented
///
/// Returns the created comment with metadata.
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/comments",
tag = "Issues",
operation_id = "issueCreateComment",
params(PathParams),
request_body(content = CreateCommentParams, description = "Comment creation parameters", content_type = "application/json"),
responses(
(status = 201, description = "Comment created successfully.", body = ApiResponse<IssueComment>),
(status = 400, description = "Invalid parameters: empty body or issue is locked", 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 = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn create_comment(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<CreateCommentParams>,
) -> Result<HttpResponse, AppError> {
let comment = service
.issue
.issue_create_comment(
&session,
&path.workspace_name,
path.number,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(comment)))
}
+62
View File
@@ -0,0 +1,62 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::issues::IssueLabel;
use crate::service::AppService;
use crate::service::issues::labels::CreateLabelParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Create a label
///
/// Creates a new issue label in a repository.
/// Requires at least Member role in the repository.
///
/// Parameters:
/// - name: Label name (required, e.g., "bug", "feature")
/// - color: Hex color code (required, e.g., "#FF0000")
/// - description: Label description (optional)
///
/// Returns the created label with metadata.
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/labels",
tag = "Issues",
operation_id = "issueCreateLabel",
params(PathParams),
request_body(content = CreateLabelParams, description = "Label creation parameters", content_type = "application/json"),
responses(
(status = 201, description = "Label created successfully.", body = ApiResponse<IssueLabel>),
(status = 400, description = "Invalid parameters: empty name or invalid color", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions (requires Member role)", body = ApiErrorResponse),
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn create_label(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<CreateLabelParams>,
) -> Result<HttpResponse, AppError> {
let label = service
.issue
.issue_create_label(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(label)))
}
+70
View File
@@ -0,0 +1,70 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::issues::IssueMilestone;
use crate::service::AppService;
use crate::service::issues::milestones::CreateMilestoneParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Repository name (unique within the workspace)
pub repo_name: String,
}
/// Create a milestone
///
/// Creates a new milestone in a repository for tracking issue progress.
/// Requires at least Member role in the repository.
///
/// Parameters:
/// - title: Milestone title (required)
/// - description: Description of the milestone (optional)
/// - due_at: Target due date (optional)
///
/// Returns the created milestone with metadata.
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/milestones",
tag = "Issues",
operation_id = "issueCreateMilestone",
params(PathParams),
request_body(
content = CreateMilestoneParams,
description = "Milestone creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Milestone created successfully. Returns the newly created milestone with metadata.", body = ApiResponse<IssueMilestone>),
(status = 400, description = "Invalid parameters: empty title", 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 = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn create_milestone(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<CreateMilestoneParams>,
) -> Result<HttpResponse, AppError> {
let milestone = service
.issue
.issue_create_milestone(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(milestone)))
}
+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::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub number: i64,
}
/// Delete an issue
///
/// Soft-deletes an issue. The issue is marked as deleted but remains in the database.
/// Requires Admin role in the workspace (or issue author).
///
/// Effects:
/// - Issue is marked as deleted (soft-delete)
/// - Workspace issue count is decremented
///
/// Returns success message on completion.
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}",
tag = "Issues",
operation_id = "issueDelete",
params(PathParams),
responses(
(status = 200, description = "Issue deleted successfully.", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions (requires Admin role or issue author)", body = ApiErrorResponse),
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn delete(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.issue
.issue_delete(&session, &path.workspace_name, path.number)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Issue deleted successfully".to_string())))
}
+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 number: i64,
pub comment_id: uuid::Uuid,
}
/// Delete an issue comment
///
/// Soft-deletes a comment. The comment author can delete their own comments.
/// Workspace admins can delete any comment.
///
/// Effects:
/// - Comment is marked as deleted
/// - Issue comment count is decremented
///
/// Returns success message on completion.
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/comments/{comment_id}",
tag = "Issues",
operation_id = "issueDeleteComment",
params(PathParams),
responses(
(status = 200, description = "Comment deleted successfully.", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Cannot delete other users' comments (requires admin)", body = ApiErrorResponse),
(status = 404, description = "Workspace, issue, or comment not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn delete_comment(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.issue
.issue_delete_comment(&session, &path.workspace_name, path.number, path.comment_id)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Comment deleted successfully".to_string())))
}
+57
View File
@@ -0,0 +1,57 @@
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 label_id: uuid::Uuid,
}
/// Delete a label
///
/// Permanently removes an issue label from a repository.
/// Requires Admin role in the repository.
///
/// Effects:
/// - Label is permanently deleted
/// - All issue-label relations using this label are removed
///
/// Returns success message on completion.
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/labels/{label_id}",
tag = "Issues",
operation_id = "issueDeleteLabel",
params(PathParams),
responses(
(status = 200, description = "Label deleted successfully.", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions (requires Admin role)", body = ApiErrorResponse),
(status = 404, description = "Repository, workspace, or label not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn delete_label(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.issue
.issue_delete_label(
&session,
&path.workspace_name,
&path.repo_name,
path.label_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Label deleted successfully".to_string())))
}
+62
View File
@@ -0,0 +1,62 @@
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 {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Repository name (unique within the workspace)
pub repo_name: String,
/// Milestone ID (UUID)
pub milestone_id: uuid::Uuid,
}
/// Delete a milestone
///
/// Permanently removes a milestone from the repository.
/// Requires Admin role in the repository.
///
/// Effects:
/// - Milestone is permanently deleted
/// - Issues attached to this milestone lose their milestone association
///
/// Returns success message on completion.
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/milestones/{milestone_id}",
tag = "Issues",
operation_id = "issueDeleteMilestone",
params(PathParams),
responses(
(status = 200, description = "Milestone deleted successfully.", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions (requires Admin role)", body = ApiErrorResponse),
(status = 404, description = "Repository, workspace, or milestone not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn delete_milestone(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.issue
.issue_delete_milestone(
&session,
&path.workspace_name,
&path.repo_name,
path.milestone_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Milestone deleted".to_string())))
}
+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::issues::Issue;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
}
/// Get an issue by number
///
/// Returns detailed information about a specific issue, identified by workspace name and issue number.
/// Requires read access to the issue (public or workspace member).
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}",
tag = "Issues",
operation_id = "issueGet",
params(PathParams),
responses(
(status = 200, description = "Issue retrieved successfully. Returns complete issue with all metadata.", body = ApiResponse<Issue>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn get(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let issue = service
.issue
.issue_get(&session, &path.workspace_name, path.number)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(issue)))
}
+85
View File
@@ -0,0 +1,85 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::issues::Issue;
use crate::service::AppService;
use crate::service::issues::core::IssueListFilters;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
/// Filter by issue state ("open" or "closed")
pub state: Option<String>,
/// Filter by priority level
pub priority: Option<String>,
/// Filter by author user ID
pub author_id: Option<uuid::Uuid>,
/// Filter by assignee user ID
pub assignee_id: Option<uuid::Uuid>,
/// Filter by milestone ID
pub milestone_id: Option<uuid::Uuid>,
/// Filter by label ID
pub label_id: Option<uuid::Uuid>,
/// Maximum number of issues to return (default: 50, max: 100)
pub limit: Option<i64>,
/// Number of issues to skip for pagination (default: 0)
pub offset: Option<i64>,
}
/// List issues in a workspace
///
/// Returns a paginated list of issues in the workspace, sorted by issue number (newest first).
/// Supports filtering by state, priority, author, assignee, milestone, and label.
/// Only returns issues visible to the authenticated user (public + workspace member access).
/// Requires authentication.
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/issues",
tag = "Issues",
operation_id = "issueList",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Issues listed successfully. Returns filtered array of issue objects with metadata.", body = ApiResponse<Vec<Issue>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Workspace not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let filters = IssueListFilters {
state: query.state.clone(),
priority: query.priority.clone(),
author_id: query.author_id,
assignee_id: query.assignee_id,
milestone_id: query.milestone_id,
label_id: query.label_id,
};
let issues = service
.issue
.issue_list(
&session,
&path.workspace_name,
filters,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(issues)))
}
+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::issues::IssueAssignee;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
/// Maximum number of assignees to return (default: 50, max: 100)
pub limit: Option<i64>,
/// Number of assignees to skip for pagination (default: 0)
pub offset: Option<i64>,
}
/// List assignees of an issue
///
/// Returns a paginated list of all users assigned to the given issue.
/// Shows who is assigned, when they were assigned, and who assigned them.
/// Requires read access to the issue.
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/assignees",
tag = "Issues",
operation_id = "issueListAssignees",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Assignees listed successfully. Returns array of assignee objects with assignment metadata.", body = ApiResponse<Vec<IssueAssignee>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
(status = 404, description = "Issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_assignees(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let assignees = service
.issue
.issue_assignees(
&session,
&path.workspace_name,
path.number,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(assignees)))
}
+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::models::issues::IssueComment;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub number: i64,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
/// List issue comments
///
/// Returns a paginated list of comments on an issue, sorted by creation date (oldest first).
/// Requires read access to the issue.
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/comments",
tag = "Issues",
operation_id = "issueListComments",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Comments listed successfully.", body = ApiResponse<Vec<IssueComment>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn list_comments(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let comments = service
.issue
.issue_comments(
&session,
&path.workspace_name,
path.number,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(comments)))
}
+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::issues::IssueEvent;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
/// Maximum number of events to return (default: 50, max: 100)
pub limit: Option<i64>,
/// Number of events to skip for pagination (default: 0)
pub offset: Option<i64>,
}
/// List issue events
///
/// Returns a chronological timeline of all events for the given issue.
/// Events include creation, updates, state changes, assignments, label changes, etc.
/// Sorted by creation date (oldest first for timeline display).
/// Requires read access to the issue.
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/events",
tag = "Issues",
operation_id = "issueListEvents",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Events listed successfully. Returns chronological array of event objects.", body = ApiResponse<Vec<IssueEvent>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
(status = 404, description = "Issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_events(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let events = service
.issue
.issue_list_events(
&session,
&path.workspace_name,
path.number,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(events)))
}
+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::issues::IssueLabelRelation;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
/// Maximum number of label relations to return (default: 50, max: 100)
pub limit: Option<i64>,
/// Number of label relations to skip for pagination (default: 0)
pub offset: Option<i64>,
}
/// List labels assigned to an issue
///
/// Returns a paginated list of all label relations for the given issue.
/// Shows which labels are attached to the issue, with assignment metadata.
/// Requires read access to the issue.
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/labels",
tag = "Issues",
operation_id = "issueListLabelRelations",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Label relations listed successfully. Returns array of label relation objects with metadata.", body = ApiResponse<Vec<IssueLabelRelation>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
(status = 404, description = "Issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_issue_labels(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let rels = service
.issue
.issue_label_relations(
&session,
&path.workspace_name,
path.number,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(rels)))
}
+46
View File
@@ -0,0 +1,46 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::issues::IssueLabel;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// List labels in a repository
///
/// Returns all issue labels defined in the repository, sorted alphabetically.
/// Requires read access to the repository.
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/labels",
tag = "Issues",
operation_id = "issueListLabels",
params(PathParams),
responses(
(status = 200, description = "Labels listed successfully.", body = ApiResponse<Vec<IssueLabel>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn list_labels(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let labels = service
.issue
.issue_labels(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(labels)))
}
+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::issues::IssueMilestone;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Repository name (unique within the workspace)
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
/// Maximum number of milestones to return (default: 50, max: 100)
pub limit: Option<i64>,
/// Number of milestones to skip for pagination (default: 0)
pub offset: Option<i64>,
}
/// List milestones in a repository
///
/// Returns a paginated list of milestones in the repository, sorted by state (open first) then by due date.
/// Includes milestone metadata such as title, description, state, due date, and progress.
/// Requires read access to the repository.
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/milestones",
tag = "Issues",
operation_id = "issueListMilestones",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Milestones listed successfully. Returns array of milestone objects with metadata.", body = ApiResponse<Vec<IssueMilestone>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to access this repository", body = ApiErrorResponse),
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_milestones(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let milestones = service
.issue
.issue_milestones(
&session,
&path.workspace_name,
&path.repo_name,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(milestones)))
}
+62
View File
@@ -0,0 +1,62 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::issues::Issue;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub number: i64,
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct LockIssueParams {
/// Whether to lock (true) or unlock (false) the issue
pub locked: bool,
}
/// Lock or unlock an issue
///
/// Locks or unlocks conversation on an issue. When locked, only users with write access can comment.
/// Requires write access to the issue (author or workspace member).
///
/// Returns the updated issue with lock status.
#[utoipa::path(
put,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/lock",
tag = "Issues",
operation_id = "issueLock",
params(PathParams),
request_body(
content = LockIssueParams,
description = "Lock/unlock parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Issue lock status updated successfully.", body = ApiResponse<Issue>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to manage this issue", body = ApiErrorResponse),
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn lock(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<LockIssueParams>,
) -> Result<HttpResponse, AppError> {
let issue = service
.issue
.issue_lock(&session, &path.workspace_name, path.number, params.locked)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(issue)))
}
+185
View File
@@ -0,0 +1,185 @@
pub mod assign_issue;
pub mod assign_label;
pub mod close;
pub mod create;
pub mod create_comment;
pub mod create_label;
pub mod create_milestone;
pub mod delete;
pub mod delete_comment;
pub mod delete_label;
pub mod delete_milestone;
pub mod get;
pub mod list;
pub mod list_assignees;
pub mod list_comments;
pub mod list_events;
pub mod list_issue_labels;
pub mod list_labels;
pub mod list_milestones;
pub mod lock;
pub mod pr_relations;
pub mod reactions;
pub mod reopen;
pub mod repo_relations;
pub mod subscribers;
pub mod templates;
pub mod transfer;
pub mod unassign_issue;
pub mod unassign_label;
pub mod update;
pub mod update_comment;
pub mod update_label;
pub mod update_milestone;
use actix_web::web;
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/issues")
// Core
.route("", web::get().to(list::list))
.route("", web::post().to(create::create))
.route("/{number}", web::get().to(get::get))
.route("/{number}", web::put().to(update::update))
.route("/{number}", web::delete().to(delete::delete))
.route("/{number}/close", web::post().to(close::close))
.route("/{number}/reopen", web::post().to(reopen::reopen))
.route("/{number}/lock", web::put().to(lock::lock))
.route("/{number}/transfer", web::post().to(transfer::transfer))
// Comments
.route(
"/{number}/comments",
web::get().to(list_comments::list_comments),
)
.route(
"/{number}/comments",
web::post().to(create_comment::create_comment),
)
.route(
"/{number}/comments/{comment_id}",
web::put().to(update_comment::update_comment),
)
.route(
"/{number}/comments/{comment_id}",
web::delete().to(delete_comment::delete_comment),
)
// Labels (issue-level)
.route(
"/{number}/labels",
web::get().to(list_issue_labels::list_issue_labels),
)
.route(
"/{number}/labels/{label_id}",
web::post().to(assign_label::assign_label),
)
.route(
"/{number}/labels/{label_id}",
web::delete().to(unassign_label::unassign_label),
)
// Assignees
.route(
"/{number}/assignees",
web::get().to(list_assignees::list_assignees),
)
.route(
"/{number}/assignees/{user_id}",
web::post().to(assign_issue::assign_issue),
)
.route(
"/{number}/assignees/{user_id}",
web::delete().to(unassign_issue::unassign_issue),
)
// Events
.route("/{number}/events", web::get().to(list_events::list_events))
// Reactions
.route(
"/{number}/reactions",
web::get().to(reactions::list_reactions),
)
.route(
"/{number}/reactions",
web::post().to(reactions::add_reaction),
)
.route(
"/{number}/reactions/{reaction_id}",
web::delete().to(reactions::remove_reaction),
)
// Subscribers
.route(
"/{number}/subscribers",
web::get().to(subscribers::list_subscribers),
)
.route(
"/{number}/subscribe",
web::post().to(subscribers::subscribe),
)
.route(
"/{number}/subscribe",
web::delete().to(subscribers::unsubscribe),
)
.route("/{number}/mute", web::put().to(subscribers::mute))
// Repo relations
.route(
"/{number}/repos",
web::get().to(repo_relations::list_repo_relations),
)
.route("/{number}/repos", web::post().to(repo_relations::link_repo))
.route(
"/{number}/repos/{relation_id}",
web::delete().to(repo_relations::unlink_repo),
)
// PR relations
.route(
"/{number}/prs",
web::get().to(pr_relations::list_pr_relations),
)
.route("/{number}/prs", web::post().to(pr_relations::link_pr))
.route(
"/{number}/prs/{relation_id}",
web::delete().to(pr_relations::unlink_pr),
),
);
}
pub fn configure_repo_level(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/issues")
.route("/labels", web::get().to(list_labels::list_labels))
.route("/labels", web::post().to(create_label::create_label))
.route(
"/labels/{label_id}",
web::put().to(update_label::update_label),
)
.route(
"/labels/{label_id}",
web::delete().to(delete_label::delete_label),
)
.route(
"/milestones",
web::get().to(list_milestones::list_milestones),
)
.route(
"/milestones",
web::post().to(create_milestone::create_milestone),
)
.route(
"/milestones/{milestone_id}",
web::put().to(update_milestone::update_milestone),
)
.route(
"/milestones/{milestone_id}",
web::delete().to(delete_milestone::delete_milestone),
)
.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),
),
);
}
+170
View File
@@ -0,0 +1,170 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::issues::IssuePrRelation;
use crate::service::AppService;
use crate::service::issues::pr_relations::LinkPrParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
/// Maximum number of relations to return (default: 50, max: 100)
pub limit: Option<i64>,
/// Number of relations to skip for pagination (default: 0)
pub offset: Option<i64>,
}
/// List pull request relations for an issue
///
/// Returns a paginated list of all pull requests linked to the given issue.
/// Shows relation type (closes, references, depends_on, etc.) and link metadata.
/// Requires read access to the issue.
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/prs",
tag = "Issues",
operation_id = "issueListPrRelations",
params(PathParams, QueryParams),
responses(
(status = 200, description = "PR relations listed successfully. Returns array of PR relation objects.", body = ApiResponse<Vec<IssuePrRelation>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
(status = 404, description = "Issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_pr_relations(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let relations = service
.issue
.issue_pr_relations(
&session,
&path.workspace_name,
path.number,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(relations)))
}
/// Link a pull request to an issue
///
/// Creates a relation between the given issue and a pull request.
/// Commonly used to mark a PR as closing or referencing an issue.
/// Requires write access to the issue.
///
/// Parameters:
/// - pull_request_id: Pull request ID (UUID) to link
/// - relation_type: Relation type ("closes", "references", "depends_on", default: "references")
///
/// Returns the created relation.
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/prs",
tag = "Issues",
operation_id = "issueLinkPr",
params(PathParams),
request_body(
content = LinkPrParams,
description = "Link pull request parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Pull request linked successfully. Returns the created relation.", body = ApiResponse<IssuePrRelation>),
(status = 400, description = "Invalid parameters: invalid relation type", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
(status = 404, description = "Issue or pull request not found", body = ApiErrorResponse),
(status = 409, description = "Pull request is already linked to this issue", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn link_pr(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<LinkPrParams>,
) -> Result<HttpResponse, AppError> {
let relation = service
.issue
.issue_link_pr(
&session,
&path.workspace_name,
path.number,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(relation)))
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct RelationPathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
/// Relation ID (UUID)
pub relation_id: uuid::Uuid,
}
/// Unlink a pull request from an issue
///
/// Removes a pull request relation from the given issue.
/// Requires write access to the issue.
///
/// Returns success message on completion.
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/prs/{relation_id}",
tag = "Issues",
operation_id = "issueUnlinkPr",
params(RelationPathParams),
responses(
(status = 200, description = "Pull request unlinked successfully.", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
(status = 404, description = "PR relation not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn unlink_pr(
service: web::Data<AppService>,
session: Session,
path: web::Path<RelationPathParams>,
) -> Result<HttpResponse, AppError> {
service
.issue
.issue_unlink_pr(
&session,
&path.workspace_name,
path.number,
path.relation_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("PR unlinked".to_string())))
}
+169
View File
@@ -0,0 +1,169 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::issues::IssueReaction;
use crate::service::AppService;
use crate::service::issues::reactions::CreateIssueReactionParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
/// Maximum number of reactions to return (default: 50, max: 100)
pub limit: Option<i64>,
/// Number of reactions to skip for pagination (default: 0)
pub offset: Option<i64>,
}
/// List reactions on an issue
///
/// Returns a paginated list of all emoji reactions on the given issue.
/// Includes reaction content, target type, and user who added each reaction.
/// Requires read access to the issue.
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/reactions",
tag = "Issues",
operation_id = "issueListReactions",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Reactions listed successfully. Returns array of reaction objects.", body = ApiResponse<Vec<IssueReaction>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
(status = 404, description = "Issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_reactions(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let reactions = service
.issue
.issue_reactions(
&session,
&path.workspace_name,
path.number,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(reactions)))
}
/// Add a reaction to an issue
///
/// Adds an emoji reaction to the given issue.
/// Requires read access to the issue.
///
/// Parameters:
/// - content: Reaction content (e.g., "👍", "❤️", "🎉")
/// - target_type: Target type for the reaction (defaults to "Issue")
/// - target_id: Target ID for reactions on specific comments (optional)
///
/// Returns the created reaction.
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/reactions",
tag = "Issues",
operation_id = "issueAddReaction",
params(PathParams),
request_body(
content = CreateIssueReactionParams,
description = "Reaction creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Reaction added successfully. Returns the created reaction.", body = ApiResponse<IssueReaction>),
(status = 400, description = "Invalid parameters: empty content or invalid target type", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
(status = 404, description = "Issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn add_reaction(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<CreateIssueReactionParams>,
) -> Result<HttpResponse, AppError> {
let reaction = service
.issue
.issue_add_reaction(
&session,
&path.workspace_name,
path.number,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(reaction)))
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct ReactionPathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
/// Reaction ID (UUID)
pub reaction_id: uuid::Uuid,
}
/// Remove a reaction from an issue
///
/// Removes a previously added reaction. Only the user who added the reaction can remove it.
/// Requires read access to the issue.
///
/// Returns success message on completion.
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/reactions/{reaction_id}",
tag = "Issues",
operation_id = "issueRemoveReaction",
params(ReactionPathParams),
responses(
(status = 200, description = "Reaction removed successfully.", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Cannot remove another user's reaction", body = ApiErrorResponse),
(status = 404, description = "Reaction not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn remove_reaction(
service: web::Data<AppService>,
session: Session,
path: web::Path<ReactionPathParams>,
) -> Result<HttpResponse, AppError> {
service
.issue
.issue_remove_reaction(
&session,
&path.workspace_name,
path.number,
path.reaction_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Reaction removed".to_string())))
}
+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::issues::Issue;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub number: i64,
}
/// Reopen an issue
///
/// Reopens a closed issue. The issue state changes back to "open" and closed metadata is cleared.
/// Requires write access to the issue (author or workspace member).
///
/// Returns the reopened issue with updated metadata.
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/reopen",
tag = "Issues",
operation_id = "issueReopen",
params(PathParams),
responses(
(status = 200, description = "Issue reopened successfully. Returns the reopened issue with updated metadata.", body = ApiResponse<Issue>),
(status = 400, description = "Issue is not closed", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to reopen this issue", body = ApiErrorResponse),
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn reopen(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let issue = service
.issue
.issue_reopen(&session, &path.workspace_name, path.number)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(issue)))
}
+169
View File
@@ -0,0 +1,169 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::issues::IssueRepoRelation;
use crate::service::AppService;
use crate::service::issues::repo_relations::LinkRepoParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
/// Maximum number of relations to return (default: 50, max: 100)
pub limit: Option<i64>,
/// Number of relations to skip for pagination (default: 0)
pub offset: Option<i64>,
}
/// List repository relations for an issue
///
/// Returns a paginated list of all repositories linked to the given issue.
/// Shows relation type (references, duplicates, blocks, etc.) and link metadata.
/// Requires read access to the issue.
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/repos",
tag = "Issues",
operation_id = "issueListRepoRelations",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Repository relations listed successfully. Returns array of relation objects.", body = ApiResponse<Vec<IssueRepoRelation>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
(status = 404, description = "Issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_repo_relations(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let relations = service
.issue
.issue_repo_relations(
&session,
&path.workspace_name,
path.number,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(relations)))
}
/// Link a repository to an issue
///
/// Creates a relation between the given issue and a repository.
/// Requires write access to the issue.
///
/// Parameters:
/// - repo_id: Repository ID (UUID) to link
/// - relation_type: Relation type ("references", "duplicates", "blocks", "depends_on", default: "references")
///
/// Returns the created relation.
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/repos",
tag = "Issues",
operation_id = "issueLinkRepo",
params(PathParams),
request_body(
content = LinkRepoParams,
description = "Link repository parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Repository linked successfully. Returns the created relation.", body = ApiResponse<IssueRepoRelation>),
(status = 400, description = "Invalid parameters: invalid relation type", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
(status = 404, description = "Issue or repository not found", body = ApiErrorResponse),
(status = 409, description = "Repository is already linked to this issue", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn link_repo(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<LinkRepoParams>,
) -> Result<HttpResponse, AppError> {
let relation = service
.issue
.issue_link_repo(
&session,
&path.workspace_name,
path.number,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(relation)))
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct RelationPathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
/// Relation ID (UUID)
pub relation_id: uuid::Uuid,
}
/// Unlink a repository from an issue
///
/// Removes a repository relation from the given issue.
/// Requires write access to the issue.
///
/// Returns success message on completion.
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/repos/{relation_id}",
tag = "Issues",
operation_id = "issueUnlinkRepo",
params(RelationPathParams),
responses(
(status = 200, description = "Repository unlinked successfully.", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
(status = 404, description = "Repository relation not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn unlink_repo(
service: web::Data<AppService>,
session: Session,
path: web::Path<RelationPathParams>,
) -> Result<HttpResponse, AppError> {
service
.issue
.issue_unlink_repo(
&session,
&path.workspace_name,
path.number,
path.relation_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Repo unlinked".to_string())))
}
+186
View File
@@ -0,0 +1,186 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::issues::IssueSubscriber;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
/// Maximum number of subscribers to return (default: 50, max: 100)
pub limit: Option<i64>,
/// Number of subscribers to skip for pagination (default: 0)
pub offset: Option<i64>,
}
/// List subscribers of an issue
///
/// Returns a paginated list of all users subscribed to the given issue.
/// Shows who receives notifications and their subscription reason (author, assignee, manual).
/// Requires read access to the issue.
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/subscribers",
tag = "Issues",
operation_id = "issueListSubscribers",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Subscribers listed successfully. Returns array of subscriber objects.", body = ApiResponse<Vec<IssueSubscriber>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
(status = 404, description = "Issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_subscribers(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let subscribers = service
.issue
.issue_subscribers(
&session,
&path.workspace_name,
path.number,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(subscribers)))
}
/// Subscribe to an issue
///
/// Subscribes the authenticated user to the given issue to receive notifications.
/// Requires read access to the issue.
///
/// Effects:
/// - User is added as a subscriber with "manual" reason
/// - User receives notifications for all issue activity
///
/// Returns the created subscription record.
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/subscribe",
tag = "Issues",
operation_id = "issueSubscribe",
params(PathParams),
responses(
(status = 200, description = "Subscribed successfully. Returns the subscription record.", body = ApiResponse<IssueSubscriber>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
(status = 404, description = "Issue not found", body = ApiErrorResponse),
(status = 409, description = "Already subscribed to this issue", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn subscribe(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let sub = service
.issue
.issue_subscribe(&session, &path.workspace_name, path.number)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(sub)))
}
/// Unsubscribe from an issue
///
/// Removes the authenticated user's subscription to the given issue.
/// Stops all notifications for this issue.
///
/// Returns success message on completion.
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/subscribe",
tag = "Issues",
operation_id = "issueUnsubscribe",
params(PathParams),
responses(
(status = 200, description = "Unsubscribed successfully.", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Not currently subscribed to this issue", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn unsubscribe(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.issue
.issue_unsubscribe(&session, &path.workspace_name, path.number)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Unsubscribed".to_string())))
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct MuteIssueParams {
/// Whether to mute (true) or unmute (false) notifications
pub muted: bool,
}
/// Mute or unmute issue notifications
///
/// Mutes or unmutes notifications for the given issue without unsubscribing.
/// Requires an active subscription to the issue.
///
/// Returns success message on completion.
#[utoipa::path(
put,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/mute",
tag = "Issues",
operation_id = "issueMute",
params(PathParams),
request_body(
content = MuteIssueParams,
description = "Mute/unmute parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Mute status updated successfully.", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Not currently subscribed to this issue", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn mute(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<MuteIssueParams>,
) -> Result<HttpResponse, AppError> {
service
.issue
.issue_mute(&session, &path.workspace_name, path.number, params.muted)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Mute status updated".to_string())))
}
+220
View File
@@ -0,0 +1,220 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::issues::IssueTemplate;
use crate::service::AppService;
use crate::service::issues::templates::{CreateTemplateParams, UpdateTemplateParams};
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Repository name (unique within the workspace)
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
/// Maximum number of templates to return (default: 50, max: 100)
pub limit: Option<i64>,
/// Number of templates to skip for pagination (default: 0)
pub offset: Option<i64>,
}
/// List issue templates in a repository
///
/// Returns a paginated list of all active issue templates in the repository.
/// Templates provide pre-filled content for creating new issues.
/// Sorted alphabetically by name.
/// Requires read access to the repository.
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/templates",
tag = "Issues",
operation_id = "issueListTemplates",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Templates listed successfully. Returns array of template objects.", body = ApiResponse<Vec<IssueTemplate>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to access this repository", body = ApiErrorResponse),
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", 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 templates = service
.issue
.issue_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(templates)))
}
/// Create an issue template
///
/// Creates a new issue template in the repository.
/// Requires at least Member role in the repository.
///
/// Parameters:
/// - name: Template name (required)
/// - description: Template description (optional)
/// - title_template: Default title for issues (optional, supports placeholders)
/// - body_template: Default body content in markdown (required)
/// - labels: List of label names to auto-apply (optional)
///
/// Returns the created template with metadata.
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/templates",
tag = "Issues",
operation_id = "issueCreateTemplate",
params(PathParams),
request_body(
content = CreateTemplateParams,
description = "Template creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Template created successfully. Returns the newly created template.", body = ApiResponse<IssueTemplate>),
(status = 400, description = "Invalid parameters: empty name or body template", 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 = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn create_template(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<CreateTemplateParams>,
) -> Result<HttpResponse, AppError> {
let template = service
.issue
.issue_create_template(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(template)))
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct TemplatePathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Repository name (unique within the workspace)
pub repo_name: String,
/// Template ID (UUID)
pub template_id: uuid::Uuid,
}
/// Update an issue template
///
/// Updates an existing issue template's metadata and content.
/// Requires Admin role in the repository.
///
/// All fields are optional; only provided fields are updated.
/// Returns the updated template.
#[utoipa::path(
put,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/templates/{template_id}",
tag = "Issues",
operation_id = "issueUpdateTemplate",
params(TemplatePathParams),
request_body(
content = UpdateTemplateParams,
description = "Template update parameters (all fields optional)",
content_type = "application/json"
),
responses(
(status = 200, description = "Template updated successfully. Returns the updated template.", body = ApiResponse<IssueTemplate>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions (requires Admin role)", body = ApiErrorResponse),
(status = 404, description = "Repository, workspace, or 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<TemplatePathParams>,
params: web::Json<UpdateTemplateParams>,
) -> Result<HttpResponse, AppError> {
let template = service
.issue
.issue_update_template(
&session,
&path.workspace_name,
&path.repo_name,
path.template_id,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(template)))
}
/// Delete an issue template
///
/// Permanently removes an issue template from the repository.
/// Requires Admin role in the repository.
///
/// Returns success message on completion.
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/templates/{template_id}",
tag = "Issues",
operation_id = "issueDeleteTemplate",
params(TemplatePathParams),
responses(
(status = 200, description = "Template deleted successfully.", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions (requires Admin role)", 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<TemplatePathParams>,
) -> Result<HttpResponse, AppError> {
service
.issue
.issue_delete_template(
&session,
&path.workspace_name,
&path.repo_name,
path.template_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Template deleted".to_string())))
}
+75
View File
@@ -0,0 +1,75 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::issues::Issue;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct TransferIssueParams {
/// Target workspace name to transfer the issue to
pub target_workspace_name: String,
}
/// Transfer an issue to another workspace
///
/// Moves an issue from the current workspace to a different workspace.
/// Requires Admin role in both the source and target workspaces.
///
/// Effects:
/// - Issue is transferred to the target workspace with a new number
/// - Source workspace issue count is decremented
/// - Target workspace issue count is incremented
///
/// Returns the transferred issue with updated workspace and number.
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/transfer",
tag = "Issues",
operation_id = "issueTransfer",
params(PathParams),
request_body(
content = TransferIssueParams,
description = "Transfer parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Issue transferred successfully. Returns the issue with new workspace assignment.", body = ApiResponse<Issue>),
(status = 400, description = "Invalid target workspace", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions in source or target workspace", body = ApiErrorResponse),
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn transfer(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<TransferIssueParams>,
) -> Result<HttpResponse, AppError> {
let issue = service
.issue
.issue_transfer(
&session,
&path.workspace_name,
path.number,
&params.target_workspace_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(issue)))
}
+57
View File
@@ -0,0 +1,57 @@
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 {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
/// User ID (UUID) to unassign
pub user_id: uuid::Uuid,
}
/// Unassign a user from an issue
///
/// Removes a user from the issue's assignee list.
/// Requires write access to the issue (author or workspace member).
///
/// Effects:
/// - User is removed from the issue's assignees
/// - Issue assignee count is decremented
///
/// Returns success message on completion.
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/assignees/{user_id}",
tag = "Issues",
operation_id = "issueUnassign",
params(PathParams),
responses(
(status = 200, description = "User unassigned successfully.", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
(status = 404, description = "User is not assigned to this issue or not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn unassign_issue(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.issue
.issue_unassign(&session, &path.workspace_name, path.number, path.user_id)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("User unassigned".to_string())))
}
+57
View File
@@ -0,0 +1,57 @@
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 {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
/// Label ID (UUID) to unassign
pub label_id: uuid::Uuid,
}
/// Unassign a label from an issue
///
/// Removes a label from the given issue.
/// Requires write access to the issue (author or workspace member).
///
/// Effects:
/// - Label relation is removed from the issue
/// - Issue label count is decremented
///
/// Returns success message on completion.
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/labels/{label_id}",
tag = "Issues",
operation_id = "issueUnassignLabel",
params(PathParams),
responses(
(status = 200, description = "Label unassigned successfully.", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
(status = 404, description = "Label is not assigned to this issue or not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn unassign_label(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.issue
.issue_unassign_label(&session, &path.workspace_name, path.number, path.label_id)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Label unassigned".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::issues::Issue;
use crate::service::AppService;
use crate::service::issues::core::UpdateIssueParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Issue number (unique within the workspace)
pub number: i64,
}
/// Update an issue
///
/// Updates an existing issue's metadata such as title, body, priority, visibility, due date, and milestone.
/// Requires write access to the issue (author or workspace member).
///
/// All fields are optional; only provided fields are updated.
/// Returns the updated issue with full metadata.
#[utoipa::path(
put,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}",
tag = "Issues",
operation_id = "issueUpdate",
params(PathParams),
request_body(
content = UpdateIssueParams,
description = "Issue update parameters (all fields optional)",
content_type = "application/json"
),
responses(
(status = 200, description = "Issue updated successfully. Returns the updated issue with full metadata.", body = ApiResponse<Issue>),
(status = 400, description = "Invalid parameters: invalid priority, visibility, or milestone", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
(status = 404, description = "Workspace, issue, or referenced resource not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn update(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<UpdateIssueParams>,
) -> Result<HttpResponse, AppError> {
let issue = service
.issue
.issue_update(
&session,
&path.workspace_name,
path.number,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(issue)))
}
+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::models::issues::IssueComment;
use crate::service::AppService;
use crate::service::issues::comments::UpdateCommentParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub number: i64,
pub comment_id: uuid::Uuid,
}
/// Update an issue comment
///
/// Updates the body of an existing comment. Only the comment author can update their own comments.
/// Requires read access to the issue.
///
/// Returns the updated comment with edit timestamp.
#[utoipa::path(
put,
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/comments/{comment_id}",
tag = "Issues",
operation_id = "issueUpdateComment",
params(PathParams),
request_body(content = UpdateCommentParams, description = "Comment update parameters", content_type = "application/json"),
responses(
(status = 200, description = "Comment updated successfully.", body = ApiResponse<IssueComment>),
(status = 400, description = "Invalid parameters: empty body", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Cannot edit other users' comments", body = ApiErrorResponse),
(status = 404, description = "Workspace, issue, or comment not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn update_comment(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<UpdateCommentParams>,
) -> Result<HttpResponse, AppError> {
let comment = service
.issue
.issue_update_comment(
&session,
&path.workspace_name,
path.number,
path.comment_id,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(comment)))
}
+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::models::issues::IssueLabel;
use crate::service::AppService;
use crate::service::issues::labels::UpdateLabelParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub label_id: uuid::Uuid,
}
/// Update a label
///
/// Updates an existing issue label's name, color, or description.
/// Requires Admin role in the repository.
///
/// All fields are optional; only provided fields are updated.
/// Returns the updated label.
#[utoipa::path(
put,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/labels/{label_id}",
tag = "Issues",
operation_id = "issueUpdateLabel",
params(PathParams),
request_body(content = UpdateLabelParams, description = "Label update parameters (all fields optional)", content_type = "application/json"),
responses(
(status = 200, description = "Label updated successfully.", body = ApiResponse<IssueLabel>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "Insufficient permissions (requires Admin role)", body = ApiErrorResponse),
(status = 404, description = "Repository, workspace, or label not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn update_label(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<UpdateLabelParams>,
) -> Result<HttpResponse, AppError> {
let label = service
.issue
.issue_update_label(
&session,
&path.workspace_name,
&path.repo_name,
path.label_id,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(label)))
}
+75
View File
@@ -0,0 +1,75 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::issues::IssueMilestone;
use crate::service::AppService;
use crate::service::issues::milestones::UpdateMilestoneParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
/// Workspace name (unique identifier)
pub workspace_name: String,
/// Repository name (unique within the workspace)
pub repo_name: String,
/// Milestone ID (UUID)
pub milestone_id: uuid::Uuid,
}
/// Update a milestone
///
/// Updates an existing milestone's metadata. Can also close or reopen the milestone via the state field.
/// Requires at least Member role in the repository.
///
/// Updatable fields:
/// - title: Milestone title (optional)
/// - description: Description (optional)
/// - due_at: Target due date (optional)
/// - state: State ("open" or "closed") for closing/reopening the milestone (optional)
///
/// All fields are optional; only provided fields are updated.
/// Returns the updated milestone with full metadata.
#[utoipa::path(
put,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/milestones/{milestone_id}",
tag = "Issues",
operation_id = "issueUpdateMilestone",
params(PathParams),
request_body(
content = UpdateMilestoneParams,
description = "Milestone update parameters (all fields optional)",
content_type = "application/json"
),
responses(
(status = 200, description = "Milestone updated successfully. Returns the updated milestone with full metadata.", body = ApiResponse<IssueMilestone>),
(status = 400, description = "Invalid parameters", 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 = 404, description = "Repository, workspace, or milestone not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn update_milestone(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<UpdateMilestoneParams>,
) -> Result<HttpResponse, AppError> {
let milestone = service
.issue
.issue_update_milestone(
&session,
&path.workspace_name,
&path.repo_name,
path.milestone_id,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(milestone)))
}
+2
View File
@@ -1,6 +1,8 @@
pub mod auth;
pub mod issue;
pub mod openapi;
pub mod repo;
pub mod response;
pub mod routes;
pub mod user;
pub mod workspace;
+217 -8
View File
@@ -4,18 +4,28 @@ use crate::api::auth::regenerate_2fa_backup_codes::{
Regenerate2FABackupCodesRequest, Regenerate2FABackupCodesResponse,
};
use crate::api::auth::register::RegisterResponse;
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse, ApiResponse};
use crate::api::issue::lock::LockIssueParams;
use crate::api::issue::subscribers::MuteIssueParams;
use crate::api::issue::transfer::TransferIssueParams;
use crate::api::repo::accept_invitation::AcceptInvitationParams;
use crate::api::repo::set_branch_protection::SetBranchProtectionParams;
use crate::api::repo::transfer_owner::TransferOwnerParams;
use crate::service::repo::watches::WatchParams;
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse, ApiResponse};
use crate::api::workspace::accept_invitation::AcceptInvitationRequest;
use crate::api::workspace::review_approval::ReviewApprovalRequest;
use crate::api::workspace::transfer_owner::TransferOwnerRequest;
use crate::models::issues::{
Issue, IssueAssignee, IssueComment, IssueEvent, IssueLabel, IssueLabelRelation, IssueMilestone,
IssuePrRelation, IssueReaction, IssueRepoRelation, IssueSubscriber, IssueTemplate,
};
use crate::models::repos::{
BranchProtectionRule, Repo, RepoBranch, RepoCommitComment,
RepoCommitStatus, RepoDeployKey, RepoFork, RepoInvitation, RepoMember, RepoRelease,
RepoStar, RepoStats, RepoTag, RepoWatch, RepoWebhook,
BranchProtectionRule, Repo, RepoBranch, RepoCommitComment, RepoCommitStatus, RepoDeployKey,
RepoFork, RepoInvitation, RepoMember, RepoRelease, RepoStar, RepoStats, RepoTag, RepoWatch,
RepoWebhook,
};
use crate::models::users::{
User, UserAppearance, UserDevice, UserGpgKey, UserNotifySetting, UserProfile, UserSecurityLog,
UserSshKey,
};
use crate::models::workspaces::{
Workspace, WorkspaceAuditLog, WorkspaceBilling, WorkspaceCustomBranding, WorkspaceDomain,
@@ -34,6 +44,14 @@ use crate::service::auth::rsa::RsaResponse;
use crate::service::auth::totp::{
Disable2FAParams, Enable2FAResponse, Get2FAStatusResponse, Verify2FAParams,
};
use crate::service::issues::comments::{CreateCommentParams, UpdateCommentParams};
use crate::service::issues::core::{CreateIssueParams, IssueListFilters, UpdateIssueParams};
use crate::service::issues::labels::{CreateLabelParams, UpdateLabelParams};
use crate::service::issues::milestones::{CreateMilestoneParams, UpdateMilestoneParams};
use crate::service::issues::pr_relations::LinkPrParams;
use crate::service::issues::reactions::CreateIssueReactionParams;
use crate::service::issues::repo_relations::LinkRepoParams;
use crate::service::issues::templates::{CreateTemplateParams, UpdateTemplateParams};
use crate::service::repo::branches::CreateBranchParams;
use crate::service::repo::commit_status::{CreateCommitCommentParams, CreateCommitStatusParams};
use crate::service::repo::core::{CreateRepoParams, UpdateRepoParams};
@@ -46,6 +64,18 @@ use crate::service::repo::protection::{
};
use crate::service::repo::releases::{CreateReleaseParams, UpdateReleaseParams};
use crate::service::repo::tags::CreateTagParams;
use crate::service::repo::watches::WatchParams;
use crate::service::repo::webhooks::{
CreateWebhookParams as RepoCreateWebhookParams, UpdateWebhookParams as RepoUpdateWebhookParams,
};
use crate::service::user::account::{
UpdateUserAccountParams, UploadUserAvatarParams, UserAvatarResponse,
};
use crate::service::user::appearance::UpdateUserAppearanceParams;
use crate::service::user::keys::{AddGpgKeyParams, AddSshKeyParams};
use crate::service::user::notify::UpdateUserNotifySettingParams;
use crate::service::user::profile::UpdateUserProfileParams;
use crate::service::user::security::{UserOAuthInfo, UserPersonalAccessTokenInfo, UserSessionInfo};
use crate::service::workspace::approvals::RequestApprovalParams;
use crate::service::workspace::billing::UpdateBillingParams;
use crate::service::workspace::branding::UpdateBrandingParams;
@@ -66,8 +96,10 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
),
tags(
(name = "Auth", description = "Authentication, registration, session and email security endpoints."),
(name = "User", description = "User account management, profile, appearance, notification settings, SSH/GPG keys, sessions, devices, OAuth accounts, security logs, and personal access tokens."),
(name = "Workspaces", description = "Workspace CRUD, archiving, ownership transfer, and avatar management."),
(name = "Repos", description = "Repository management including branches, tags, releases, forks, stars, watches, members, invitations, deploy keys, webhooks, protection rules, commit statuses, and statistics."),
(name = "Issues", description = "Issue tracking, comments, labels, milestones, assignees, events, reactions, subscribers, templates, and cross-references with repos and pull requests."),
),
paths(
// Auth
@@ -88,6 +120,89 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
crate::api::auth::verify_2fa::handle,
crate::api::auth::disable_2fa::handle,
crate::api::auth::regenerate_2fa_backup_codes::handle,
// User
crate::api::user::get_account::get_account,
crate::api::user::update_account::update_account,
crate::api::user::upload_avatar::upload_avatar,
crate::api::user::delete_account::delete_account,
crate::api::user::get_appearance::get_appearance,
crate::api::user::update_appearance::update_appearance,
crate::api::user::get_profile::get_profile,
crate::api::user::update_profile::update_profile,
crate::api::user::get_notifications::get_notifications,
crate::api::user::update_notifications::update_notifications,
crate::api::user::list_ssh_keys::list_ssh_keys,
crate::api::user::add_ssh_key::add_ssh_key,
crate::api::user::delete_ssh_key::delete_ssh_key,
crate::api::user::list_gpg_keys::list_gpg_keys,
crate::api::user::add_gpg_key::add_gpg_key,
crate::api::user::delete_gpg_key::delete_gpg_key,
crate::api::user::list_sessions::list_sessions,
crate::api::user::revoke_session::revoke_session,
crate::api::user::list_devices::list_devices,
crate::api::user::delete_device::delete_device,
crate::api::user::list_oauth_accounts::list_oauth_accounts,
crate::api::user::unlink_oauth::unlink_oauth,
crate::api::user::list_security_logs::list_security_logs,
crate::api::user::list_personal_access_tokens::list_tokens,
crate::api::user::revoke_personal_access_token::revoke_token,
// Issues - Core
crate::api::issue::list::list,
crate::api::issue::get::get,
crate::api::issue::create::create,
crate::api::issue::update::update,
crate::api::issue::close::close,
crate::api::issue::reopen::reopen,
crate::api::issue::delete::delete,
crate::api::issue::lock::lock,
crate::api::issue::transfer::transfer,
// Issues - Comments
crate::api::issue::list_comments::list_comments,
crate::api::issue::create_comment::create_comment,
crate::api::issue::update_comment::update_comment,
crate::api::issue::delete_comment::delete_comment,
// Issues - Labels (repo-level)
crate::api::issue::list_labels::list_labels,
crate::api::issue::create_label::create_label,
crate::api::issue::update_label::update_label,
crate::api::issue::delete_label::delete_label,
// Issues - Label relations (issue-level)
crate::api::issue::list_issue_labels::list_issue_labels,
crate::api::issue::assign_label::assign_label,
crate::api::issue::unassign_label::unassign_label,
// Issues - Milestones (repo-level)
crate::api::issue::list_milestones::list_milestones,
crate::api::issue::create_milestone::create_milestone,
crate::api::issue::update_milestone::update_milestone,
crate::api::issue::delete_milestone::delete_milestone,
// Issues - Assignees
crate::api::issue::list_assignees::list_assignees,
crate::api::issue::assign_issue::assign_issue,
crate::api::issue::unassign_issue::unassign_issue,
// Issues - Events
crate::api::issue::list_events::list_events,
// Issues - Reactions
crate::api::issue::reactions::list_reactions,
crate::api::issue::reactions::add_reaction,
crate::api::issue::reactions::remove_reaction,
// Issues - Subscribers
crate::api::issue::subscribers::list_subscribers,
crate::api::issue::subscribers::subscribe,
crate::api::issue::subscribers::unsubscribe,
crate::api::issue::subscribers::mute,
// Issues - Templates (repo-level)
crate::api::issue::templates::list_templates,
crate::api::issue::templates::create_template,
crate::api::issue::templates::update_template,
crate::api::issue::templates::delete_template,
// Issues - Repo relations
crate::api::issue::repo_relations::list_repo_relations,
crate::api::issue::repo_relations::link_repo,
crate::api::issue::repo_relations::unlink_repo,
// Issues - PR relations
crate::api::issue::pr_relations::list_pr_relations,
crate::api::issue::pr_relations::link_pr,
crate::api::issue::pr_relations::unlink_pr,
// Workspaces
crate::api::workspace::list::handle,
crate::api::workspace::get::handle,
@@ -225,6 +340,101 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
Disable2FAParams,
Regenerate2FABackupCodesRequest,
Regenerate2FABackupCodesResponse,
// User
ApiResponse<User>,
ApiResponse<UserAvatarResponse>,
ApiResponse<String>,
User,
UpdateUserAccountParams,
UploadUserAvatarParams,
UserAvatarResponse,
ApiResponse<UserAppearance>,
UserAppearance,
UpdateUserAppearanceParams,
ApiResponse<UserProfile>,
UserProfile,
UpdateUserProfileParams,
ApiResponse<UserNotifySetting>,
UserNotifySetting,
UpdateUserNotifySettingParams,
ApiResponse<UserSshKey>,
ApiResponse<Vec<UserSshKey>>,
UserSshKey,
AddSshKeyParams,
ApiResponse<UserGpgKey>,
ApiResponse<Vec<UserGpgKey>>,
UserGpgKey,
AddGpgKeyParams,
ApiResponse<UserSessionInfo>,
ApiResponse<Vec<UserSessionInfo>>,
UserSessionInfo,
ApiResponse<UserDevice>,
ApiResponse<Vec<UserDevice>>,
UserDevice,
ApiResponse<UserOAuthInfo>,
ApiResponse<Vec<UserOAuthInfo>>,
UserOAuthInfo,
ApiResponse<UserSecurityLog>,
ApiResponse<Vec<UserSecurityLog>>,
UserSecurityLog,
ApiResponse<UserPersonalAccessTokenInfo>,
ApiResponse<Vec<UserPersonalAccessTokenInfo>>,
UserPersonalAccessTokenInfo,
// Issues
ApiResponse<Issue>,
ApiResponse<Vec<Issue>>,
Issue,
CreateIssueParams,
IssueListFilters,
UpdateIssueParams,
LockIssueParams,
TransferIssueParams,
ApiResponse<IssueComment>,
ApiResponse<Vec<IssueComment>>,
IssueComment,
CreateCommentParams,
UpdateCommentParams,
ApiResponse<IssueLabel>,
ApiResponse<Vec<IssueLabel>>,
IssueLabel,
CreateLabelParams,
UpdateLabelParams,
ApiResponse<IssueLabelRelation>,
ApiResponse<Vec<IssueLabelRelation>>,
IssueLabelRelation,
ApiResponse<IssueMilestone>,
ApiResponse<Vec<IssueMilestone>>,
IssueMilestone,
CreateMilestoneParams,
UpdateMilestoneParams,
ApiResponse<IssueAssignee>,
ApiResponse<Vec<IssueAssignee>>,
IssueAssignee,
ApiResponse<IssueEvent>,
ApiResponse<Vec<IssueEvent>>,
IssueEvent,
ApiResponse<IssueReaction>,
ApiResponse<Vec<IssueReaction>>,
IssueReaction,
CreateIssueReactionParams,
ApiResponse<IssueSubscriber>,
ApiResponse<Vec<IssueSubscriber>>,
IssueSubscriber,
MuteIssueParams,
ApiResponse<IssueTemplate>,
ApiResponse<Vec<IssueTemplate>>,
IssueTemplate,
CreateTemplateParams,
UpdateTemplateParams,
ApiResponse<IssueRepoRelation>,
ApiResponse<Vec<IssueRepoRelation>>,
IssueRepoRelation,
LinkRepoParams,
ApiResponse<IssuePrRelation>,
ApiResponse<Vec<IssuePrRelation>>,
IssuePrRelation,
LinkPrParams,
// Workspaces
ApiResponse<Workspace>,
ApiResponse<Vec<Workspace>>,
ApiResponse<WorkspaceMember>,
@@ -307,7 +517,6 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
ApiResponse<RepoCommitComment>,
ApiResponse<Vec<RepoCommitComment>>,
ApiResponse<RepoStats>,
ApiResponse<String>,
Repo,
CreateRepoParams,
UpdateRepoParams,
@@ -334,8 +543,8 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
RepoDeployKey,
AddDeployKeyParams,
RepoWebhook,
CreateWebhookParams,
UpdateWebhookParams,
RepoCreateWebhookParams,
RepoUpdateWebhookParams,
BranchProtectionRule,
CreateProtectionRuleParams,
UpdateProtectionRuleParams,
+4 -4
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::ToSchema;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoInvitation;
use crate::service::AppService;
@@ -18,12 +18,12 @@ pub struct AcceptInvitationParams {
///
/// Accepts a pending repository invitation using the token received via email.
/// Requires authentication and a verified email address matching the invitation.
///
///
/// Effects:
/// - User is added as a repository member with the invited role
/// - User is added to the workspace if not already a member
/// - Invitation is marked as accepted
///
///
/// Returns the accepted invitation with full metadata.
#[utoipa::path(
post,
+12 -7
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoDeployKey;
use crate::service::repo::deploy_keys::AddDeployKeyParams;
use crate::service::AppService;
use crate::service::repo::deploy_keys::AddDeployKeyParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -21,17 +21,17 @@ pub struct PathParams {
///
/// Adds an SSH public key for automated deployments and CI/CD access to the repository.
/// Requires Admin role or higher in the repository.
///
///
/// Parameters:
/// - title: Human-readable name for the deploy key (1-100 characters)
/// - key: SSH public key in OpenSSH format (e.g., "ssh-rsa AAAA...")
/// - read_only: Whether the key has read-only access (default: true)
///
///
/// Effects:
/// - Deploy key is added to the repository
/// - Key can be used for Git operations (clone, fetch, push if not read-only)
/// - Key fingerprint is calculated and stored
///
///
/// Returns the created deploy key with metadata including fingerprint.
#[utoipa::path(
post,
@@ -65,7 +65,12 @@ pub async fn add_deploy_key(
) -> Result<HttpResponse, AppError> {
let key = service
.repo
.repo_add_deploy_key(&session, &path.workspace_name, &path.repo_name, params.into_inner())
.repo_add_deploy_key(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(key)))
+11 -6
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoMember;
use crate::service::repo::members::AddRepoMemberParams;
use crate::service::AppService;
use crate::service::repo::members::AddRepoMemberParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -21,12 +21,12 @@ pub struct PathParams {
///
/// Grants a user access to the repository with a specific role.
/// Requires Admin role or higher in the repository.
///
///
/// Requirements:
/// - User must exist in the system
/// - User must be a member of the workspace
/// - Role must be one of: "read", "write", "admin" (cannot assign "owner")
///
///
/// Returns the created member record with user information and role.
#[utoipa::path(
post,
@@ -60,7 +60,12 @@ pub async fn add_member(
) -> Result<HttpResponse, AppError> {
let member = service
.repo
.repo_add_member(&session, &path.workspace_name, &path.repo_name, params.into_inner())
.repo_add_member(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(member)))
+4 -4
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -19,13 +19,13 @@ pub struct PathParams {
///
/// Marks a repository as archived, making it read-only. All write operations (push, create issues, etc.) are disabled.
/// Requires Owner role in the repository.
///
///
/// Effects:
/// - Repository status changes to "archived"
/// - All write operations are blocked
/// - Repository remains visible based on its visibility setting
/// - Can be unarchived later by repository owners
///
///
/// Returns success message on completion.
#[utoipa::path(
post,
+3 -3
View File
@@ -1,11 +1,11 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::repo::protection::BranchMergeCheck;
use crate::service::AppService;
use crate::service::repo::protection::BranchMergeCheck;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
+6 -6
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::Repo;
use crate::service::repo::core::CreateRepoParams;
use crate::service::AppService;
use crate::service::repo::core::CreateRepoParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -18,17 +18,17 @@ pub struct PathParams {
/// Create a new repository
///
/// Creates a new Git repository within the specified workspace. The authenticated user becomes the repository owner.
///
///
/// Requirements:
/// - User must have at least Member role in the workspace
/// - Repository name must be unique within the workspace
/// - Name must be 1-100 characters, alphanumeric, hyphens, underscores, and dots allowed
///
///
/// Optional parameters:
/// - description: Repository description (max 500 characters)
/// - visibility: "public", "private", or "internal" (defaults to workspace setting)
/// - default_branch: Default branch name (defaults to "main")
///
///
/// Returns the created repository with full metadata.
#[utoipa::path(
post,
+11 -6
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoBranch;
use crate::service::repo::branches::CreateBranchParams;
use crate::service::AppService;
use crate::service::repo::branches::CreateBranchParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -21,11 +21,11 @@ pub struct PathParams {
///
/// Creates a new branch in the repository based on an existing commit or branch.
/// Requires Write role or higher in the repository.
///
///
/// Parameters:
/// - name: Branch name (1-100 characters, alphanumeric, hyphens, underscores, dots, slashes allowed)
/// - from: Source branch name or commit SHA to branch from (defaults to default branch)
///
///
/// Returns the created branch with metadata including the initial commit SHA.
#[utoipa::path(
post,
@@ -59,7 +59,12 @@ pub async fn create_branch(
) -> Result<HttpResponse, AppError> {
let branch = service
.repo
.repo_create_branch(&session, &path.workspace_name, &path.repo_name, params.into_inner())
.repo_create_branch(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(branch)))
+12 -7
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoCommitComment;
use crate::service::repo::commit_status::CreateCommitCommentParams;
use crate::service::AppService;
use crate::service::repo::commit_status::CreateCommitCommentParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -21,18 +21,18 @@ pub struct PathParams {
///
/// Creates a new comment on a specific commit. Comments can be general or inline (attached to a specific file and line).
/// Requires Write role or higher in the repository.
///
///
/// Parameters:
/// - commit_sha: Commit SHA to comment on (must exist in repository)
/// - body: Comment body in markdown format (1-10000 characters)
/// - path: File path for inline comments (optional)
/// - line: Line number for inline comments (optional, requires path)
///
///
/// Effects:
/// - Comment is attached to the commit
/// - Comment author receives notifications for replies
/// - Inline comments appear in code review interfaces
///
///
/// Returns the created comment with metadata including ID and timestamps.
#[utoipa::path(
post,
@@ -65,7 +65,12 @@ pub async fn create_commit_comment(
) -> Result<HttpResponse, AppError> {
let comment = service
.repo
.repo_create_commit_comment(&session, &path.workspace_name, &path.repo_name, params.into_inner())
.repo_create_commit_comment(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(comment)))
+12 -7
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoCommitStatus;
use crate::service::repo::commit_status::CreateCommitStatusParams;
use crate::service::AppService;
use crate::service::repo::commit_status::CreateCommitStatusParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -21,19 +21,19 @@ pub struct PathParams {
///
/// Creates a new status check for a specific commit, typically used by CI/CD systems.
/// Requires Write role or higher in the repository.
///
///
/// Parameters:
/// - commit_sha: Commit SHA to attach the status to (must exist in repository)
/// - state: Status state ("pending", "success", "failure", "error")
/// - context: Status context name (e.g., "ci/build", "ci/test") - must be unique per commit
/// - description: Human-readable description of the status (optional, max 500 characters)
/// - target_url: URL with detailed information about the status (optional)
///
///
/// Effects:
/// - Status is attached to the commit
/// - Can be used by branch protection rules to enforce status checks
/// - Multiple statuses can exist for the same commit with different contexts
///
///
/// Returns the created status with metadata including ID and timestamps.
#[utoipa::path(
post,
@@ -67,7 +67,12 @@ pub async fn create_commit_status(
) -> Result<HttpResponse, AppError> {
let status = service
.repo
.repo_create_commit_status(&session, &path.workspace_name, &path.repo_name, params.into_inner())
.repo_create_commit_status(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(status)))
+12 -7
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoInvitation;
use crate::service::repo::invitations::CreateRepoInvitationParams;
use crate::service::AppService;
use crate::service::repo::invitations::CreateRepoInvitationParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -21,16 +21,16 @@ pub struct PathParams {
///
/// Sends an invitation email to a user to join the repository with a specific role.
/// Requires Admin role or higher in the repository.
///
///
/// Parameters:
/// - email: Email address of the invitee (must be a valid email)
/// - role: Role to assign when invitation is accepted ("read", "write", "admin")
///
///
/// Effects:
/// - Invitation email is sent to the invitee
/// - Invitation expires after 7 days
/// - Invitee must have a verified email to accept
///
///
/// Returns the created invitation with metadata including expiration date.
#[utoipa::path(
post,
@@ -64,7 +64,12 @@ pub async fn create_invitation(
) -> Result<HttpResponse, AppError> {
let invitation = service
.repo
.repo_create_invitation(&session, &path.workspace_name, &path.repo_name, params.into_inner())
.repo_create_invitation(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(invitation)))
+11 -6
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::BranchProtectionRule;
use crate::service::repo::protection::CreateProtectionRuleParams;
use crate::service::AppService;
use crate::service::repo::protection::CreateProtectionRuleParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -21,7 +21,7 @@ pub struct PathParams {
///
/// Creates a new branch protection rule that enforces policies on matching branches.
/// Requires Admin role or higher in the repository.
///
///
/// Parameters:
/// - pattern: Branch name pattern (supports wildcards like "feature/*", "release/**")
/// - required_approvals: Number of required approvals before merging (0-10)
@@ -30,7 +30,7 @@ pub struct PathParams {
/// - restrict_pushes: Restrict who can push to matching branches
/// - allow_force_pushes: Allow force pushes (only if restrict_pushes is false)
/// - allow_deletions: Allow branch deletion (only if restrict_pushes is false)
///
///
/// Returns the created protection rule with full configuration.
#[utoipa::path(
post,
@@ -64,7 +64,12 @@ pub async fn create_protection_rule(
) -> Result<HttpResponse, AppError> {
let rule = service
.repo
.repo_create_protection_rule(&session, &path.workspace_name, &path.repo_name, params.into_inner())
.repo_create_protection_rule(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(rule)))
+11 -6
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoRelease;
use crate::service::repo::releases::CreateReleaseParams;
use crate::service::AppService;
use crate::service::repo::releases::CreateReleaseParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -21,7 +21,7 @@ pub struct PathParams {
///
/// Creates a new release in the repository, optionally creating a tag if it doesn't exist.
/// Requires Write role or higher in the repository.
///
///
/// Parameters:
/// - tag_name: Tag name for the release (will be created if it doesn't exist)
/// - name: Release name/title (required)
@@ -29,7 +29,7 @@ pub struct PathParams {
/// - draft: Whether this is a draft release (default: false)
/// - prerelease: Whether this is a prerelease (default: false)
/// - target_commitish: Commit SHA or branch name to tag (defaults to default branch)
///
///
/// Returns the created release with metadata including download URLs for assets.
#[utoipa::path(
post,
@@ -63,7 +63,12 @@ pub async fn create_release(
) -> Result<HttpResponse, AppError> {
let release = service
.repo
.repo_create_release(&session, &path.workspace_name, &path.repo_name, params.into_inner())
.repo_create_release(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(release)))
+11 -6
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoTag;
use crate::service::repo::tags::CreateTagParams;
use crate::service::AppService;
use crate::service::repo::tags::CreateTagParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -21,12 +21,12 @@ pub struct PathParams {
///
/// Creates a new tag in the repository pointing to a specific commit or branch.
/// Requires Write role or higher in the repository.
///
///
/// Parameters:
/// - name: Tag name (1-100 characters, typically follows semantic versioning like v1.0.0)
/// - target: Commit SHA or branch name to tag (defaults to HEAD of default branch)
/// - message: Optional tag message for annotated tags
///
///
/// Returns the created tag with metadata including the commit SHA.
#[utoipa::path(
post,
@@ -60,7 +60,12 @@ pub async fn create_tag(
) -> Result<HttpResponse, AppError> {
let tag = service
.repo
.repo_create_tag(&session, &path.workspace_name, &path.repo_name, params.into_inner())
.repo_create_tag(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(tag)))
+12 -7
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoWebhook;
use crate::service::repo::webhooks::CreateWebhookParams;
use crate::service::AppService;
use crate::service::repo::webhooks::CreateWebhookParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -21,17 +21,17 @@ pub struct PathParams {
///
/// Creates a new webhook that receives HTTP POST notifications for repository events.
/// Requires Admin role or higher in the repository.
///
///
/// Parameters:
/// - url: Webhook endpoint URL (must be HTTPS in production)
/// - events: List of events to subscribe to (e.g., "push", "pull_request", "issue")
/// - secret: Optional secret for webhook signature verification
/// - active: Whether the webhook is active (default: true)
///
///
/// Effects:
/// - Webhook is created and starts receiving events
/// - Webhook deliveries are logged and can be retried on failure
///
///
/// Returns the created webhook with metadata including ID and configuration.
#[utoipa::path(
post,
@@ -64,7 +64,12 @@ pub async fn create_webhook(
) -> Result<HttpResponse, AppError> {
let webhook = service
.repo
.repo_create_webhook(&session, &path.workspace_name, &path.repo_name, params.into_inner())
.repo_create_webhook(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(webhook)))
+4 -4
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -23,9 +23,9 @@ pub struct PathParams {
/// - Issues, pull requests, and comments
/// - Webhooks, deploy keys, and protection rules
/// - Stars, watches, and forks
///
///
/// Requires Owner role in the repository. This action is irreversible.
///
///
/// Returns success message on completion.
#[utoipa::path(
delete,
+10 -5
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -21,12 +21,12 @@ pub struct PathParams {
///
/// 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(
delete,
@@ -53,7 +53,12 @@ pub async fn delete_branch(
) -> Result<HttpResponse, AppError> {
service
.repo
.repo_delete_branch(&session, &path.workspace_name, &path.repo_name, path.branch_id)
.repo_delete_branch(
&session,
&path.workspace_name,
&path.repo_name,
path.branch_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Branch deleted successfully".to_string())))
+7 -5
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -21,12 +21,12 @@ pub struct PathParams {
///
/// Removes an SSH deploy key from the repository, revoking its access.
/// Requires Admin role or higher in the repository.
///
///
/// Effects:
/// - Deploy key is permanently removed from the repository
/// - Key can no longer be used for Git operations
/// - Automated systems using this key will lose access
///
///
/// Returns success message on completion.
#[utoipa::path(
delete,
@@ -55,5 +55,7 @@ pub async fn delete_deploy_key(
.repo_delete_deploy_key(&session, &path.workspace_name, &path.repo_name, path.key_id)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Deploy key deleted successfully".to_string())))
Ok(HttpResponse::Ok().json(ApiResponse::new(
"Deploy key deleted successfully".to_string(),
)))
}
+13 -6
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -21,12 +21,12 @@ pub struct PathParams {
///
/// Permanently removes a branch protection rule from the repository.
/// Requires Admin role or higher in the repository.
///
///
/// Effects:
/// - Protection rule is permanently removed
/// - Branches matching the pattern are no longer protected by this rule
/// - Pushes and merges to matching branches are no longer restricted
///
///
/// Returns success message on completion.
#[utoipa::path(
delete,
@@ -52,8 +52,15 @@ pub async fn delete_protection_rule(
) -> Result<HttpResponse, AppError> {
service
.repo
.repo_delete_protection_rule(&session, &path.workspace_name, &path.repo_name, path.rule_id)
.repo_delete_protection_rule(
&session,
&path.workspace_name,
&path.repo_name,
path.rule_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Protection rule deleted successfully".to_string())))
Ok(HttpResponse::Ok().json(ApiResponse::new(
"Protection rule deleted successfully".to_string(),
)))
}
+10 -5
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -21,13 +21,13 @@ pub struct PathParams {
///
/// Permanently deletes a release from the repository. The associated tag and commits are not deleted.
/// Requires Write role or higher in the repository.
///
///
/// Effects:
/// - Release metadata is permanently removed
/// - Release assets are deleted
/// - The associated tag remains in the repository
/// - The tagged commits remain in the repository history
///
///
/// Returns success message on completion.
#[utoipa::path(
delete,
@@ -53,7 +53,12 @@ pub async fn delete_release(
) -> Result<HttpResponse, AppError> {
service
.repo
.repo_delete_release(&session, &path.workspace_name, &path.repo_name, path.release_id)
.repo_delete_release(
&session,
&path.workspace_name,
&path.repo_name,
path.release_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Release deleted successfully".to_string())))
+4 -4
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -21,12 +21,12 @@ pub struct PathParams {
///
/// 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(
delete,
+10 -5
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -21,12 +21,12 @@ pub struct PathParams {
///
/// Permanently removes a webhook from the repository, stopping all event notifications.
/// Requires Admin role or higher in the repository.
///
///
/// Effects:
/// - Webhook is permanently removed from the repository
/// - Webhook stops receiving event notifications immediately
/// - Webhook delivery history is deleted
///
///
/// Returns success message on completion.
#[utoipa::path(
delete,
@@ -52,7 +52,12 @@ pub async fn delete_webhook(
) -> Result<HttpResponse, AppError> {
service
.repo
.repo_delete_webhook(&session, &path.workspace_name, &path.repo_name, path.webhook_id)
.repo_delete_webhook(
&session,
&path.workspace_name,
&path.repo_name,
path.webhook_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Webhook deleted successfully".to_string())))
+10 -5
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::Repo;
use crate::service::AppService;
@@ -22,13 +22,13 @@ use crate::service::repo::fork::ForkRepoParams;
///
/// Creates a copy of the repository in the specified workspace or the current user's workspace.
/// Requires read access to the source repository and write access to the target workspace.
///
///
/// Effects:
/// - Creates a new repository with all branches, tags, and commit history
/// - Establishes a parent-child relationship between source and fork
/// - Fork is initially set to private visibility
/// - Current user becomes the owner of the fork
///
///
/// Returns the created fork repository with full metadata.
#[utoipa::path(
post,
@@ -62,7 +62,12 @@ pub async fn fork_repo(
) -> Result<HttpResponse, AppError> {
let repo = service
.repo
.repo_fork(&session, &path.workspace_name, &path.repo_name, params.into_inner())
.repo_fork(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(repo)))
+2 -2
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::Repo;
use crate::service::AppService;
+9 -4
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::BranchProtectionRule;
use crate::service::AppService;
@@ -22,7 +22,7 @@ pub struct PathParams {
///
/// Returns detailed information about a specific branch protection rule.
/// Requires read access to the repository.
///
///
/// Returns the complete protection rule with all configuration details including:
/// - Branch name pattern
/// - Required approvals and status checks
@@ -52,7 +52,12 @@ pub async fn get_protection_rule(
) -> Result<HttpResponse, AppError> {
let rule = service
.repo
.repo_get_protection_rule(&session, &path.workspace_name, &path.repo_name, path.rule_id)
.repo_get_protection_rule(
&session,
&path.workspace_name,
&path.repo_name,
path.rule_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(rule)))
+3 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoStats;
use crate::service::AppService;
@@ -27,7 +27,7 @@ pub struct PathParams {
/// - Open issues and pull requests count
/// - Storage size and bandwidth usage
/// - Last push timestamp
///
///
/// Requires read access to the repository.
#[utoipa::path(
get,
+5 -5
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -19,14 +19,14 @@ pub struct PathParams {
///
/// Removes the current user's access to the repository.
/// Requires the user to be a member of the repository.
///
///
/// Restrictions:
/// - Repository owner cannot leave (use transfer_owner instead)
///
///
/// Effects:
/// - User loses all access to the repository
/// - User is removed from all repository activities
///
///
/// Returns success message on completion.
#[utoipa::path(
post,
+2 -2
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::Repo;
use crate::service::AppService;
+3 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoBranch;
use crate::service::AppService;
@@ -32,7 +32,7 @@ pub struct QueryParams {
/// - Protected status
/// - Default branch flag
/// - Last push information
///
///
/// Requires read access to the repository.
#[utoipa::path(
get,
+3 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoCommitComment;
use crate::service::AppService;
@@ -35,7 +35,7 @@ pub struct QueryParams {
/// - File path and line number (for inline comments)
/// - Resolved status
/// - Creation and update timestamps
///
///
/// Requires read access to the repository.
#[utoipa::path(
get,
+3 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoCommitStatus;
use crate::service::AppService;
@@ -34,7 +34,7 @@ pub struct QueryParams {
/// - Context name (e.g., "ci/build", "ci/test")
/// - Description and target URL
/// - Creator information
///
///
/// Requires read access to the repository.
#[utoipa::path(
get,
+3 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoDeployKey;
use crate::service::AppService;
@@ -32,7 +32,7 @@ pub struct QueryParams {
/// - Read-only status
/// - Creator information
/// - Creation date and last used date
///
///
/// Requires Admin role or higher in the repository.
#[utoipa::path(
get,
+3 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoFork;
use crate::service::AppService;
@@ -31,7 +31,7 @@ pub struct QueryParams {
/// - Fork repository information
/// - Fork owner and workspace
/// - Fork creation date
///
///
/// Requires read access to the repository.
#[utoipa::path(
get,
+3 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoInvitation;
use crate::service::AppService;
@@ -33,7 +33,7 @@ pub struct QueryParams {
/// - Inviter information
/// - Expiration date
/// - Creation date
///
///
/// Requires Admin role or higher in the repository.
#[utoipa::path(
get,
+3 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoMember;
use crate::service::AppService;
@@ -31,7 +31,7 @@ pub struct QueryParams {
/// - User information (ID, username, display name)
/// - Role (owner, admin, write, read)
/// - Join date and last activity
///
///
/// Requires read access to the repository.
#[utoipa::path(
get,
+3 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::BranchProtectionRule;
use crate::service::AppService;
@@ -33,7 +33,7 @@ pub struct QueryParams {
/// - Required status checks
/// - Restrictions on pushes and deletions
/// - Creator information
///
///
/// Requires read access to the repository.
#[utoipa::path(
get,
+3 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoRelease;
use crate::service::AppService;
@@ -33,7 +33,7 @@ pub struct QueryParams {
/// - Author and creation date
/// - Draft and prerelease status
/// - Asset download URLs
///
///
/// Requires read access to the repository.
#[utoipa::path(
get,
+3 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoStar;
use crate::service::AppService;
@@ -30,7 +30,7 @@ pub struct QueryParams {
/// Includes stargazer metadata such as:
/// - User information (ID, username, display name)
/// - Star timestamp
///
///
/// Requires read access to the repository.
#[utoipa::path(
get,
+3 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoTag;
use crate::service::AppService;
@@ -31,7 +31,7 @@ pub struct QueryParams {
/// - Tag name and commit SHA
/// - Tagger information and timestamp
/// - Tag message (for annotated tags)
///
///
/// Requires read access to the repository.
#[utoipa::path(
get,
+3 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoWatch;
use crate::service::AppService;
@@ -31,7 +31,7 @@ pub struct QueryParams {
/// - User information (ID, username, display name)
/// - Watch level (participating, watching, or ignoring)
/// - Watch timestamp
///
///
/// Requires read access to the repository.
#[utoipa::path(
get,
+3 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoWebhook;
use crate::service::AppService;
@@ -32,7 +32,7 @@ pub struct QueryParams {
/// - Active status
/// - Last delivery status and timestamp
/// - Creator information
///
///
/// Requires Admin role or higher in the repository.
#[utoipa::path(
get,
+3 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::BranchProtectionRule;
use crate::service::AppService;
@@ -26,7 +26,7 @@ pub struct QueryParams {
///
/// Checks if a branch name matches any protection rule in the repository.
/// Requires read access to the repository.
///
///
/// Returns the matching protection rule if found, or null if no rules match.
/// Useful for determining what protections apply to a specific branch before performing operations.
#[utoipa::path(
+121 -70
View File
@@ -1,64 +1,64 @@
use actix_web::web;
pub mod list;
pub mod get;
pub mod create;
pub mod update;
pub mod archive;
pub mod unarchive;
pub mod delete;
pub mod transfer_owner;
pub mod list_branches;
pub mod create_branch;
pub mod set_default_branch;
pub mod set_branch_protection;
pub mod delete_branch;
pub mod list_tags;
pub mod create_tag;
pub mod delete_tag;
pub mod list_releases;
pub mod create_release;
pub mod update_release;
pub mod delete_release;
pub mod list_forks;
pub mod fork_repo;
pub mod sync_fork;
pub mod star_repo;
pub mod unstar_repo;
pub mod list_stargazers;
pub mod watch_repo;
pub mod unwatch_repo;
pub mod list_watchers;
pub mod list_members;
pub mod add_member;
pub mod update_member_role;
pub mod remove_member;
pub mod leave_repo;
pub mod list_invitations;
pub mod create_invitation;
pub mod revoke_invitation;
pub mod accept_invitation;
pub mod list_deploy_keys;
pub mod add_deploy_key;
pub mod delete_deploy_key;
pub mod list_webhooks;
pub mod create_webhook;
pub mod update_webhook;
pub mod delete_webhook;
pub mod list_protection_rules;
pub mod get_protection_rule;
pub mod match_protection;
pub mod create_protection_rule;
pub mod update_protection_rule;
pub mod delete_protection_rule;
pub mod add_member;
pub mod archive;
pub mod check_branch_merge;
pub mod list_commit_statuses;
pub mod create_commit_status;
pub mod list_commit_comments;
pub mod create;
pub mod create_branch;
pub mod create_commit_comment;
pub mod resolve_commit_comment;
pub mod create_commit_status;
pub mod create_invitation;
pub mod create_protection_rule;
pub mod create_release;
pub mod create_tag;
pub mod create_webhook;
pub mod delete;
pub mod delete_branch;
pub mod delete_deploy_key;
pub mod delete_protection_rule;
pub mod delete_release;
pub mod delete_tag;
pub mod delete_webhook;
pub mod fork_repo;
pub mod get;
pub mod get_protection_rule;
pub mod get_stats;
pub mod leave_repo;
pub mod list;
pub mod list_branches;
pub mod list_commit_comments;
pub mod list_commit_statuses;
pub mod list_deploy_keys;
pub mod list_forks;
pub mod list_invitations;
pub mod list_members;
pub mod list_protection_rules;
pub mod list_releases;
pub mod list_stargazers;
pub mod list_tags;
pub mod list_watchers;
pub mod list_webhooks;
pub mod match_protection;
pub mod refresh_stats;
pub mod remove_member;
pub mod resolve_commit_comment;
pub mod revoke_invitation;
pub mod set_branch_protection;
pub mod set_default_branch;
pub mod star_repo;
pub mod sync_fork;
pub mod transfer_owner;
pub mod unarchive;
pub mod unstar_repo;
pub mod unwatch_repo;
pub mod update;
pub mod update_member_role;
pub mod update_protection_rule;
pub mod update_release;
pub mod update_webhook;
pub mod watch_repo;
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(
@@ -69,13 +69,22 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.route("/{repo_name}", web::put().to(update::update))
.route("/{repo_name}", web::delete().to(delete::delete))
.route("/{repo_name}/archive", web::post().to(archive::archive))
.route("/{repo_name}/unarchive", web::post().to(unarchive::unarchive))
.route(
"/{repo_name}/unarchive",
web::post().to(unarchive::unarchive),
)
.route(
"/{repo_name}/transfer-owner",
web::post().to(transfer_owner::transfer_owner),
)
.route("/{repo_name}/branches", web::get().to(list_branches::list_branches))
.route("/{repo_name}/branches", web::post().to(create_branch::create_branch))
.route(
"/{repo_name}/branches",
web::get().to(list_branches::list_branches),
)
.route(
"/{repo_name}/branches",
web::post().to(create_branch::create_branch),
)
.route(
"/{repo_name}/branches/{branch_id}/default",
web::put().to(set_default_branch::set_default_branch),
@@ -90,9 +99,18 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
)
.route("/{repo_name}/tags", web::get().to(list_tags::list_tags))
.route("/{repo_name}/tags", web::post().to(create_tag::create_tag))
.route("/{repo_name}/tags/{tag_id}", web::delete().to(delete_tag::delete_tag))
.route("/{repo_name}/releases", web::get().to(list_releases::list_releases))
.route("/{repo_name}/releases", web::post().to(create_release::create_release))
.route(
"/{repo_name}/tags/{tag_id}",
web::delete().to(delete_tag::delete_tag),
)
.route(
"/{repo_name}/releases",
web::get().to(list_releases::list_releases),
)
.route(
"/{repo_name}/releases",
web::post().to(create_release::create_release),
)
.route(
"/{repo_name}/releases/{release_id}",
web::put().to(update_release::update_release),
@@ -105,13 +123,31 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.route("/{repo_name}/fork", web::post().to(fork_repo::fork_repo))
.route("/{repo_name}/sync", web::post().to(sync_fork::sync_fork))
.route("/{repo_name}/star", web::post().to(star_repo::star_repo))
.route("/{repo_name}/star", web::delete().to(unstar_repo::unstar_repo))
.route("/{repo_name}/stargazers", web::get().to(list_stargazers::list_stargazers))
.route(
"/{repo_name}/star",
web::delete().to(unstar_repo::unstar_repo),
)
.route(
"/{repo_name}/stargazers",
web::get().to(list_stargazers::list_stargazers),
)
.route("/{repo_name}/watch", web::post().to(watch_repo::watch_repo))
.route("/{repo_name}/watch", web::delete().to(unwatch_repo::unwatch_repo))
.route("/{repo_name}/watchers", web::get().to(list_watchers::list_watchers))
.route("/{repo_name}/members", web::get().to(list_members::list_members))
.route("/{repo_name}/members", web::post().to(add_member::add_member))
.route(
"/{repo_name}/watch",
web::delete().to(unwatch_repo::unwatch_repo),
)
.route(
"/{repo_name}/watchers",
web::get().to(list_watchers::list_watchers),
)
.route(
"/{repo_name}/members",
web::get().to(list_members::list_members),
)
.route(
"/{repo_name}/members",
web::post().to(add_member::add_member),
)
.route(
"/{repo_name}/members/{member_id}/role",
web::put().to(update_member_role::update_member_role),
@@ -121,7 +157,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
web::delete().to(remove_member::remove_member),
)
.route("/{repo_name}/leave", web::post().to(leave_repo::leave_repo))
.route("/{repo_name}/invitations", web::get().to(list_invitations::list_invitations))
.route(
"/{repo_name}/invitations",
web::get().to(list_invitations::list_invitations),
)
.route(
"/{repo_name}/invitations",
web::post().to(create_invitation::create_invitation),
@@ -130,14 +169,26 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
"/{repo_name}/invitations/{invitation_id}",
web::delete().to(revoke_invitation::revoke_invitation),
)
.route("/{repo_name}/deploy-keys", web::get().to(list_deploy_keys::list_deploy_keys))
.route("/{repo_name}/deploy-keys", web::post().to(add_deploy_key::add_deploy_key))
.route(
"/{repo_name}/deploy-keys",
web::get().to(list_deploy_keys::list_deploy_keys),
)
.route(
"/{repo_name}/deploy-keys",
web::post().to(add_deploy_key::add_deploy_key),
)
.route(
"/{repo_name}/deploy-keys/{key_id}",
web::delete().to(delete_deploy_key::delete_deploy_key),
)
.route("/{repo_name}/webhooks", web::get().to(list_webhooks::list_webhooks))
.route("/{repo_name}/webhooks", web::post().to(create_webhook::create_webhook))
.route(
"/{repo_name}/webhooks",
web::get().to(list_webhooks::list_webhooks),
)
.route(
"/{repo_name}/webhooks",
web::post().to(create_webhook::create_webhook),
)
.route(
"/{repo_name}/webhooks/{webhook_id}",
web::put().to(update_webhook::update_webhook),
+4 -4
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoStats;
use crate::service::AppService;
@@ -20,7 +20,7 @@ pub struct PathParams {
///
/// Recalculates and updates repository statistics from the current state of the repository.
/// Requires Admin role or higher in the repository.
///
///
/// Effects:
/// - Recalculates star, watcher, and fork counts
/// - Recalculates branch and tag counts
@@ -29,7 +29,7 @@ pub struct PathParams {
/// - Recalculates open issues and pull requests count
/// - Updates storage size and bandwidth usage
/// - Updates last push timestamp
///
///
/// Returns the refreshed statistics with all updated values.
#[utoipa::path(
post,
+11 -6
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -21,15 +21,15 @@ pub struct PathParams {
///
/// Revokes a user's access to the repository.
/// Requires Admin role or higher in the repository.
///
///
/// Restrictions:
/// - Cannot remove the repository owner (use transfer_owner instead)
/// - Cannot remove members with equal or higher role than your own
///
///
/// Effects:
/// - Member loses all access to the repository
/// - Member is removed from all repository activities
///
///
/// Returns success message on completion.
#[utoipa::path(
delete,
@@ -56,7 +56,12 @@ pub async fn remove_member(
) -> Result<HttpResponse, AppError> {
service
.repo
.repo_remove_member(&session, &path.workspace_name, &path.repo_name, path.member_id)
.repo_remove_member(
&session,
&path.workspace_name,
&path.repo_name,
path.member_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Member removed successfully".to_string())))
+13 -6
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -21,12 +21,12 @@ pub struct PathParams {
///
/// Marks a commit comment as resolved, indicating the issue has been addressed.
/// Requires Write role or higher in the repository.
///
///
/// Effects:
/// - Comment is marked as resolved
/// - Resolved comments are visually distinguished in code review interfaces
/// - Resolution is recorded with the resolver's user ID and timestamp
///
///
/// Returns success message on completion.
#[utoipa::path(
post,
@@ -53,8 +53,15 @@ pub async fn resolve_commit_comment(
) -> Result<HttpResponse, AppError> {
service
.repo
.repo_resolve_commit_comment(&session, &path.workspace_name, &path.repo_name, path.comment_id)
.repo_resolve_commit_comment(
&session,
&path.workspace_name,
&path.repo_name,
path.comment_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Commit comment resolved successfully".to_string())))
Ok(HttpResponse::Ok().json(ApiResponse::new(
"Commit comment resolved successfully".to_string(),
)))
}
+13 -6
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -21,12 +21,12 @@ pub struct PathParams {
///
/// Cancels a pending invitation, preventing the invitee from accepting it.
/// Requires Admin role or higher in the repository.
///
///
/// Effects:
/// - Invitation is marked as revoked
/// - Invitee can no longer accept the invitation
/// - Invitation email link becomes invalid
///
///
/// Returns success message on completion.
#[utoipa::path(
delete,
@@ -52,8 +52,15 @@ pub async fn revoke_invitation(
) -> Result<HttpResponse, AppError> {
service
.repo
.repo_revoke_invitation(&session, &path.workspace_name, &path.repo_name, path.invitation_id)
.repo_revoke_invitation(
&session,
&path.workspace_name,
&path.repo_name,
path.invitation_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Invitation revoked successfully".to_string())))
Ok(HttpResponse::Ok().json(ApiResponse::new(
"Invitation revoked successfully".to_string(),
)))
}
+5 -3
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::{IntoParams, ToSchema};
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -73,5 +73,7 @@ pub async fn set_branch_protection(
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Branch protection rules set successfully".to_string())))
Ok(HttpResponse::Ok().json(ApiResponse::new(
"Branch protection rules set successfully".to_string(),
)))
}
+13 -6
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -23,9 +23,9 @@ pub struct PathParams {
/// - New pull requests base branch
/// - Repository cloning
/// - New branch creation base
///
///
/// Requires Admin role or higher in the repository.
///
///
/// Returns success message on completion.
#[utoipa::path(
put,
@@ -51,8 +51,15 @@ pub async fn set_default_branch(
) -> Result<HttpResponse, AppError> {
service
.repo
.repo_set_default_branch(&session, &path.workspace_name, &path.repo_name, path.branch_id)
.repo_set_default_branch(
&session,
&path.workspace_name,
&path.repo_name,
path.branch_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Default branch set successfully".to_string())))
Ok(HttpResponse::Ok().json(ApiResponse::new(
"Default branch set successfully".to_string(),
)))
}
+7 -5
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -19,12 +19,12 @@ pub struct PathParams {
///
/// Adds the current user to the repository's stargazers list.
/// Requires read access to the repository.
///
///
/// Effects:
/// - User is added to the repository's stargazers
/// - Repository star count is incremented
/// - User can unstar later to remove themselves
///
///
/// Returns success message on completion. Idempotent operation (starring an already starred repository is a no-op).
#[utoipa::path(
post,
@@ -53,5 +53,7 @@ pub async fn star_repo(
.repo_star(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Repository starred successfully".to_string())))
Ok(HttpResponse::Ok().json(ApiResponse::new(
"Repository starred successfully".to_string(),
)))
}
+7 -5
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -19,12 +19,12 @@ pub struct PathParams {
///
/// Synchronizes a forked repository with the latest changes from the parent repository.
/// Requires Write role or higher in the fork repository.
///
///
/// Effects:
/// - Merges changes from the parent repository's default branch into the fork
/// - Creates a merge commit if there are conflicts
/// - Updates the fork's commit history
///
///
/// Only works on repositories that are forks (have a parent repository).
/// Returns success message on completion.
#[utoipa::path(
@@ -56,5 +56,7 @@ pub async fn sync_fork(
.repo_sync_fork(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Fork synchronized successfully".to_string())))
Ok(HttpResponse::Ok().json(ApiResponse::new(
"Fork synchronized successfully".to_string(),
)))
}
+4 -4
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::{IntoParams, ToSchema};
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::Repo;
use crate::service::AppService;
@@ -26,13 +26,13 @@ pub struct TransferOwnerParams {
///
/// Transfers ownership of a repository to another user. The new owner must be an existing repository member.
/// Requires Owner role in the repository.
///
///
/// Effects:
/// - Current owner becomes an Admin
/// - New owner gains full Owner permissions
/// - Repository URL remains unchanged
/// - All forks and stars are preserved
///
///
/// Returns the updated repository with new owner information.
#[utoipa::path(
post,
+4 -4
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -19,12 +19,12 @@ pub struct PathParams {
///
/// Restores an archived repository to active status, re-enabling all write operations.
/// Requires Owner role in the repository.
///
///
/// Effects:
/// - Repository status changes from "archived" to "active"
/// - All write operations are re-enabled
/// - Previous visibility and settings are preserved
///
///
/// Returns success message on completion.
#[utoipa::path(
post,
+7 -5
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -19,11 +19,11 @@ pub struct PathParams {
///
/// Removes the current user from the repository's stargazers list.
/// Requires read access to the repository.
///
///
/// Effects:
/// - User is removed from the repository's stargazers
/// - Repository star count is decremented
///
///
/// Returns success message on completion. Idempotent operation (unstarring an unstarred repository is a no-op).
#[utoipa::path(
delete,
@@ -52,5 +52,7 @@ pub async fn unstar_repo(
.repo_unstar(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Repository unstarred successfully".to_string())))
Ok(HttpResponse::Ok().json(ApiResponse::new(
"Repository unstarred successfully".to_string(),
)))
}
+7 -5
View File
@@ -1,8 +1,8 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
@@ -19,11 +19,11 @@ pub struct PathParams {
///
/// Removes the current user's watch subscription from the repository.
/// Requires read access to the repository.
///
///
/// Effects:
/// - User is removed from the repository's watchers
/// - User will no longer receive notifications for repository activities
///
///
/// Returns success message on completion. Idempotent operation (unwatching an unwatched repository is a no-op).
#[utoipa::path(
delete,
@@ -52,5 +52,7 @@ pub async fn unwatch_repo(
.repo_unwatch(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Repository watch subscription removed successfully".to_string())))
Ok(HttpResponse::Ok().json(ApiResponse::new(
"Repository watch subscription removed successfully".to_string(),
)))
}
+5 -5
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::Repo;
use crate::service::repo::core::UpdateRepoParams;
use crate::service::AppService;
use crate::service::repo::core::UpdateRepoParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -21,13 +21,13 @@ pub struct PathParams {
///
/// Updates repository metadata such as name, description, visibility, and default branch.
/// Requires Admin role or higher in the repository.
///
///
/// Update rules:
/// - name: Must be unique within workspace if changed (1-100 characters)
/// - description: Max 500 characters
/// - visibility: "public", "private", or "internal" (workspace owners can restrict public repos)
/// - default_branch: Must be an existing branch name
///
///
/// All fields are optional; only provided fields are updated.
/// Returns the updated repository with full metadata.
#[utoipa::path(
+5 -5
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoMember;
use crate::service::repo::members::UpdateRepoMemberRoleParams;
use crate::service::AppService;
use crate::service::repo::members::UpdateRepoMemberRoleParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -23,13 +23,13 @@ pub struct PathParams {
///
/// Changes the access level of an existing repository member.
/// Requires Admin role or higher in the repository.
///
///
/// Role restrictions:
/// - Cannot change the owner's role (use transfer_owner instead)
/// - Cannot assign "owner" role (use transfer_owner instead)
/// - Can only assign roles equal to or lower than your own
/// - Valid roles: "read", "write", "admin"
///
///
/// Returns the updated member record with new role information.
#[utoipa::path(
put,
+5 -5
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::BranchProtectionRule;
use crate::service::repo::protection::UpdateProtectionRuleParams;
use crate::service::AppService;
use crate::service::repo::protection::UpdateProtectionRuleParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -23,7 +23,7 @@ pub struct PathParams {
///
/// Updates an existing branch protection rule's configuration.
/// Requires Admin role or higher in the repository.
///
///
/// Updatable fields:
/// - required_approvals: Number of required approvals before merging (0-10)
/// - require_status_checks: Whether status checks must pass
@@ -31,7 +31,7 @@ pub struct PathParams {
/// - restrict_pushes: Restrict who can push to matching branches
/// - allow_force_pushes: Allow force pushes (only if restrict_pushes is false)
/// - allow_deletions: Allow branch deletion (only if restrict_pushes is false)
///
///
/// All fields are optional; only provided fields are updated.
/// Returns the updated protection rule with full configuration.
#[utoipa::path(
+5 -5
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoRelease;
use crate::service::repo::releases::UpdateReleaseParams;
use crate::service::AppService;
use crate::service::repo::releases::UpdateReleaseParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -23,13 +23,13 @@ pub struct PathParams {
///
/// Updates release metadata such as name, description, draft status, and prerelease flag.
/// Requires Write role or higher in the repository.
///
///
/// Updatable fields:
/// - name: Release name/title (max 255 characters)
/// - body: Release notes in markdown format (max 10000 characters)
/// - draft: Whether this is a draft release
/// - prerelease: Whether this is a prerelease
///
///
/// All fields are optional; only provided fields are updated.
/// Returns the updated release with full metadata.
#[utoipa::path(
+5 -5
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoWebhook;
use crate::service::repo::webhooks::UpdateWebhookParams;
use crate::service::AppService;
use crate::service::repo::webhooks::UpdateWebhookParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
@@ -23,13 +23,13 @@ pub struct PathParams {
///
/// Updates webhook configuration such as URL, events, secret, and active status.
/// Requires Admin role or higher in the repository.
///
///
/// Updatable fields:
/// - url: Webhook endpoint URL (must be HTTPS in production)
/// - events: List of events to subscribe to
/// - secret: Secret for webhook signature verification
/// - active: Whether the webhook is active
///
///
/// All fields are optional; only provided fields are updated.
/// Returns the updated webhook with full metadata.
#[utoipa::path(
+14 -7
View File
@@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse};
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiResponse, ApiErrorResponse};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
use crate::service::repo::watches::WatchParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
@@ -20,12 +20,12 @@ pub struct PathParams {
///
/// Subscribes the current user to notifications for repository activities.
/// Requires read access to the repository.
///
///
/// Watch levels:
/// - "participating": Notifications for issues/PRs you're involved in
/// - "watching": All repository notifications (default)
/// - "ignoring": No notifications
///
///
/// Returns success message on completion. Idempotent operation.
#[utoipa::path(
post,
@@ -58,8 +58,15 @@ pub async fn watch_repo(
) -> Result<HttpResponse, AppError> {
service
.repo
.repo_watch(&session, &path.workspace_name, &path.repo_name, params.into_inner())
.repo_watch(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Repository watch subscription updated successfully".to_string())))
Ok(HttpResponse::Ok().json(ApiResponse::new(
"Repository watch subscription updated successfully".to_string(),
)))
}
+9 -1
View File
@@ -2,14 +2,22 @@ use actix_web::web;
use actix_web::web::scope;
use crate::api::auth;
use crate::api::issue;
use crate::api::repo;
use crate::api::user;
use crate::api::workspace;
pub fn init_routes(cfg: &mut web::ServiceConfig) {
cfg.service(
scope("/api/v1")
.configure(auth::configure)
.configure(user::configure)
.configure(workspace::configure)
.configure(repo::configure),
.configure(repo::configure)
.service(
scope("/workspaces/{workspace_name}")
.configure(issue::configure)
.service(scope("/repos/{repo_name}").configure(issue::configure_repo_level)),
),
);
}
+58
View File
@@ -0,0 +1,58 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::users::UserGpgKey;
use crate::service::AppService;
use crate::service::user::keys::AddGpgKeyParams;
use crate::session::Session;
/// Add a GPG key
///
/// Registers a new GPG public key for the authenticated user.
/// Requires authentication.
///
/// Parameters:
/// - public_key: ASCII-armored PGP public key block
/// - key_id: Short key ID or full fingerprint
/// - primary_email: Primary email associated with the key (optional)
/// - expires_at: Optional expiration date for the key
///
/// Effects:
/// - Key fingerprint is computed and stored
/// - Key is immediately usable for verifying signed commits/tags
/// - Duplicate keys are rejected
///
/// Returns the created GPG key with fingerprint and metadata.
#[utoipa::path(
post,
path = "/api/v1/user/keys/gpg",
tag = "User",
operation_id = "userAddGpgKey",
request_body(
content = AddGpgKeyParams,
description = "GPG key creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "GPG key added successfully. Returns the created key with fingerprint and metadata.", body = ApiResponse<UserGpgKey>),
(status = 400, description = "Invalid parameters: invalid PGP key format", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 409, description = "GPG key with this fingerprint already exists", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn add_gpg_key(
service: web::Data<AppService>,
session: Session,
params: web::Json<AddGpgKeyParams>,
) -> Result<HttpResponse, AppError> {
let key = service
.user
.user_add_gpg_key(&session, params.into_inner())
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(key)))
}
+58
View File
@@ -0,0 +1,58 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::users::UserSshKey;
use crate::service::AppService;
use crate::service::user::keys::AddSshKeyParams;
use crate::session::Session;
/// Add an SSH key
///
/// Registers a new SSH public key for the authenticated user.
/// Requires authentication.
///
/// Parameters:
/// - title: Human-readable label for the key (e.g., "Work Laptop")
/// - public_key: SSH public key string (supports RSA, Ed25519, ECDSA, DSA)
/// - key_type: Key algorithm type ("rsa", "ed25519", "ecdsa", "dsa")
/// - expires_at: Optional expiration date for the key
///
/// Effects:
/// - Key fingerprint is computed and stored
/// - Key is immediately usable for Git operations
/// - Duplicate keys are rejected
///
/// Returns the created SSH key with fingerprint and metadata.
#[utoipa::path(
post,
path = "/api/v1/user/keys/ssh",
tag = "User",
operation_id = "userAddSshKey",
request_body(
content = AddSshKeyParams,
description = "SSH key creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "SSH key added successfully. Returns the created key with fingerprint and metadata.", body = ApiResponse<UserSshKey>),
(status = 400, description = "Invalid parameters: invalid key format, unsupported key type, or type mismatch", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 409, description = "SSH key with this fingerprint already exists", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn add_ssh_key(
service: web::Data<AppService>,
session: Session,
params: web::Json<AddSshKeyParams>,
) -> Result<HttpResponse, AppError> {
let key = service
.user
.user_add_ssh_key(&session, params.into_inner())
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(key)))
}
+46
View File
@@ -0,0 +1,46 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
/// Delete user account
///
/// Permanently deletes the authenticated user's account and all associated data.
/// Requires authentication.
///
/// Preconditions:
/// - User must transfer or delete all owned workspaces
/// - User must transfer or delete all owned repositories
///
/// Effects:
/// - All user data is removed (SSH keys, GPG keys, sessions, devices, OAuth links, etc.)
/// - User is soft-deleted (marked as deleted, not physically removed)
/// - Current session is cleared
/// - Account cannot be recovered
///
/// Returns success message on completion.
#[utoipa::path(
delete,
path = "/api/v1/user/account",
tag = "User",
operation_id = "userDeleteAccount",
responses(
(status = 200, description = "Account deleted successfully. All user data has been removed.", body = ApiResponse<String>),
(status = 400, description = "Cannot delete: user still owns workspaces or repositories", body = ApiErrorResponse),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "User not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn delete_account(
service: web::Data<AppService>,
session: Session,
) -> Result<HttpResponse, AppError> {
service.user.user_delete_account(&session).await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Account deleted successfully".to_string())))
}

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