diff --git a/.junie/memory/errors.md b/.junie/memory/errors.md new file mode 100644 index 0000000..e69de29 diff --git a/.junie/memory/feedback.md b/.junie/memory/feedback.md new file mode 100644 index 0000000..e69de29 diff --git a/.junie/memory/language.json b/.junie/memory/language.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/.junie/memory/language.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/.junie/memory/memory.version b/.junie/memory/memory.version new file mode 100644 index 0000000..f398a20 --- /dev/null +++ b/.junie/memory/memory.version @@ -0,0 +1 @@ +3.0 \ No newline at end of file diff --git a/.junie/memory/tasks.md b/.junie/memory/tasks.md new file mode 100644 index 0000000..e69de29 diff --git a/.junie/models/mimo.json b/.junie/models/mimo.json new file mode 100644 index 0000000..61dfac6 --- /dev/null +++ b/.junie/models/mimo.json @@ -0,0 +1,9 @@ +{ + "baseUrl": "https://token-plan-cn.xiaomimimo.com/v1/chat/completions", + "id": "mimo-v2.5-pro", + "apiKey": "tp-c1ngp1xj5nd7smshsf0t82rex47moenx8mlwobslw7sk0gpv", + "apiType": "OpenAICompletion", + "primaryModel": { + "id": "mimo-v2.5-pro" + } +} \ No newline at end of file diff --git a/api/issue/assign_issue.rs b/api/issue/assign_issue.rs new file mode 100644 index 0000000..0a13de5 --- /dev/null +++ b/api/issue/assign_issue.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + let assignee = service + .issue + .issue_assign(&session, &path.workspace_name, path.number, path.user_id) + .await?; + Ok(HttpResponse::Created().json(ApiResponse::new(assignee))) +} diff --git a/api/issue/assign_label.rs b/api/issue/assign_label.rs new file mode 100644 index 0000000..d7e212a --- /dev/null +++ b/api/issue/assign_label.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + let rel = service + .issue + .issue_assign_label(&session, &path.workspace_name, path.number, path.label_id) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(rel))) +} diff --git a/api/issue/close.rs b/api/issue/close.rs new file mode 100644 index 0000000..777bb43 --- /dev/null +++ b/api/issue/close.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + let issue = service + .issue + .issue_close(&session, &path.workspace_name, path.number) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(issue))) +} diff --git a/api/issue/create.rs b/api/issue/create.rs new file mode 100644 index 0000000..b511bbd --- /dev/null +++ b/api/issue/create.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + let issue = service + .issue + .issue_create(&session, &path.workspace_name, params.into_inner()) + .await?; + Ok(HttpResponse::Created().json(ApiResponse::new(issue))) +} diff --git a/api/issue/create_comment.rs b/api/issue/create_comment.rs new file mode 100644 index 0000000..663242d --- /dev/null +++ b/api/issue/create_comment.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + let comment = service + .issue + .issue_create_comment( + &session, + &path.workspace_name, + path.number, + params.into_inner(), + ) + .await?; + Ok(HttpResponse::Created().json(ApiResponse::new(comment))) +} diff --git a/api/issue/create_label.rs b/api/issue/create_label.rs new file mode 100644 index 0000000..36499fc --- /dev/null +++ b/api/issue/create_label.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/issue/create_milestone.rs b/api/issue/create_milestone.rs new file mode 100644 index 0000000..d0e9f75 --- /dev/null +++ b/api/issue/create_milestone.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/issue/delete.rs b/api/issue/delete.rs new file mode 100644 index 0000000..d989255 --- /dev/null +++ b/api/issue/delete.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + service + .issue + .issue_delete(&session, &path.workspace_name, path.number) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new("Issue deleted successfully".to_string()))) +} diff --git a/api/issue/delete_comment.rs b/api/issue/delete_comment.rs new file mode 100644 index 0000000..440aaec --- /dev/null +++ b/api/issue/delete_comment.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/issue/delete_label.rs b/api/issue/delete_label.rs new file mode 100644 index 0000000..092c5a7 --- /dev/null +++ b/api/issue/delete_label.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/issue/delete_milestone.rs b/api/issue/delete_milestone.rs new file mode 100644 index 0000000..b313a8b --- /dev/null +++ b/api/issue/delete_milestone.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/issue/get.rs b/api/issue/get.rs new file mode 100644 index 0000000..e7b7b59 --- /dev/null +++ b/api/issue/get.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + let issue = service + .issue + .issue_get(&session, &path.workspace_name, path.number) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(issue))) +} diff --git a/api/issue/list.rs b/api/issue/list.rs new file mode 100644 index 0000000..952d6f4 --- /dev/null +++ b/api/issue/list.rs @@ -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, + /// Filter by priority level + pub priority: Option, + /// Filter by author user ID + pub author_id: Option, + /// Filter by assignee user ID + pub assignee_id: Option, + /// Filter by milestone ID + pub milestone_id: Option, + /// Filter by label ID + pub label_id: Option, + /// Maximum number of issues to return (default: 50, max: 100) + pub limit: Option, + /// Number of issues to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/issue/list_assignees.rs b/api/issue/list_assignees.rs new file mode 100644 index 0000000..947eda4 --- /dev/null +++ b/api/issue/list_assignees.rs @@ -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, + /// Number of assignees to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/issue/list_comments.rs b/api/issue/list_comments.rs new file mode 100644 index 0000000..e2e62c8 --- /dev/null +++ b/api/issue/list_comments.rs @@ -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, + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/issue/list_events.rs b/api/issue/list_events.rs new file mode 100644 index 0000000..935fcb0 --- /dev/null +++ b/api/issue/list_events.rs @@ -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, + /// Number of events to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/issue/list_issue_labels.rs b/api/issue/list_issue_labels.rs new file mode 100644 index 0000000..5767ccf --- /dev/null +++ b/api/issue/list_issue_labels.rs @@ -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, + /// Number of label relations to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/issue/list_labels.rs b/api/issue/list_labels.rs new file mode 100644 index 0000000..6939d63 --- /dev/null +++ b/api/issue/list_labels.rs @@ -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>), + (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, + session: Session, + path: web::Path, +) -> Result { + let labels = service + .issue + .issue_labels(&session, &path.workspace_name, &path.repo_name) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(labels))) +} diff --git a/api/issue/list_milestones.rs b/api/issue/list_milestones.rs new file mode 100644 index 0000000..19a2ced --- /dev/null +++ b/api/issue/list_milestones.rs @@ -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, + /// Number of milestones to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/issue/lock.rs b/api/issue/lock.rs new file mode 100644 index 0000000..5c9240d --- /dev/null +++ b/api/issue/lock.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + let issue = service + .issue + .issue_lock(&session, &path.workspace_name, path.number, params.locked) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(issue))) +} diff --git a/api/issue/mod.rs b/api/issue/mod.rs new file mode 100644 index 0000000..67a37e7 --- /dev/null +++ b/api/issue/mod.rs @@ -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), + ), + ); +} diff --git a/api/issue/pr_relations.rs b/api/issue/pr_relations.rs new file mode 100644 index 0000000..037e8fa --- /dev/null +++ b/api/issue/pr_relations.rs @@ -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, + /// Number of relations to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/issue/reactions.rs b/api/issue/reactions.rs new file mode 100644 index 0000000..097c4a9 --- /dev/null +++ b/api/issue/reactions.rs @@ -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, + /// Number of reactions to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/issue/reopen.rs b/api/issue/reopen.rs new file mode 100644 index 0000000..fce94b0 --- /dev/null +++ b/api/issue/reopen.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + let issue = service + .issue + .issue_reopen(&session, &path.workspace_name, path.number) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(issue))) +} diff --git a/api/issue/repo_relations.rs b/api/issue/repo_relations.rs new file mode 100644 index 0000000..0958dfd --- /dev/null +++ b/api/issue/repo_relations.rs @@ -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, + /// Number of relations to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/issue/subscribers.rs b/api/issue/subscribers.rs new file mode 100644 index 0000000..07a6236 --- /dev/null +++ b/api/issue/subscribers.rs @@ -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, + /// Number of subscribers to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + service + .issue + .issue_mute(&session, &path.workspace_name, path.number, params.muted) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new("Mute status updated".to_string()))) +} diff --git a/api/issue/templates.rs b/api/issue/templates.rs new file mode 100644 index 0000000..17fc15d --- /dev/null +++ b/api/issue/templates.rs @@ -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, + /// Number of templates to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/issue/transfer.rs b/api/issue/transfer.rs new file mode 100644 index 0000000..627a3b3 --- /dev/null +++ b/api/issue/transfer.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + let issue = service + .issue + .issue_transfer( + &session, + &path.workspace_name, + path.number, + ¶ms.target_workspace_name, + ) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(issue))) +} diff --git a/api/issue/unassign_issue.rs b/api/issue/unassign_issue.rs new file mode 100644 index 0000000..a6727ff --- /dev/null +++ b/api/issue/unassign_issue.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + service + .issue + .issue_unassign(&session, &path.workspace_name, path.number, path.user_id) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new("User unassigned".to_string()))) +} diff --git a/api/issue/unassign_label.rs b/api/issue/unassign_label.rs new file mode 100644 index 0000000..68c8118 --- /dev/null +++ b/api/issue/unassign_label.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/issue/update.rs b/api/issue/update.rs new file mode 100644 index 0000000..98eb99b --- /dev/null +++ b/api/issue/update.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + let issue = service + .issue + .issue_update( + &session, + &path.workspace_name, + path.number, + params.into_inner(), + ) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(issue))) +} diff --git a/api/issue/update_comment.rs b/api/issue/update_comment.rs new file mode 100644 index 0000000..6b26ffd --- /dev/null +++ b/api/issue/update_comment.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/issue/update_label.rs b/api/issue/update_label.rs new file mode 100644 index 0000000..9cd35ec --- /dev/null +++ b/api/issue/update_label.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/issue/update_milestone.rs b/api/issue/update_milestone.rs new file mode 100644 index 0000000..96ffd5b --- /dev/null +++ b/api/issue/update_milestone.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/mod.rs b/api/mod.rs index ffc70e5..8baefd6 100644 --- a/api/mod.rs +++ b/api/mod.rs @@ -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; diff --git a/api/openapi.rs b/api/openapi.rs index 983caa5..7af80ad 100644 --- a/api/openapi.rs +++ b/api/openapi.rs @@ -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, + ApiResponse, + ApiResponse, + User, + UpdateUserAccountParams, + UploadUserAvatarParams, + UserAvatarResponse, + ApiResponse, + UserAppearance, + UpdateUserAppearanceParams, + ApiResponse, + UserProfile, + UpdateUserProfileParams, + ApiResponse, + UserNotifySetting, + UpdateUserNotifySettingParams, + ApiResponse, + ApiResponse>, + UserSshKey, + AddSshKeyParams, + ApiResponse, + ApiResponse>, + UserGpgKey, + AddGpgKeyParams, + ApiResponse, + ApiResponse>, + UserSessionInfo, + ApiResponse, + ApiResponse>, + UserDevice, + ApiResponse, + ApiResponse>, + UserOAuthInfo, + ApiResponse, + ApiResponse>, + UserSecurityLog, + ApiResponse, + ApiResponse>, + UserPersonalAccessTokenInfo, + // Issues + ApiResponse, + ApiResponse>, + Issue, + CreateIssueParams, + IssueListFilters, + UpdateIssueParams, + LockIssueParams, + TransferIssueParams, + ApiResponse, + ApiResponse>, + IssueComment, + CreateCommentParams, + UpdateCommentParams, + ApiResponse, + ApiResponse>, + IssueLabel, + CreateLabelParams, + UpdateLabelParams, + ApiResponse, + ApiResponse>, + IssueLabelRelation, + ApiResponse, + ApiResponse>, + IssueMilestone, + CreateMilestoneParams, + UpdateMilestoneParams, + ApiResponse, + ApiResponse>, + IssueAssignee, + ApiResponse, + ApiResponse>, + IssueEvent, + ApiResponse, + ApiResponse>, + IssueReaction, + CreateIssueReactionParams, + ApiResponse, + ApiResponse>, + IssueSubscriber, + MuteIssueParams, + ApiResponse, + ApiResponse>, + IssueTemplate, + CreateTemplateParams, + UpdateTemplateParams, + ApiResponse, + ApiResponse>, + IssueRepoRelation, + LinkRepoParams, + ApiResponse, + ApiResponse>, + IssuePrRelation, + LinkPrParams, + // Workspaces ApiResponse, ApiResponse>, ApiResponse, @@ -307,7 +517,6 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara ApiResponse, ApiResponse>, ApiResponse, - ApiResponse, Repo, CreateRepoParams, UpdateRepoParams, @@ -334,8 +543,8 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara RepoDeployKey, AddDeployKeyParams, RepoWebhook, - CreateWebhookParams, - UpdateWebhookParams, + RepoCreateWebhookParams, + RepoUpdateWebhookParams, BranchProtectionRule, CreateProtectionRuleParams, UpdateProtectionRuleParams, diff --git a/api/repo/accept_invitation.rs b/api/repo/accept_invitation.rs index 765072b..f4373df 100644 --- a/api/repo/accept_invitation.rs +++ b/api/repo/accept_invitation.rs @@ -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, diff --git a/api/repo/add_deploy_key.rs b/api/repo/add_deploy_key.rs index 697eeee..d29a1f7 100644 --- a/api/repo/add_deploy_key.rs +++ b/api/repo/add_deploy_key.rs @@ -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 { 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))) diff --git a/api/repo/add_member.rs b/api/repo/add_member.rs index 2d2f3e7..9f3314e 100644 --- a/api/repo/add_member.rs +++ b/api/repo/add_member.rs @@ -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 { 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))) diff --git a/api/repo/archive.rs b/api/repo/archive.rs index 4d10e3d..108b747 100644 --- a/api/repo/archive.rs +++ b/api/repo/archive.rs @@ -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, diff --git a/api/repo/check_branch_merge.rs b/api/repo/check_branch_merge.rs index df009a8..ccd7d30 100644 --- a/api/repo/check_branch_merge.rs +++ b/api/repo/check_branch_merge.rs @@ -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)] diff --git a/api/repo/create.rs b/api/repo/create.rs index 5e9fb7d..db945a5 100644 --- a/api/repo/create.rs +++ b/api/repo/create.rs @@ -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, diff --git a/api/repo/create_branch.rs b/api/repo/create_branch.rs index fa9ac3a..ddba989 100644 --- a/api/repo/create_branch.rs +++ b/api/repo/create_branch.rs @@ -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 { 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))) diff --git a/api/repo/create_commit_comment.rs b/api/repo/create_commit_comment.rs index 6ddb5b9..85445a7 100644 --- a/api/repo/create_commit_comment.rs +++ b/api/repo/create_commit_comment.rs @@ -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 { 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))) diff --git a/api/repo/create_commit_status.rs b/api/repo/create_commit_status.rs index 74a62a7..693aaa3 100644 --- a/api/repo/create_commit_status.rs +++ b/api/repo/create_commit_status.rs @@ -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 { 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))) diff --git a/api/repo/create_invitation.rs b/api/repo/create_invitation.rs index 5663e7e..7fa04bb 100644 --- a/api/repo/create_invitation.rs +++ b/api/repo/create_invitation.rs @@ -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 { 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))) diff --git a/api/repo/create_protection_rule.rs b/api/repo/create_protection_rule.rs index 9136241..714e145 100644 --- a/api/repo/create_protection_rule.rs +++ b/api/repo/create_protection_rule.rs @@ -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 { 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))) diff --git a/api/repo/create_release.rs b/api/repo/create_release.rs index 743297e..f5e3da0 100644 --- a/api/repo/create_release.rs +++ b/api/repo/create_release.rs @@ -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 { 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))) diff --git a/api/repo/create_tag.rs b/api/repo/create_tag.rs index b46a7bb..29ad189 100644 --- a/api/repo/create_tag.rs +++ b/api/repo/create_tag.rs @@ -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 { 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))) diff --git a/api/repo/create_webhook.rs b/api/repo/create_webhook.rs index 034f201..a15d4cc 100644 --- a/api/repo/create_webhook.rs +++ b/api/repo/create_webhook.rs @@ -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 { 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))) diff --git a/api/repo/delete.rs b/api/repo/delete.rs index 686d675..4a83050 100644 --- a/api/repo/delete.rs +++ b/api/repo/delete.rs @@ -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, diff --git a/api/repo/delete_branch.rs b/api/repo/delete_branch.rs index 36dd99c..240a25e 100644 --- a/api/repo/delete_branch.rs +++ b/api/repo/delete_branch.rs @@ -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 { 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()))) diff --git a/api/repo/delete_deploy_key.rs b/api/repo/delete_deploy_key.rs index 06e2f12..413a1cf 100644 --- a/api/repo/delete_deploy_key.rs +++ b/api/repo/delete_deploy_key.rs @@ -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(), + ))) } diff --git a/api/repo/delete_protection_rule.rs b/api/repo/delete_protection_rule.rs index 108313b..20df822 100644 --- a/api/repo/delete_protection_rule.rs +++ b/api/repo/delete_protection_rule.rs @@ -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 { 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(), + ))) } diff --git a/api/repo/delete_release.rs b/api/repo/delete_release.rs index 56c3727..f472285 100644 --- a/api/repo/delete_release.rs +++ b/api/repo/delete_release.rs @@ -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 { 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()))) diff --git a/api/repo/delete_tag.rs b/api/repo/delete_tag.rs index 075bb3e..e0c0352 100644 --- a/api/repo/delete_tag.rs +++ b/api/repo/delete_tag.rs @@ -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, diff --git a/api/repo/delete_webhook.rs b/api/repo/delete_webhook.rs index 04082fd..654e58d 100644 --- a/api/repo/delete_webhook.rs +++ b/api/repo/delete_webhook.rs @@ -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 { 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()))) diff --git a/api/repo/fork_repo.rs b/api/repo/fork_repo.rs index 47ac0f5..e5b8434 100644 --- a/api/repo/fork_repo.rs +++ b/api/repo/fork_repo.rs @@ -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 { 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))) diff --git a/api/repo/get.rs b/api/repo/get.rs index f1900f4..b7ca14c 100644 --- a/api/repo/get.rs +++ b/api/repo/get.rs @@ -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; diff --git a/api/repo/get_protection_rule.rs b/api/repo/get_protection_rule.rs index 08f2376..f950975 100644 --- a/api/repo/get_protection_rule.rs +++ b/api/repo/get_protection_rule.rs @@ -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 { 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))) diff --git a/api/repo/get_stats.rs b/api/repo/get_stats.rs index 56a2026..46c8019 100644 --- a/api/repo/get_stats.rs +++ b/api/repo/get_stats.rs @@ -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, diff --git a/api/repo/leave_repo.rs b/api/repo/leave_repo.rs index 6fbc4bb..bbffece 100644 --- a/api/repo/leave_repo.rs +++ b/api/repo/leave_repo.rs @@ -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, diff --git a/api/repo/list.rs b/api/repo/list.rs index f00e175..c76ee7e 100644 --- a/api/repo/list.rs +++ b/api/repo/list.rs @@ -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; diff --git a/api/repo/list_branches.rs b/api/repo/list_branches.rs index ec46cf5..89a1bca 100644 --- a/api/repo/list_branches.rs +++ b/api/repo/list_branches.rs @@ -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, diff --git a/api/repo/list_commit_comments.rs b/api/repo/list_commit_comments.rs index d7a0b87..d8cc5f0 100644 --- a/api/repo/list_commit_comments.rs +++ b/api/repo/list_commit_comments.rs @@ -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, diff --git a/api/repo/list_commit_statuses.rs b/api/repo/list_commit_statuses.rs index 5874c30..5a4f275 100644 --- a/api/repo/list_commit_statuses.rs +++ b/api/repo/list_commit_statuses.rs @@ -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, diff --git a/api/repo/list_deploy_keys.rs b/api/repo/list_deploy_keys.rs index ecf622e..609fc63 100644 --- a/api/repo/list_deploy_keys.rs +++ b/api/repo/list_deploy_keys.rs @@ -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, diff --git a/api/repo/list_forks.rs b/api/repo/list_forks.rs index f10d709..57d9666 100644 --- a/api/repo/list_forks.rs +++ b/api/repo/list_forks.rs @@ -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, diff --git a/api/repo/list_invitations.rs b/api/repo/list_invitations.rs index cbd136d..cc347d5 100644 --- a/api/repo/list_invitations.rs +++ b/api/repo/list_invitations.rs @@ -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, diff --git a/api/repo/list_members.rs b/api/repo/list_members.rs index 216dc2a..fabe229 100644 --- a/api/repo/list_members.rs +++ b/api/repo/list_members.rs @@ -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, diff --git a/api/repo/list_protection_rules.rs b/api/repo/list_protection_rules.rs index 34657a2..0f1c23e 100644 --- a/api/repo/list_protection_rules.rs +++ b/api/repo/list_protection_rules.rs @@ -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, diff --git a/api/repo/list_releases.rs b/api/repo/list_releases.rs index 3b842db..17381a6 100644 --- a/api/repo/list_releases.rs +++ b/api/repo/list_releases.rs @@ -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, diff --git a/api/repo/list_stargazers.rs b/api/repo/list_stargazers.rs index c9e47d2..bed27fd 100644 --- a/api/repo/list_stargazers.rs +++ b/api/repo/list_stargazers.rs @@ -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, diff --git a/api/repo/list_tags.rs b/api/repo/list_tags.rs index 76666c7..9a3599d 100644 --- a/api/repo/list_tags.rs +++ b/api/repo/list_tags.rs @@ -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, diff --git a/api/repo/list_watchers.rs b/api/repo/list_watchers.rs index f9a89b2..789fc1d 100644 --- a/api/repo/list_watchers.rs +++ b/api/repo/list_watchers.rs @@ -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, diff --git a/api/repo/list_webhooks.rs b/api/repo/list_webhooks.rs index 28e163a..90d63cd 100644 --- a/api/repo/list_webhooks.rs +++ b/api/repo/list_webhooks.rs @@ -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, diff --git a/api/repo/match_protection.rs b/api/repo/match_protection.rs index ecdd6db..0870dbe 100644 --- a/api/repo/match_protection.rs +++ b/api/repo/match_protection.rs @@ -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( diff --git a/api/repo/mod.rs b/api/repo/mod.rs index 0b58765..90a4323 100644 --- a/api/repo/mod.rs +++ b/api/repo/mod.rs @@ -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), diff --git a/api/repo/refresh_stats.rs b/api/repo/refresh_stats.rs index f0606b3..733ad36 100644 --- a/api/repo/refresh_stats.rs +++ b/api/repo/refresh_stats.rs @@ -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, diff --git a/api/repo/remove_member.rs b/api/repo/remove_member.rs index e0a6331..e1b4e22 100644 --- a/api/repo/remove_member.rs +++ b/api/repo/remove_member.rs @@ -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 { 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()))) diff --git a/api/repo/resolve_commit_comment.rs b/api/repo/resolve_commit_comment.rs index 217ebb3..66301f8 100644 --- a/api/repo/resolve_commit_comment.rs +++ b/api/repo/resolve_commit_comment.rs @@ -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 { 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(), + ))) } diff --git a/api/repo/revoke_invitation.rs b/api/repo/revoke_invitation.rs index 8865ac8..9c3882c 100644 --- a/api/repo/revoke_invitation.rs +++ b/api/repo/revoke_invitation.rs @@ -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 { 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(), + ))) } diff --git a/api/repo/set_branch_protection.rs b/api/repo/set_branch_protection.rs index 2253763..d1e0803 100644 --- a/api/repo/set_branch_protection.rs +++ b/api/repo/set_branch_protection.rs @@ -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(), + ))) } diff --git a/api/repo/set_default_branch.rs b/api/repo/set_default_branch.rs index cec6a98..2cffb0b 100644 --- a/api/repo/set_default_branch.rs +++ b/api/repo/set_default_branch.rs @@ -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 { 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(), + ))) } diff --git a/api/repo/star_repo.rs b/api/repo/star_repo.rs index eb69697..4a2e751 100644 --- a/api/repo/star_repo.rs +++ b/api/repo/star_repo.rs @@ -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(), + ))) } diff --git a/api/repo/sync_fork.rs b/api/repo/sync_fork.rs index b9bafd9..7fca55a 100644 --- a/api/repo/sync_fork.rs +++ b/api/repo/sync_fork.rs @@ -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(), + ))) } diff --git a/api/repo/transfer_owner.rs b/api/repo/transfer_owner.rs index d66d1bc..9b63a5b 100644 --- a/api/repo/transfer_owner.rs +++ b/api/repo/transfer_owner.rs @@ -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, diff --git a/api/repo/unarchive.rs b/api/repo/unarchive.rs index b225096..5e42614 100644 --- a/api/repo/unarchive.rs +++ b/api/repo/unarchive.rs @@ -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, diff --git a/api/repo/unstar_repo.rs b/api/repo/unstar_repo.rs index 679bfa4..47ce26e 100644 --- a/api/repo/unstar_repo.rs +++ b/api/repo/unstar_repo.rs @@ -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(), + ))) } diff --git a/api/repo/unwatch_repo.rs b/api/repo/unwatch_repo.rs index ab8014f..2f7fa88 100644 --- a/api/repo/unwatch_repo.rs +++ b/api/repo/unwatch_repo.rs @@ -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(), + ))) } diff --git a/api/repo/update.rs b/api/repo/update.rs index 9399bb3..fa0f2f5 100644 --- a/api/repo/update.rs +++ b/api/repo/update.rs @@ -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( diff --git a/api/repo/update_member_role.rs b/api/repo/update_member_role.rs index 08c02f7..1817b6f 100644 --- a/api/repo/update_member_role.rs +++ b/api/repo/update_member_role.rs @@ -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, diff --git a/api/repo/update_protection_rule.rs b/api/repo/update_protection_rule.rs index d6214fe..dda6636 100644 --- a/api/repo/update_protection_rule.rs +++ b/api/repo/update_protection_rule.rs @@ -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( diff --git a/api/repo/update_release.rs b/api/repo/update_release.rs index dde4324..ca81ea1 100644 --- a/api/repo/update_release.rs +++ b/api/repo/update_release.rs @@ -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( diff --git a/api/repo/update_webhook.rs b/api/repo/update_webhook.rs index 55ebd28..ffae2fa 100644 --- a/api/repo/update_webhook.rs +++ b/api/repo/update_webhook.rs @@ -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( diff --git a/api/repo/watch_repo.rs b/api/repo/watch_repo.rs index f6271d5..c1c2e59 100644 --- a/api/repo/watch_repo.rs +++ b/api/repo/watch_repo.rs @@ -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 { 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(), + ))) } diff --git a/api/routes.rs b/api/routes.rs index f2c9971..44e5917 100644 --- a/api/routes.rs +++ b/api/routes.rs @@ -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)), + ), ); } diff --git a/api/user/add_gpg_key.rs b/api/user/add_gpg_key.rs new file mode 100644 index 0000000..18de753 --- /dev/null +++ b/api/user/add_gpg_key.rs @@ -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), + (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, + session: Session, + params: web::Json, +) -> Result { + let key = service + .user + .user_add_gpg_key(&session, params.into_inner()) + .await?; + Ok(HttpResponse::Created().json(ApiResponse::new(key))) +} diff --git a/api/user/add_ssh_key.rs b/api/user/add_ssh_key.rs new file mode 100644 index 0000000..18355a9 --- /dev/null +++ b/api/user/add_ssh_key.rs @@ -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), + (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, + session: Session, + params: web::Json, +) -> Result { + let key = service + .user + .user_add_ssh_key(&session, params.into_inner()) + .await?; + Ok(HttpResponse::Created().json(ApiResponse::new(key))) +} diff --git a/api/user/delete_account.rs b/api/user/delete_account.rs new file mode 100644 index 0000000..78bf495 --- /dev/null +++ b/api/user/delete_account.rs @@ -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), + (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, + session: Session, +) -> Result { + service.user.user_delete_account(&session).await?; + Ok(HttpResponse::Ok().json(ApiResponse::new("Account deleted successfully".to_string()))) +} diff --git a/api/user/delete_device.rs b/api/user/delete_device.rs new file mode 100644 index 0000000..fe40355 --- /dev/null +++ b/api/user/delete_device.rs @@ -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 { + /// Device ID (UUID) + pub device_id: uuid::Uuid, +} + +/// Delete a user device +/// +/// Removes a registered device from the authenticated user's trusted devices. +/// Requires authentication. +/// +/// Effects: +/// - Device is permanently removed +/// - Device can no longer be used for 2FA bypass +/// - Device will need to be re-registered and verified if needed +/// +/// Returns success message on completion. +#[utoipa::path( + delete, + path = "/api/v1/user/security/devices/{device_id}", + tag = "User", + operation_id = "userDeleteDevice", + params(PathParams), + responses( + (status = 200, description = "Device deleted successfully.", body = ApiResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 404, description = "Device not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn delete_device( + service: web::Data, + session: Session, + path: web::Path, +) -> Result { + service + .user + .user_delete_device(&session, path.device_id) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new("Device deleted successfully".to_string()))) +} diff --git a/api/user/delete_gpg_key.rs b/api/user/delete_gpg_key.rs new file mode 100644 index 0000000..1a98bc7 --- /dev/null +++ b/api/user/delete_gpg_key.rs @@ -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 { + /// GPG key ID (UUID) + pub key_id: uuid::Uuid, +} + +/// Delete a GPG key +/// +/// Revokes a GPG key belonging to the authenticated user. +/// Requires authentication. +/// +/// Effects: +/// - Key is marked as revoked (soft-deleted) +/// - Key can no longer be used for verifying commits/tags +/// - Revoked keys remain visible in key history +/// +/// Returns success message on completion. +#[utoipa::path( + delete, + path = "/api/v1/user/keys/gpg/{key_id}", + tag = "User", + operation_id = "userDeleteGpgKey", + params(PathParams), + responses( + (status = 200, description = "GPG key revoked successfully.", body = ApiResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 404, description = "GPG key not found or already revoked", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn delete_gpg_key( + service: web::Data, + session: Session, + path: web::Path, +) -> Result { + service + .user + .user_delete_gpg_key(&session, path.key_id) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new("GPG key revoked successfully".to_string()))) +} diff --git a/api/user/delete_ssh_key.rs b/api/user/delete_ssh_key.rs new file mode 100644 index 0000000..c7d19c5 --- /dev/null +++ b/api/user/delete_ssh_key.rs @@ -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 { + /// SSH key ID (UUID) + pub key_id: uuid::Uuid, +} + +/// Delete an SSH key +/// +/// Revokes an SSH key belonging to the authenticated user. +/// Requires authentication. +/// +/// Effects: +/// - Key is marked as revoked (soft-deleted) +/// - Key can no longer be used for Git operations +/// - Revoked keys remain visible in key history +/// +/// Returns success message on completion. +#[utoipa::path( + delete, + path = "/api/v1/user/keys/ssh/{key_id}", + tag = "User", + operation_id = "userDeleteSshKey", + params(PathParams), + responses( + (status = 200, description = "SSH key revoked successfully.", body = ApiResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 404, description = "SSH key not found or already revoked", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn delete_ssh_key( + service: web::Data, + session: Session, + path: web::Path, +) -> Result { + service + .user + .user_delete_ssh_key(&session, path.key_id) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new("SSH key revoked successfully".to_string()))) +} diff --git a/api/user/get_account.rs b/api/user/get_account.rs new file mode 100644 index 0000000..50428ad --- /dev/null +++ b/api/user/get_account.rs @@ -0,0 +1,39 @@ +use actix_web::{HttpResponse, web}; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::models::users::User; +use crate::service::AppService; +use crate::session::Session; + +/// Get current user account +/// +/// Returns the authenticated user's account information including: +/// - Username, display name, and bio +/// - Avatar URL +/// - Account status and role +/// - Last login and creation timestamps +/// +/// Requires authentication. +#[utoipa::path( + get, + path = "/api/v1/user/account", + tag = "User", + operation_id = "userGetAccount", + responses( + (status = 200, description = "Account retrieved successfully. Returns user account with all metadata.", body = ApiResponse), + (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 get_account( + service: web::Data, + session: Session, +) -> Result { + let user = service.user.user_account(&session).await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(user))) +} diff --git a/api/user/get_appearance.rs b/api/user/get_appearance.rs new file mode 100644 index 0000000..b3ac388 --- /dev/null +++ b/api/user/get_appearance.rs @@ -0,0 +1,42 @@ +use actix_web::{HttpResponse, web}; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::models::users::UserAppearance; +use crate::service::AppService; +use crate::session::Session; + +/// Get user appearance settings +/// +/// Returns the authenticated user's UI appearance preferences including: +/// - Theme (system, light, dark) +/// - Color scheme (system, light, dark) +/// - Density (compact, comfortable) +/// - Font size (small, medium, large) +/// - Editor theme +/// - Markdown preview toggle +/// - Reduced motion toggle +/// +/// If no settings exist, defaults are created automatically. +/// Requires authentication. +#[utoipa::path( + get, + path = "/api/v1/user/appearance", + tag = "User", + operation_id = "userGetAppearance", + responses( + (status = 200, description = "Appearance settings retrieved successfully. Returns all UI preference settings.", body = ApiResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn get_appearance( + service: web::Data, + session: Session, +) -> Result { + let appearance = service.user.user_appearance(&session).await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(appearance))) +} diff --git a/api/user/get_notifications.rs b/api/user/get_notifications.rs new file mode 100644 index 0000000..4cb87e7 --- /dev/null +++ b/api/user/get_notifications.rs @@ -0,0 +1,42 @@ +use actix_web::{HttpResponse, web}; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::models::users::UserNotifySetting; +use crate::service::AppService; +use crate::session::Session; + +/// Get user notification settings +/// +/// Returns the authenticated user's notification preferences including: +/// - Email notification toggle +/// - Web push notification toggle +/// - Mention notification toggle +/// - Review notification toggle +/// - Security notification toggle +/// - Marketing email toggle +/// - Digest frequency (realtime, daily, weekly, off) +/// +/// If no settings exist, defaults are created automatically. +/// Requires authentication. +#[utoipa::path( + get, + path = "/api/v1/user/notifications", + tag = "User", + operation_id = "userGetNotifications", + responses( + (status = 200, description = "Notification settings retrieved successfully. Returns all notification preferences.", body = ApiResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn get_notifications( + service: web::Data, + session: Session, +) -> Result { + let settings = service.user.user_notify_setting(&session).await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(settings))) +} diff --git a/api/user/get_profile.rs b/api/user/get_profile.rs new file mode 100644 index 0000000..1684bb3 --- /dev/null +++ b/api/user/get_profile.rs @@ -0,0 +1,40 @@ +use actix_web::{HttpResponse, web}; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::models::users::UserProfile; +use crate::service::AppService; +use crate::session::Session; + +/// Get user profile +/// +/// Returns the authenticated user's public profile information including: +/// - Full name and company +/// - Location and website URL +/// - Twitter username +/// - Timezone and language +/// - Profile README +/// +/// If no profile exists, an empty profile is created automatically. +/// Requires authentication. +#[utoipa::path( + get, + path = "/api/v1/user/profile", + tag = "User", + operation_id = "userGetProfile", + responses( + (status = 200, description = "Profile retrieved successfully. Returns all profile fields.", body = ApiResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn get_profile( + service: web::Data, + session: Session, +) -> Result { + let profile = service.user.user_profile(&session).await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(profile))) +} diff --git a/api/user/list_devices.rs b/api/user/list_devices.rs new file mode 100644 index 0000000..690f40c --- /dev/null +++ b/api/user/list_devices.rs @@ -0,0 +1,35 @@ +use actix_web::{HttpResponse, web}; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::models::users::UserDevice; +use crate::service::AppService; +use crate::session::Session; + +/// List user devices +/// +/// Returns all registered devices for the authenticated user. +/// Devices are sorted by last seen time (most recent first). +/// Includes device metadata such as name, type, fingerprint, and trust status. +/// Requires authentication. +#[utoipa::path( + get, + path = "/api/v1/user/security/devices", + tag = "User", + operation_id = "userListDevices", + responses( + (status = 200, description = "Devices listed successfully. Returns array of device objects with metadata.", body = ApiResponse>), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn list_devices( + service: web::Data, + session: Session, +) -> Result { + let devices = service.user.user_devices(&session).await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(devices))) +} diff --git a/api/user/list_gpg_keys.rs b/api/user/list_gpg_keys.rs new file mode 100644 index 0000000..c84476c --- /dev/null +++ b/api/user/list_gpg_keys.rs @@ -0,0 +1,35 @@ +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::session::Session; + +/// List user GPG keys +/// +/// Returns all GPG public keys registered by the authenticated user. +/// Keys are sorted by creation date (newest first). +/// Only non-revoked keys are included. +/// Requires authentication. +#[utoipa::path( + get, + path = "/api/v1/user/keys/gpg", + tag = "User", + operation_id = "userListGpgKeys", + responses( + (status = 200, description = "GPG keys listed successfully. Returns array of GPG key objects with fingerprints and metadata.", body = ApiResponse>), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn list_gpg_keys( + service: web::Data, + session: Session, +) -> Result { + let keys = service.user.user_gpg_keys(&session).await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(keys))) +} diff --git a/api/user/list_oauth_accounts.rs b/api/user/list_oauth_accounts.rs new file mode 100644 index 0000000..48e0a64 --- /dev/null +++ b/api/user/list_oauth_accounts.rs @@ -0,0 +1,35 @@ +use actix_web::{HttpResponse, web}; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::service::AppService; +use crate::service::user::security::UserOAuthInfo; +use crate::session::Session; + +/// List OAuth accounts +/// +/// Returns all linked OAuth/third-party login accounts for the authenticated user. +/// Accounts are sorted by link date (most recent first). +/// Includes provider information, usernames, and token expiry status. +/// Requires authentication. +#[utoipa::path( + get, + path = "/api/v1/user/security/oauth", + tag = "User", + operation_id = "userListOAuthAccounts", + responses( + (status = 200, description = "OAuth accounts listed successfully. Returns array of linked OAuth accounts with provider details.", body = ApiResponse>), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn list_oauth_accounts( + service: web::Data, + session: Session, +) -> Result { + let accounts = service.user.user_oauth_accounts(&session).await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(accounts))) +} diff --git a/api/user/list_personal_access_tokens.rs b/api/user/list_personal_access_tokens.rs new file mode 100644 index 0000000..bc456d4 --- /dev/null +++ b/api/user/list_personal_access_tokens.rs @@ -0,0 +1,55 @@ +use actix_web::{HttpResponse, web}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::service::AppService; +use crate::service::user::security::UserPersonalAccessTokenInfo; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct QueryParams { + /// Maximum number of tokens to return (default: 50, max: 100) + pub limit: Option, + /// Number of tokens to skip for pagination (default: 0) + pub offset: Option, +} + +/// List personal access tokens +/// +/// Returns a paginated list of all personal access tokens (PATs) for the authenticated user. +/// Tokens are sorted by creation date (newest first). +/// Includes token names, scopes, last used timestamps, and expiry status. +/// Note: Token values are never returned after creation for security reasons. +/// Requires authentication. +#[utoipa::path( + get, + path = "/api/v1/user/security/tokens", + tag = "User", + operation_id = "userListTokens", + params(QueryParams), + responses( + (status = 200, description = "Personal access tokens listed successfully. Returns array of token metadata objects (token values are never exposed).", body = ApiResponse>), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn list_tokens( + service: web::Data, + session: Session, + query: web::Query, +) -> Result { + let tokens = service + .user + .user_personal_access_tokens( + &session, + query.limit.unwrap_or(50), + query.offset.unwrap_or(0), + ) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(tokens))) +} diff --git a/api/user/list_security_logs.rs b/api/user/list_security_logs.rs new file mode 100644 index 0000000..52d91bf --- /dev/null +++ b/api/user/list_security_logs.rs @@ -0,0 +1,55 @@ +use actix_web::{HttpResponse, web}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::models::users::UserSecurityLog; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct QueryParams { + /// Maximum number of log entries to return (default: 50, max: 100) + pub limit: Option, + /// Number of log entries to skip for pagination (default: 0) + pub offset: Option, +} + +/// List security logs +/// +/// Returns a paginated list of security events for the authenticated user. +/// Entries are sorted by creation date (newest first). +/// Includes event types, descriptions, IP addresses, and user agents. +/// Useful for auditing account activity and detecting suspicious behavior. +/// Requires authentication. +#[utoipa::path( + get, + path = "/api/v1/user/security/logs", + tag = "User", + operation_id = "userListSecurityLogs", + params(QueryParams), + responses( + (status = 200, description = "Security logs listed successfully. Returns array of security event entries with metadata.", body = ApiResponse>), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn list_security_logs( + service: web::Data, + session: Session, + query: web::Query, +) -> Result { + let logs = service + .user + .user_security_logs( + &session, + query.limit.unwrap_or(50), + query.offset.unwrap_or(0), + ) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(logs))) +} diff --git a/api/user/list_sessions.rs b/api/user/list_sessions.rs new file mode 100644 index 0000000..9ce04e0 --- /dev/null +++ b/api/user/list_sessions.rs @@ -0,0 +1,54 @@ +use actix_web::{HttpResponse, web}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::service::AppService; +use crate::service::user::security::UserSessionInfo; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct QueryParams { + /// Maximum number of sessions to return (default: 50, max: 100) + pub limit: Option, + /// Number of sessions to skip for pagination (default: 0) + pub offset: Option, +} + +/// List user sessions +/// +/// Returns a paginated list of all active and recently-expired sessions for the authenticated user. +/// Sessions are sorted by last activity (most recent first). +/// Includes session metadata such as IP address, user agent, and expiration time. +/// Requires authentication. +#[utoipa::path( + get, + path = "/api/v1/user/security/sessions", + tag = "User", + operation_id = "userListSessions", + params(QueryParams), + responses( + (status = 200, description = "Sessions listed successfully. Returns array of session objects with metadata.", body = ApiResponse>), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn list_sessions( + service: web::Data, + session: Session, + query: web::Query, +) -> Result { + let sessions = service + .user + .user_sessions( + &session, + query.limit.unwrap_or(50), + query.offset.unwrap_or(0), + ) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(sessions))) +} diff --git a/api/user/list_ssh_keys.rs b/api/user/list_ssh_keys.rs new file mode 100644 index 0000000..23c1bea --- /dev/null +++ b/api/user/list_ssh_keys.rs @@ -0,0 +1,35 @@ +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::session::Session; + +/// List user SSH keys +/// +/// Returns all SSH public keys registered by the authenticated user. +/// Keys are sorted by creation date (newest first). +/// Only non-revoked keys are included. +/// Requires authentication. +#[utoipa::path( + get, + path = "/api/v1/user/keys/ssh", + tag = "User", + operation_id = "userListSshKeys", + responses( + (status = 200, description = "SSH keys listed successfully. Returns array of SSH key objects with fingerprints and metadata.", body = ApiResponse>), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn list_ssh_keys( + service: web::Data, + session: Session, +) -> Result { + let keys = service.user.user_ssh_keys(&session).await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(keys))) +} diff --git a/api/user/mod.rs b/api/user/mod.rs new file mode 100644 index 0000000..ce8e957 --- /dev/null +++ b/api/user/mod.rs @@ -0,0 +1,114 @@ +pub mod add_gpg_key; +pub mod add_ssh_key; +pub mod delete_account; +pub mod delete_device; +pub mod delete_gpg_key; +pub mod delete_ssh_key; +pub mod get_account; +pub mod get_appearance; +pub mod get_notifications; +pub mod get_profile; +pub mod list_devices; +pub mod list_gpg_keys; +pub mod list_oauth_accounts; +pub mod list_personal_access_tokens; +pub mod list_security_logs; +pub mod list_sessions; +pub mod list_ssh_keys; +pub mod revoke_personal_access_token; +pub mod revoke_session; +pub mod unlink_oauth; +pub mod update_account; +pub mod update_appearance; +pub mod update_notifications; +pub mod update_profile; +pub mod upload_avatar; + +use actix_web::web; + +pub fn configure(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/user") + // Account + .route("/account", web::get().to(get_account::get_account)) + .route("/account", web::put().to(update_account::update_account)) + .route( + "/account/avatar", + web::post().to(upload_avatar::upload_avatar), + ) + .route("/account", web::delete().to(delete_account::delete_account)) + // Appearance + .route("/appearance", web::get().to(get_appearance::get_appearance)) + .route( + "/appearance", + web::put().to(update_appearance::update_appearance), + ) + // Profile + .route("/profile", web::get().to(get_profile::get_profile)) + .route("/profile", web::put().to(update_profile::update_profile)) + // Notifications + .route( + "/notifications", + web::get().to(get_notifications::get_notifications), + ) + .route( + "/notifications", + web::put().to(update_notifications::update_notifications), + ) + // SSH Keys + .route("/keys/ssh", web::get().to(list_ssh_keys::list_ssh_keys)) + .route("/keys/ssh", web::post().to(add_ssh_key::add_ssh_key)) + .route( + "/keys/ssh/{key_id}", + web::delete().to(delete_ssh_key::delete_ssh_key), + ) + // GPG Keys + .route("/keys/gpg", web::get().to(list_gpg_keys::list_gpg_keys)) + .route("/keys/gpg", web::post().to(add_gpg_key::add_gpg_key)) + .route( + "/keys/gpg/{key_id}", + web::delete().to(delete_gpg_key::delete_gpg_key), + ) + // Security - Sessions + .route( + "/security/sessions", + web::get().to(list_sessions::list_sessions), + ) + .route( + "/security/sessions/{session_id}", + web::delete().to(revoke_session::revoke_session), + ) + // Security - Devices + .route( + "/security/devices", + web::get().to(list_devices::list_devices), + ) + .route( + "/security/devices/{device_id}", + web::delete().to(delete_device::delete_device), + ) + // Security - OAuth + .route( + "/security/oauth", + web::get().to(list_oauth_accounts::list_oauth_accounts), + ) + .route( + "/security/oauth/{oauth_id}", + web::delete().to(unlink_oauth::unlink_oauth), + ) + // Security - Logs + .route( + "/security/logs", + web::get().to(list_security_logs::list_security_logs), + ) + // Security - Personal Access Tokens + .route( + "/security/tokens", + web::get().to(list_personal_access_tokens::list_tokens), + ) + .route( + "/security/tokens/{token_id}", + web::delete().to(revoke_personal_access_token::revoke_token), + ), + ); +} diff --git a/api/user/revoke_personal_access_token.rs b/api/user/revoke_personal_access_token.rs new file mode 100644 index 0000000..a6bc781 --- /dev/null +++ b/api/user/revoke_personal_access_token.rs @@ -0,0 +1,55 @@ +use actix_web::{HttpResponse, web}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// Token ID (UUID) + pub token_id: uuid::Uuid, +} + +/// Revoke a personal access token +/// +/// Immediately revokes a personal access token belonging to the authenticated user. +/// Requires authentication. +/// +/// Effects: +/// - Token is marked as revoked and can no longer be used +/// - All API calls using this token will fail with 401 Unauthorized +/// - Revoked tokens remain visible in token list for audit purposes +/// +/// Returns success message on completion. +#[utoipa::path( + delete, + path = "/api/v1/user/security/tokens/{token_id}", + tag = "User", + operation_id = "userRevokeToken", + params(PathParams), + responses( + (status = 200, description = "Personal access token revoked successfully.", body = ApiResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 404, description = "Token not found or already revoked", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn revoke_token( + service: web::Data, + session: Session, + path: web::Path, +) -> Result { + service + .user + .user_revoke_personal_access_token(&session, path.token_id) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new( + "Personal access token revoked successfully".to_string(), + ))) +} diff --git a/api/user/revoke_session.rs b/api/user/revoke_session.rs new file mode 100644 index 0000000..6335f82 --- /dev/null +++ b/api/user/revoke_session.rs @@ -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 { + /// Session ID (UUID) + pub session_id: uuid::Uuid, +} + +/// Revoke a user session +/// +/// Immediately terminates a specific session belonging to the authenticated user. +/// Requires authentication. +/// +/// Effects: +/// - Session is marked as revoked +/// - Session can no longer be used for authentication +/// - Active connections using this session are disconnected +/// +/// Returns success message on completion. +#[utoipa::path( + delete, + path = "/api/v1/user/security/sessions/{session_id}", + tag = "User", + operation_id = "userRevokeSession", + params(PathParams), + responses( + (status = 200, description = "Session revoked successfully.", body = ApiResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 404, description = "Session not found or already revoked", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn revoke_session( + service: web::Data, + session: Session, + path: web::Path, +) -> Result { + service + .user + .user_revoke_session(&session, path.session_id) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new("Session revoked successfully".to_string()))) +} diff --git a/api/user/unlink_oauth.rs b/api/user/unlink_oauth.rs new file mode 100644 index 0000000..7553fca --- /dev/null +++ b/api/user/unlink_oauth.rs @@ -0,0 +1,58 @@ +use actix_web::{HttpResponse, web}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// OAuth account ID (UUID) + pub oauth_id: uuid::Uuid, +} + +/// Unlink an OAuth account +/// +/// Removes a linked OAuth/third-party login account from the authenticated user. +/// Requires authentication. +/// +/// Preconditions: +/// - User must have at least one remaining login method (password or another OAuth account) +/// +/// Effects: +/// - OAuth account link is permanently removed +/// - User can no longer log in with this OAuth provider unless re-linked +/// +/// Returns success message on completion. +#[utoipa::path( + delete, + path = "/api/v1/user/security/oauth/{oauth_id}", + tag = "User", + operation_id = "userUnlinkOAuth", + params(PathParams), + responses( + (status = 200, description = "OAuth account unlinked successfully.", body = ApiResponse), + (status = 400, description = "Cannot unlink: this is the last login method (set a password first)", body = ApiErrorResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 404, description = "OAuth account not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn unlink_oauth( + service: web::Data, + session: Session, + path: web::Path, +) -> Result { + service + .user + .user_unlink_oauth(&session, path.oauth_id) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new( + "OAuth account unlinked successfully".to_string(), + ))) +} diff --git a/api/user/update_account.rs b/api/user/update_account.rs new file mode 100644 index 0000000..5312f14 --- /dev/null +++ b/api/user/update_account.rs @@ -0,0 +1,55 @@ +use actix_web::{HttpResponse, web}; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::models::users::User; +use crate::service::AppService; +use crate::service::user::account::UpdateUserAccountParams; +use crate::session::Session; + +/// Update user account +/// +/// Updates the authenticated user's account settings. +/// Requires authentication. +/// +/// Updatable fields: +/// - username: New username (must be unique across the platform) +/// - display_name: Human-readable display name +/// - bio: Short biography text +/// - visibility: Profile visibility ("public", "private", or "internal") +/// +/// All fields are optional; only provided fields are updated. +/// Returns the updated user account with all metadata. +#[utoipa::path( + put, + path = "/api/v1/user/account", + tag = "User", + operation_id = "userUpdateAccount", + request_body( + content = UpdateUserAccountParams, + description = "Account update parameters (all fields optional)", + content_type = "application/json" + ), + responses( + (status = 200, description = "Account updated successfully. Returns updated user account with all metadata.", body = ApiResponse), + (status = 400, description = "Invalid parameters: empty username or unsupported visibility value", body = ApiErrorResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 404, description = "User not found", body = ApiErrorResponse), + (status = 409, description = "Username already taken", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn update_account( + service: web::Data, + session: Session, + params: web::Json, +) -> Result { + let user = service + .user + .user_update_account(&session, params.into_inner()) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(user))) +} diff --git a/api/user/update_appearance.rs b/api/user/update_appearance.rs new file mode 100644 index 0000000..e9d5484 --- /dev/null +++ b/api/user/update_appearance.rs @@ -0,0 +1,56 @@ +use actix_web::{HttpResponse, web}; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::models::users::UserAppearance; +use crate::service::AppService; +use crate::service::user::appearance::UpdateUserAppearanceParams; +use crate::session::Session; + +/// Update user appearance settings +/// +/// Updates the authenticated user's UI appearance preferences. +/// Requires authentication. +/// +/// Updatable fields: +/// - theme: UI theme ("system", "light", "dark") +/// - color_scheme: Color scheme ("system", "light", "dark") +/// - density: UI density ("compact", "comfortable") +/// - font_size: Font size ("small", "medium", "large") +/// - editor_theme: Code editor theme name +/// - markdown_preview: Enable/disable markdown live preview +/// - reduced_motion: Enable/disable reduced motion +/// +/// All fields are optional; only provided fields are updated. +/// Returns the updated appearance settings. +#[utoipa::path( + put, + path = "/api/v1/user/appearance", + tag = "User", + operation_id = "userUpdateAppearance", + request_body( + content = UpdateUserAppearanceParams, + description = "Appearance update parameters (all fields optional)", + content_type = "application/json" + ), + responses( + (status = 200, description = "Appearance settings updated successfully. Returns all updated UI preferences.", body = ApiResponse), + (status = 400, description = "Invalid parameters: unsupported theme, color scheme, density, or font size", body = ApiErrorResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn update_appearance( + service: web::Data, + session: Session, + params: web::Json, +) -> Result { + let appearance = service + .user + .user_update_appearance(&session, params.into_inner()) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(appearance))) +} diff --git a/api/user/update_notifications.rs b/api/user/update_notifications.rs new file mode 100644 index 0000000..1838202 --- /dev/null +++ b/api/user/update_notifications.rs @@ -0,0 +1,56 @@ +use actix_web::{HttpResponse, web}; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::models::users::UserNotifySetting; +use crate::service::AppService; +use crate::service::user::notify::UpdateUserNotifySettingParams; +use crate::session::Session; + +/// Update user notification settings +/// +/// Updates the authenticated user's notification preferences. +/// Requires authentication. +/// +/// Updatable fields: +/// - email_notifications: Enable/disable email notifications +/// - web_notifications: Enable/disable web push notifications +/// - mention_notifications: Enable/disable @mention notifications +/// - review_notifications: Enable/disable code review notifications +/// - security_notifications: Enable/disable security notifications +/// - marketing_emails: Enable/disable marketing emails +/// - digest_frequency: Digest email frequency ("realtime", "daily", "weekly", "off") +/// +/// All fields are optional; only provided fields are updated. +/// Returns the updated notification settings. +#[utoipa::path( + put, + path = "/api/v1/user/notifications", + tag = "User", + operation_id = "userUpdateNotifications", + request_body( + content = UpdateUserNotifySettingParams, + description = "Notification settings update parameters (all fields optional)", + content_type = "application/json" + ), + responses( + (status = 200, description = "Notification settings updated successfully. Returns all updated preferences.", body = ApiResponse), + (status = 400, description = "Invalid parameters: unsupported digest frequency", body = ApiErrorResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn update_notifications( + service: web::Data, + session: Session, + params: web::Json, +) -> Result { + let settings = service + .user + .user_update_notify_setting(&session, params.into_inner()) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(settings))) +} diff --git a/api/user/update_profile.rs b/api/user/update_profile.rs new file mode 100644 index 0000000..07b7dd1 --- /dev/null +++ b/api/user/update_profile.rs @@ -0,0 +1,57 @@ +use actix_web::{HttpResponse, web}; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::models::users::UserProfile; +use crate::service::AppService; +use crate::service::user::profile::UpdateUserProfileParams; +use crate::session::Session; + +/// Update user profile +/// +/// Updates the authenticated user's public profile information. +/// Requires authentication. +/// +/// Updatable fields: +/// - full_name: Full legal name or display name +/// - company: Organization or company name +/// - location: Geographic location (e.g., "San Francisco, CA") +/// - website_url: Personal or company website URL +/// - twitter_username: Twitter/X handle +/// - timezone: IANA timezone identifier (e.g., "America/New_York") +/// - language: Preferred language code (e.g., "en", "zh-CN") +/// - profile_readme: Markdown content for profile README +/// +/// All fields are optional; only provided fields are updated. +/// Returns the updated profile with all fields. +#[utoipa::path( + put, + path = "/api/v1/user/profile", + tag = "User", + operation_id = "userUpdateProfile", + request_body( + content = UpdateUserProfileParams, + description = "Profile update parameters (all fields optional)", + content_type = "application/json" + ), + responses( + (status = 200, description = "Profile updated successfully. Returns all updated profile fields.", body = ApiResponse), + (status = 400, description = "Invalid parameters", body = ApiErrorResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn update_profile( + service: web::Data, + session: Session, + params: web::Json, +) -> Result { + let profile = service + .user + .user_update_profile(&session, params.into_inner()) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(profile))) +} diff --git a/api/user/upload_avatar.rs b/api/user/upload_avatar.rs new file mode 100644 index 0000000..93ea950 --- /dev/null +++ b/api/user/upload_avatar.rs @@ -0,0 +1,56 @@ +use actix_web::{HttpResponse, web}; + +use crate::api::response::{ApiErrorResponse, ApiResponse}; +use crate::error::AppError; +use crate::service::AppService; +use crate::service::user::account::{UploadUserAvatarParams, UserAvatarResponse}; +use crate::session::Session; + +/// Upload user avatar +/// +/// Uploads a new avatar image for the authenticated user. +/// Requires authentication. +/// +/// Parameters: +/// - data: Raw avatar image bytes (PNG, JPEG, or WebP, max 5MB) +/// - content_type: MIME type of the image (e.g., "image/png") +/// - file_name: Original file name (used to infer file extension) +/// +/// Effects: +/// - Avatar image is stored in S3-compatible object storage +/// - Previous avatar is deleted from storage +/// - User's avatar URL is updated +/// +/// Returns the new avatar URL and storage key. +#[utoipa::path( + post, + path = "/api/v1/user/account/avatar", + tag = "User", + operation_id = "userUploadAvatar", + request_body( + content = UploadUserAvatarParams, + description = "Avatar upload parameters", + content_type = "application/json" + ), + responses( + (status = 200, description = "Avatar uploaded successfully. Returns the new avatar URL and storage key.", body = ApiResponse), + (status = 400, description = "Invalid parameters: unsupported file type or image too large", 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 or S3 storage failure", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn upload_avatar( + service: web::Data, + session: Session, + params: web::Json, +) -> Result { + let response = service + .user + .user_upload_avatar(&session, params.into_inner()) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(response))) +} diff --git a/models/issues/issue.rs b/models/issues/issue.rs index ab20d3b..4b9e76d 100644 --- a/models/issues/issue.rs +++ b/models/issues/issue.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct Issue { pub id: Uuid, pub workspace_id: Uuid, diff --git a/models/issues/issue_assignees.rs b/models/issues/issue_assignees.rs index cc3d477..26fde3b 100644 --- a/models/issues/issue_assignees.rs +++ b/models/issues/issue_assignees.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct IssueAssignee { pub id: Uuid, pub issue_id: Uuid, diff --git a/models/issues/issue_comments.rs b/models/issues/issue_comments.rs index a11fd9d..589281d 100644 --- a/models/issues/issue_comments.rs +++ b/models/issues/issue_comments.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct IssueComment { pub id: Uuid, pub issue_id: Uuid, diff --git a/models/issues/issue_events.rs b/models/issues/issue_events.rs index 8433384..e15b686 100644 --- a/models/issues/issue_events.rs +++ b/models/issues/issue_events.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct IssueEvent { pub id: Uuid, pub issue_id: Uuid, diff --git a/models/issues/issue_label_relations.rs b/models/issues/issue_label_relations.rs index 856074f..0ad066e 100644 --- a/models/issues/issue_label_relations.rs +++ b/models/issues/issue_label_relations.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct IssueLabelRelation { pub id: Uuid, pub issue_id: Uuid, diff --git a/models/issues/issue_labels.rs b/models/issues/issue_labels.rs index 46db829..d51f9b6 100644 --- a/models/issues/issue_labels.rs +++ b/models/issues/issue_labels.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct IssueLabel { pub id: Uuid, pub repo_id: Uuid, diff --git a/models/issues/issue_milestones.rs b/models/issues/issue_milestones.rs index ba69fef..c7cf570 100644 --- a/models/issues/issue_milestones.rs +++ b/models/issues/issue_milestones.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct IssueMilestone { pub id: Uuid, pub repo_id: Uuid, diff --git a/models/issues/issue_pr_relations.rs b/models/issues/issue_pr_relations.rs index 8807e5d..fb8394e 100644 --- a/models/issues/issue_pr_relations.rs +++ b/models/issues/issue_pr_relations.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct IssuePrRelation { pub id: Uuid, pub issue_id: Uuid, diff --git a/models/issues/issue_reaction.rs b/models/issues/issue_reaction.rs index 3684d7f..874322c 100644 --- a/models/issues/issue_reaction.rs +++ b/models/issues/issue_reaction.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct IssueReaction { pub id: Uuid, pub issue_id: Uuid, diff --git a/models/issues/issue_repo_relations.rs b/models/issues/issue_repo_relations.rs index eaf865f..c28e995 100644 --- a/models/issues/issue_repo_relations.rs +++ b/models/issues/issue_repo_relations.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct IssueRepoRelation { pub id: Uuid, pub issue_id: Uuid, diff --git a/models/issues/issue_subscribers.rs b/models/issues/issue_subscribers.rs index f066756..f5cb162 100644 --- a/models/issues/issue_subscribers.rs +++ b/models/issues/issue_subscribers.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct IssueSubscriber { pub id: Uuid, pub issue_id: Uuid, diff --git a/models/issues/issue_templates.rs b/models/issues/issue_templates.rs index c4b72e1..eb3b70f 100644 --- a/models/issues/issue_templates.rs +++ b/models/issues/issue_templates.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct IssueTemplate { pub id: Uuid, pub repo_id: Uuid, diff --git a/models/users/user.rs b/models/users/user.rs index 97f0a2b..9b6be71 100644 --- a/models/users/user.rs +++ b/models/users/user.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct User { pub id: Uuid, pub username: String, diff --git a/models/users/user_appearance.rs b/models/users/user_appearance.rs index a2246a9..104e071 100644 --- a/models/users/user_appearance.rs +++ b/models/users/user_appearance.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct UserAppearance { pub user_id: Uuid, pub theme: Theme, diff --git a/models/users/user_device.rs b/models/users/user_device.rs index ad9c33f..17bb2c7 100644 --- a/models/users/user_device.rs +++ b/models/users/user_device.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct UserDevice { pub id: Uuid, pub user_id: Uuid, diff --git a/models/users/user_gpg_key.rs b/models/users/user_gpg_key.rs index 178a4f6..bd7d680 100644 --- a/models/users/user_gpg_key.rs +++ b/models/users/user_gpg_key.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct UserGpgKey { pub id: Uuid, pub user_id: Uuid, diff --git a/models/users/user_notify_setting.rs b/models/users/user_notify_setting.rs index 9b8c040..81e0b2e 100644 --- a/models/users/user_notify_setting.rs +++ b/models/users/user_notify_setting.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct UserNotifySetting { pub user_id: Uuid, pub email_notifications: bool, diff --git a/models/users/user_profile.rs b/models/users/user_profile.rs index 0e7d01e..b5d13dc 100644 --- a/models/users/user_profile.rs +++ b/models/users/user_profile.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct UserProfile { pub user_id: Uuid, pub full_name: Option, diff --git a/models/users/user_security_log.rs b/models/users/user_security_log.rs index afd5b31..d2599c3 100644 --- a/models/users/user_security_log.rs +++ b/models/users/user_security_log.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct UserSecurityLog { pub id: Uuid, pub user_id: Uuid, diff --git a/models/users/user_ssh_key.rs b/models/users/user_ssh_key.rs index 98d888c..3142a91 100644 --- a/models/users/user_ssh_key.rs +++ b/models/users/user_ssh_key.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct UserSshKey { pub id: Uuid, pub user_id: Uuid,