Compare commits
47 Commits
4586b79cb8
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c6f99fff47 | |||
| 96b391ff2d | |||
| 0e13f90834 | |||
| 6f40921576 | |||
| 44fe5a519b | |||
| 70f2f7d63d | |||
| 10a4398e81 | |||
| e386f44ee2 | |||
| 293102e5f2 | |||
| 934858bebf | |||
| a40da90ef9 | |||
| c32a7cad2f | |||
| 0207cde234 | |||
| 2dd384f7be | |||
| 0782a9fe6d | |||
| 1dca8b3b78 | |||
| 49c1675e01 | |||
| bcd750b905 | |||
| c8729d38bc | |||
| c3017a255f | |||
| c9c1a739fd | |||
| 45c00b2dee | |||
| e582b269f1 | |||
| 0665772079 | |||
| 1c22700769 | |||
| e4080dcbc7 | |||
| 939931acad | |||
| 9a0c26e5f6 | |||
| ab32e8826e | |||
| eeb4d9f902 | |||
| c2487ec0b6 | |||
| f5044fb099 | |||
| 66afd932ed | |||
| 8f472a0443 | |||
| d243dce027 | |||
| 8c95eb230d | |||
| 5c99b27421 | |||
| 5b740eecd7 | |||
| a815f63927 | |||
| 7631e57f69 | |||
| cc202d6d1f | |||
| 729604f13b | |||
| 4a87ea475d | |||
| f0a443932a | |||
| 998f393ed0 | |||
| 737e934043 | |||
| dcb0fb74c5 |
@@ -0,0 +1,7 @@
|
||||
.codegraph
|
||||
target
|
||||
.git
|
||||
.idea
|
||||
*.md
|
||||
LICENSE
|
||||
.env
|
||||
@@ -0,0 +1,23 @@
|
||||
REPO_PREFIX_PATH=/home/zhenyi/RustroverProjects/gitks/data
|
||||
GITKS_HOST=0.0.0.0
|
||||
GITKS_PORT=50051
|
||||
GITKS_ADVERTISE_ADDR=http://gitks-node1:50051
|
||||
GITKS_METRICS_PORT=9100
|
||||
GITKS_DISK_CACHE_ENABLED=false
|
||||
GITKS_DISK_CACHE_MAX_AGE=300
|
||||
GITKS_PACK_CACHE_ENABLED=true
|
||||
GITKS_PACK_CACHE_BACKPRESSURE=true
|
||||
GITKS_RATE_LIMIT_MAX_CONCURRENT=100
|
||||
GITKS_HOOKS_ENABLED=true
|
||||
GITKS_HOOK_TIMEOUT=30
|
||||
GITKS_ALLOW_CUSTOM_HOOKS=true
|
||||
#GITKS_SERVER_HOOKS_DIR=/etc/gitks/hooks
|
||||
GITKS_HOOK_CALLBACK_ADDR=http://localhost:50052
|
||||
GITKS_ETCD_ENDPOINTS=http://localhost:2379
|
||||
GITKS_CLUSTER_PORT=4697
|
||||
GITKS_CLUSTER_COOKIE=gitks-default-cookie
|
||||
GITKS_LEASE_TTL=15
|
||||
GITKS_ETCD_CONNECT_TIMEOUT=5000
|
||||
GITKS_HEALTH_CHECK_INTERVAL=1
|
||||
GITKS_MAX_HEALTH_FAILURES=10
|
||||
STORAGE_NAME=default
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
/target
|
||||
.idea
|
||||
.codegraph
|
||||
.claude
|
||||
.env*
|
||||
!.env.example
|
||||
AGENT.md
|
||||
CLAUDE.md
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
.DS_Store
|
||||
.env
|
||||
|
||||
Generated
-10
@@ -1,10 +0,0 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# 已忽略包含查询文件的默认文件夹
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@@ -0,0 +1,161 @@
|
||||
# AGENTS.md — Development Guidelines
|
||||
|
||||
> Unified development guidelines for all AI coding assistants (Claude Code, Cursor, etc.)
|
||||
|
||||
**Last Updated**: 2026-06-11
|
||||
|
||||
---
|
||||
|
||||
## 1. Language
|
||||
|
||||
Always respond in **Chinese (中文)**. Code, commands, and technical terms remain in English.
|
||||
|
||||
---
|
||||
|
||||
## 2. Code Style
|
||||
|
||||
### 2.1 Basic Principles
|
||||
|
||||
- Follow existing project conventions
|
||||
- Use meaningful variable names
|
||||
- Keep functions under **50 lines**
|
||||
- Maximum nesting depth: **3 levels**
|
||||
- Add comments for complex logic only
|
||||
|
||||
### 2.2 Rust Best Practices
|
||||
|
||||
- Use `?` operator; never use `unwrap()` in non-test code
|
||||
- Avoid `unsafe`; if necessary, add `// SAFETY:` comment
|
||||
- Minimize `clone()`; prefer references
|
||||
- No magic numbers; use named constants
|
||||
- No hardcoded strings; use enums or constants
|
||||
|
||||
### 2.3 Import Order
|
||||
|
||||
```rust
|
||||
// std → third-party crates → local modules
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Forbidden Patterns
|
||||
|
||||
- `// ── xxxx ──────────` — no divider comments
|
||||
- `unwrap()` / `expect()` in non-test code — use `?` instead
|
||||
- `panic!()` / `unreachable!()` — use error types instead
|
||||
- Untracked `todo!()` — must have a corresponding issue
|
||||
- Commented-out code — use Git history instead
|
||||
- Nesting depth ≥ 4 — flatten with early return
|
||||
- Functions > 50 lines — split into smaller functions
|
||||
- Magic numbers — define named `const`
|
||||
- Hardcoded strings — use enums or constants
|
||||
|
||||
---
|
||||
|
||||
## 4. Error Handling
|
||||
|
||||
### 4.1 Principles
|
||||
|
||||
- Handle all errors explicitly; no silent failures
|
||||
- Log internal errors; keep user-facing messages helpful
|
||||
- Add context with `.context()` or `.map_err()`
|
||||
|
||||
### 4.2 Log Format
|
||||
|
||||
```rust
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
operation = "operation_name",
|
||||
"Failed to perform operation"
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Security
|
||||
|
||||
- Never hardcode secrets or API keys
|
||||
- Always validate and sanitize user input
|
||||
- Use parameterized queries (no SQL injection)
|
||||
- Use proper password hashing (Argon2, bcrypt)
|
||||
|
||||
---
|
||||
|
||||
## 6. Workflow
|
||||
|
||||
### 6.1 Development Flow
|
||||
|
||||
1. **Read before write** — understand context first
|
||||
2. **Minimal changes** — don't refactor unrelated code
|
||||
3. **Verify after changes** — run tests or check output
|
||||
|
||||
### 6.2 AI Assistant Rules
|
||||
|
||||
- Always read existing code before making changes
|
||||
- Make minimal changes
|
||||
- Run `cargo check` or `cargo test` after changes
|
||||
- Explain what you changed and why
|
||||
|
||||
### 6.3 Common Commands
|
||||
|
||||
```bash
|
||||
cargo build # Build
|
||||
cargo check # Quick syntax check
|
||||
cargo test # Run tests
|
||||
cargo clippy # Lint
|
||||
cargo fmt # Format
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Git Workflow
|
||||
|
||||
### 7.1 Commit Message Format
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
Types: `feat` · `fix` · `refactor` · `docs` · `test` · `chore`
|
||||
|
||||
### 7.2 Commit Principles
|
||||
|
||||
- Each commit addresses one concern (atomic)
|
||||
- Each commit leaves the codebase in a working state
|
||||
- Never force push to `main`
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Architecture Overview
|
||||
|
||||
```
|
||||
gitks — Git Repository Operations Service
|
||||
|
||||
actor/ → Actor model
|
||||
archive/ → Archive operations
|
||||
blame/ → Blame operations
|
||||
blob/ → Blob objects
|
||||
branch/ → Branch operations
|
||||
commit/ → Commit operations
|
||||
diff/ → Diff operations
|
||||
merge/ → Merge operations
|
||||
pack/ → Pack operations
|
||||
refs/ → Reference management
|
||||
remote/ → Remote operations
|
||||
repository/ → Repository operations
|
||||
server/ → gRPC server
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*For questions or suggestions, please open an issue.*
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Gitks Security Best Practices
|
||||
|
||||
This document outlines security best practices for the gitks project.
|
||||
|
||||
## Input Validation
|
||||
|
||||
### Revision Strings
|
||||
All revision strings (branch names, commit hashes, refs) are validated using `sanitize::validate_revision()`:
|
||||
- Prevents command injection via `~N` and `^N` operators
|
||||
- Limits revision string length to 256 characters
|
||||
- Limits ancestry depth to 10000 to prevent DoS attacks
|
||||
- Validates branch name characters to prevent shell metacharacter injection
|
||||
|
||||
### File Paths
|
||||
File paths are validated using `sanitize::validate_file_path()`:
|
||||
- Rejects absolute paths
|
||||
- Blocks path traversal attacks (`..`)
|
||||
- Prevents null byte injection
|
||||
- Blocks `.git` directory access
|
||||
- On Windows, blocks reserved device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
|
||||
|
||||
### Git Configuration Keys
|
||||
Configuration keys are validated using `sanitize::validate_config_key()`:
|
||||
- Blocks dangerous keys that could execute arbitrary commands (core.sshCommand, core.hooksPath)
|
||||
- Blocks network-related keys (http.proxy, https.proxy, remote.*.url)
|
||||
- Blocks credential helpers
|
||||
- Only allows alphanumeric characters, dots, hyphens, and underscores
|
||||
|
||||
### Relative Paths
|
||||
Relative paths are validated using `sanitize::validate_relative_path()`:
|
||||
- Rejects absolute paths
|
||||
- Blocks path traversal attacks (`..`)
|
||||
|
||||
## Path Security
|
||||
|
||||
### TOCTOU Prevention
|
||||
Path validation uses a unified approach to prevent Time-Of-Check-Time-Of-Use vulnerabilities:
|
||||
1. Canonicalize the path if it exists
|
||||
2. If path doesn't exist, validate parent directory and filename separately
|
||||
3. Verify canonical path starts with allowed prefix
|
||||
4. Reject any path that escapes the allowed directory
|
||||
|
||||
### Cache Invalidation
|
||||
Cache entries are invalidated when repositories are modified:
|
||||
- Uses precise substring matching on relative path
|
||||
- Invalidates all cache keys containing the modified repository path
|
||||
- Prevents stale data from being served after modifications
|
||||
|
||||
## Message Decoding Security
|
||||
|
||||
### String Decoding
|
||||
The `decode_strings()` function in `actor/message.rs` includes:
|
||||
- Total message size limit (50MB)
|
||||
- Individual string length limit (10MB)
|
||||
- Overflow protection using `checked_add()`
|
||||
- Graceful degradation on malformed data
|
||||
|
||||
## Cluster Registration
|
||||
|
||||
### Primary/Replica Role Assignment
|
||||
When registering repositories in a cluster:
|
||||
- Single node: registers as PRIMARY
|
||||
- Multiple nodes: registers as REPLICA initially
|
||||
- Final role determination happens at query time via `route_repository`
|
||||
- This conservative approach prevents split-brain scenarios
|
||||
|
||||
## Testing
|
||||
|
||||
All security-critical functions have comprehensive unit tests:
|
||||
- `tests/sanitize_test.rs`: Input validation tests
|
||||
- `tests/macro_test.rs`: Revision resolution tests
|
||||
- Tests cover both valid and malicious inputs
|
||||
|
||||
## Code Quality
|
||||
|
||||
- All code passes `cargo clippy --all-targets --all-features` with zero warnings
|
||||
- Code is formatted with `cargo fmt`
|
||||
- All tests pass with `cargo test`
|
||||
- No known security vulnerabilities in dependencies (verified with `cargo deny`)
|
||||
|
||||
## Recommendations for Users
|
||||
|
||||
1. **Never trust user input**: Always validate revisions, paths, and config keys
|
||||
2. **Use the sanitize module**: All user-provided strings should go through validation
|
||||
3. **Keep dependencies updated**: Run `cargo update` regularly and check for security advisories
|
||||
4. **Monitor logs**: Watch for validation failures which may indicate attack attempts
|
||||
5. **Limit cluster size**: The cluster registration logic assumes a reasonable number of nodes
|
||||
6. **Use HTTPS**: When deploying in production, use TLS for gRPC connections
|
||||
7. **Audit configuration**: Regularly review which git config keys are allowed
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
If you discover a security vulnerability, please report it responsibly by:
|
||||
1. Creating a private security advisory
|
||||
2. Providing detailed reproduction steps
|
||||
3. Allowing maintainers time to address the issue before public disclosure
|
||||
Generated
+1211
-3584
File diff suppressed because it is too large
Load Diff
+44
-49
@@ -1,58 +1,53 @@
|
||||
[package]
|
||||
name = "appks"
|
||||
version = "0.1.0"
|
||||
name = "gitks"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
|
||||
authors = ["gitks contributors"]
|
||||
description = "A gRPC-accessible Git repository operations library for bare repositories"
|
||||
repository = "https://github.com/appks/gitks"
|
||||
homepage = "https://github.com/appks/gitks"
|
||||
license = "PolyForm-Noncommercial-1.0.0"
|
||||
keywords = ["git", "grpc", "bare-repository", "gix"]
|
||||
categories = ["development-tools"]
|
||||
|
||||
[lib]
|
||||
name = "appks"
|
||||
path = "lib.rs"
|
||||
|
||||
name = "gitks"
|
||||
[dependencies]
|
||||
moka = { version = "0.12", default-features = false, features = ["sync"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.11"
|
||||
uuid = { version = "1", features = ["v7"] }
|
||||
gix = { version = "0.84", default-features = false, features = ["serde", "blame", "sha256", "sha1", "tracing", "merge", "max-performance-safe", "revision"] }
|
||||
gix-archive = { version = "0.33", features = ["sha256","sha1","document-features"] }
|
||||
duct = { version = "1", features = [] }
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||
tracing-appender = "0.2"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "io-util", "sync", "net", "signal"] }
|
||||
tokio-stream = { version = "0.1", features = ["full"] }
|
||||
tokio-util = "0.7"
|
||||
thiserror = { version = "2", features = [] }
|
||||
prost = "0.14"
|
||||
prost-types = "0.14"
|
||||
tonic = { version = "0.14", features = ["transport", "gzip"] }
|
||||
tonic-health = "0.14"
|
||||
tonic-prost = "0.14"
|
||||
tempfile = "3"
|
||||
dotenvy = "0.15"
|
||||
etcd-client = { version = "0.18", features = ["tls"] }
|
||||
dashmap = "6"
|
||||
hyper = { version = "1", features = ["server", "http1"] }
|
||||
hyper-util = { version = "0.1", features = ["tokio"] }
|
||||
http-body-util = "0.1"
|
||||
bytes = "1"
|
||||
crc32fast = "1"
|
||||
[[bin]]
|
||||
name = "appks"
|
||||
name = "gitks"
|
||||
path = "main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "gen_openapi"
|
||||
path = "gen_openapi.rs"
|
||||
[dependencies]
|
||||
sqlx = { version = "0.9.0", features = ["postgres","runtime-tokio","chrono","uuid","json","migrate"] }
|
||||
tokio = { version = "1.52.3", features = ["full"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = { version = "1.0.150", features = [] }
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
uuid = { version = "1.23.1", features = ["serde","v4","v7","v5"] }
|
||||
reqwest = { version = "0.13.4", features = ["json"] }
|
||||
tracing = { version = "0.1.44", features = [] }
|
||||
tracing-subscriber = { version = "0.3.23", features = ["fmt"] }
|
||||
dotenvy = "0.15.7"
|
||||
thiserror = "2"
|
||||
redis = { version = "1.2.1", features = ["cluster","cluster-async","aio","tokio-comp","connection-manager"] }
|
||||
dashmap = "6.1"
|
||||
object_store = { version = "0.13.2", features = ["tokio","aws","cloud"] }
|
||||
argon2 = "0.5"
|
||||
rsa = "0.9"
|
||||
chacha20poly1305 = "0.10"
|
||||
hkdf = "0.12"
|
||||
sha2 = "0.10"
|
||||
sha1 = "0.10"
|
||||
hmac = "0.12"
|
||||
base64 = "0.22"
|
||||
rand = "0.8"
|
||||
captcha-rs = "0.5"
|
||||
tonic = { version = "0.14.6", features = ["transport", "channel"] }
|
||||
prost = "0.14.3"
|
||||
prost-types = "0.14.3"
|
||||
tonic-prost = "0.14.6"
|
||||
url = "2.5"
|
||||
etcd-client = { version = "0.18.0", features = ["tls"] }
|
||||
tokio-stream = "0.1"
|
||||
async-nats = "0.49"
|
||||
futures-util = "0.3"
|
||||
utoipa = { version = "5.5.0", features = ["uuid","chrono","actix_extras","decimal","macros"]}
|
||||
actix-web = { version = "4", features = ["secure-cookies"] }
|
||||
actix-multipart = "0.7"
|
||||
hex = "0.4.3"
|
||||
|
||||
[build-dependencies]
|
||||
tonic-prost-build = "0.14.6"
|
||||
tonic-prost-build = "0.14"
|
||||
serde_yml = "0.0.12"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
FROM rust:1.96-bookworm AS chef
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
protobuf-compiler libprotobuf-dev mold clang && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN cargo install cargo-chef
|
||||
WORKDIR /app
|
||||
|
||||
FROM chef AS planner
|
||||
COPY . .
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM chef AS builder
|
||||
COPY --from=planner /app/recipe.json recipe.json
|
||||
RUN cargo chef cook --release --recipe-path recipe.json
|
||||
COPY . .
|
||||
RUN cargo build --release --bin gitks && \
|
||||
strip target/release/gitks
|
||||
|
||||
FROM ubuntu:26.04
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends git ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /app/target/release/gitks /usr/local/bin/gitks
|
||||
|
||||
ENV GITKS_HOST=0.0.0.0
|
||||
ENV GITKS_PORT=50051
|
||||
ENV REPO_PREFIX_PATH=/data/repos
|
||||
|
||||
RUN mkdir -p /data/repos
|
||||
EXPOSE 50051
|
||||
ENTRYPOINT ["gitks"]
|
||||
@@ -0,0 +1,17 @@
|
||||
FROM ubuntu:26.04
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends git && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY target/release/gitks /usr/local/bin/gitks
|
||||
|
||||
ENV GITKS_HOST=0.0.0.0
|
||||
ENV GITKS_PORT=50051
|
||||
ENV REPO_PREFIX_PATH=/data/repos
|
||||
|
||||
RUN mkdir -p /data/repos
|
||||
|
||||
EXPOSE 50051
|
||||
|
||||
ENTRYPOINT ["gitks"]
|
||||
@@ -0,0 +1,58 @@
|
||||
PolyForm Noncommercial License 1.0.0
|
||||
|
||||
Copyright (c) 2024 gitks contributors
|
||||
|
||||
License: "Noncommercial" as defined below.
|
||||
|
||||
"Noncommercial" means primarily intended for or directed towards the
|
||||
advantage or monetary gain of a business, commercial entity, or for-profit
|
||||
organization. A use is "Noncommercial" if it is not primarily intended for
|
||||
or directed towards commercial advantage or monetary compensation.
|
||||
|
||||
1. Grant of Copyright License. Subject to the terms of this license,
|
||||
Licensor grants you a worldwide, royalty-free, non-exclusive, limited
|
||||
license to exercise the Licensed Rights in the Licensed Material for
|
||||
Noncommercial purposes only.
|
||||
|
||||
2. Grant of Patent License. Subject to the terms of this license, Licensor
|
||||
grants you a worldwide, royalty-free, non-exclusive, limited license
|
||||
under patent claims owned or controlled by Licensor that are embodied
|
||||
in the Licensed Material as furnished by Licensor, to make, use, sell,
|
||||
offer for sale, have made, and import the Licensed Material for
|
||||
Noncommercial purposes only.
|
||||
|
||||
3. Limitations. The license granted in Section 1 and Section 2 above is
|
||||
expressly limited to Noncommercial purposes. You may not exercise the
|
||||
Licensed Rights for the purpose of providing services to third parties,
|
||||
including but not limited to:
|
||||
(a) offering the Licensed Material as a hosted or managed service
|
||||
where third parties access or use the Licensed Material;
|
||||
(b) offering the Licensed Material as part of a product or service
|
||||
that is sold, licensed, or otherwise provided for monetary gain;
|
||||
(c) using the Licensed Material to provide consulting, support, or
|
||||
other services for monetary gain.
|
||||
|
||||
4. Acceptance. Any use of the Licensed Material in violation of this
|
||||
license will automatically terminate your rights under this license
|
||||
for the current and all future versions of the Licensed Material.
|
||||
|
||||
5. Patents. If you institute patent litigation against any entity
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
the Licensed Material constitutes direct or contributory patent
|
||||
infringement, then any patent licenses granted to you under this
|
||||
license for the Licensed Material shall terminate as of the date
|
||||
such litigation is filed.
|
||||
|
||||
6. Disclaimer of Warranty. THE LICENSED MATERIAL IS PROVIDED "AS IS" AND
|
||||
WITHOUT ANY WARRANTY OF ANY KIND. LICENSOR DISCLAIMS ALL WARRANTIES,
|
||||
EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES
|
||||
OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A
|
||||
PARTICULAR PURPOSE.
|
||||
|
||||
7. Limitation of Liability. IN NO EVENT WILL LICENSOR BE LIABLE TO YOU
|
||||
FOR ANY DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THESE TERMS OR IN CONNECTION
|
||||
WITH THE USE OR INABILITY TO USE THE LICENSED MATERIAL, EVEN IF
|
||||
LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
For the full license text, see: https://polyformproject.org/licenses/noncommercial/1.0.0
|
||||
@@ -0,0 +1,7 @@
|
||||
# gitks
|
||||
A Git bare repository operation library based on gRPC.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[PolyForm Noncommercial 1.0.0](LICENSE) — Free for noncommercial use. For commercial licenses, please contact us.
|
||||
@@ -1,38 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::captcha::{CaptchaQuery, CaptchaResponse};
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/auth/captcha",
|
||||
tag = "Auth",
|
||||
operation_id = "authGetCaptcha",
|
||||
summary = "Get captcha image",
|
||||
description = "Generate a one-time captcha image and store the plaintext captcha in the current session. Captchas are used for sensitive entry points such as login and sending registration email codes. Set rsa=true to return the current session RSA public key at the same time and reduce frontend initialization requests. The captcha is consumed after either successful or failed validation, so clients must fetch a new one after failure.",
|
||||
params(
|
||||
("w" = u32, Query, description = "Captcha image width; allowed range is 80..=400.", example = 160),
|
||||
("h" = u32, Query, description = "Captcha image height; allowed range is 30..=200.", example = 64),
|
||||
("dark" = bool, Query, description = "Whether to generate a dark-mode captcha.", example = false),
|
||||
("rsa" = bool, Query, description = "Whether to include the RSA public key in the response.", example = true)
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Captcha generated successfully. The base64 field is image data that can be used directly as img.src.", body = ApiResponse<CaptchaResponse>),
|
||||
(status = 400, description = "Invalid captcha size.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Session write failed or RSA initialization failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<CaptchaQuery>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let data = service
|
||||
.auth
|
||||
.auth_captcha(&session, query.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::totp::Disable2FAParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/2fa/disable",
|
||||
tag = "Auth",
|
||||
operation_id = "authDisableTwoFactor",
|
||||
summary = "Disable two-factor authentication",
|
||||
description = "Disable TOTP two-factor authentication for the current signed-in user. This requires verifying both the current password and a valid TOTP code or backup code. password must be encrypted with the current session RSA public key; a successfully verified backup code is consumed.",
|
||||
request_body(
|
||||
content = Disable2FAParams,
|
||||
description = "TOTP/backup code and the current password encrypted with RSA.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "2FA has been disabled.", body = ApiEmptyResponse),
|
||||
(status = 400, description = "2FA is not enabled, the verification code is incorrect, the password is incorrect, or RSA decryption failed.", body = ApiErrorResponse),
|
||||
(status = 401, description = "The current session is not authenticated.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database write failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<Disable2FAParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.auth
|
||||
.auth_2fa_disable(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("two-factor authentication disabled")))
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::totp::Enable2FAResponse;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/2fa/enable",
|
||||
tag = "Auth",
|
||||
operation_id = "authPrepareTwoFactorEnable",
|
||||
summary = "Initialize two-factor authentication setup",
|
||||
description = "Generate a new TOTP secret, otpauth QR-code URI, and 10 one-time backup codes for the current signed-in user, and save them in a not-yet-enabled state. Clients must guide the user to scan the QR code and call /auth/2fa/verify with a dynamic code before 2FA is actually enabled. Backup codes are returned in plaintext only once in this response; frontends must remind users to store them securely.",
|
||||
responses(
|
||||
(status = 200, description = "2FA setup initialized successfully. Returns the secret, QR-code URI, and backup codes.", body = ApiResponse<Enable2FAResponse>),
|
||||
(status = 400, description = "The current user has already enabled 2FA.", body = ApiErrorResponse),
|
||||
(status = 401, description = "The current session is not authenticated.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database write or backup code hashing failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let data = service.auth.auth_2fa_enable(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::totp::Get2FAStatusResponse;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/auth/2fa/status",
|
||||
tag = "Auth",
|
||||
operation_id = "authGetTwoFactorStatus",
|
||||
summary = "Get two-factor authentication status",
|
||||
description = "Read the current signed-in user's TOTP two-factor authentication status, including whether it is enabled, the authentication method, and whether backup codes are still available.",
|
||||
responses(
|
||||
(status = 200, description = "Read successfully.", body = ApiResponse<Get2FAStatusResponse>),
|
||||
(status = 401, description = "The current session is not authenticated.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let data = service.auth.auth_2fa_status(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::email::EmailResponse;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/auth/email",
|
||||
tag = "Auth",
|
||||
operation_id = "authGetPrimaryEmail",
|
||||
summary = "Get current user verified email",
|
||||
description = "Return the verified primary email for the current signed-in user. If no verified email is bound to the account, the email field is null.",
|
||||
responses(
|
||||
(status = 200, description = "Read successfully.", body = ApiResponse<EmailResponse>),
|
||||
(status = 401, description = "The current session is not authenticated.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let data = service.auth.auth_get_email(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::login::LoginParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/login",
|
||||
tag = "Auth",
|
||||
operation_id = "authLogin",
|
||||
summary = "Account login",
|
||||
description = "Log in using a username or verified email. password must be a Base64 ciphertext encrypted with the public key returned by /auth/rsa; the first login attempt must include captcha. If the account has TOTP enabled, the first successful password check returns 400/two-factor required and records pending verification state in the session. Then submit username, password, and totp_code again in the same session to complete login. On success, the session is renewed, the current user is bound, and temporary RSA keys are cleared.",
|
||||
request_body(
|
||||
content = LoginParams,
|
||||
description = "Login parameters. username accepts a username or email; password is an RSA-OAEP-SHA256 encrypted ciphertext; captcha is the captcha stored in the current session; totp_code is required only during the second verification step.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Login succeeded. The server establishes login state through the session cookie.", body = ApiEmptyResponse),
|
||||
(status = 400, description = "Captcha error, RSA decryption failure, or missing/incorrect TOTP.", body = ApiErrorResponse),
|
||||
(status = 404, description = "User does not exist or password is incorrect; to reduce enumeration risk, incorrect passwords are also treated as user-not-found.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database, cache, or session write failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<LoginParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.auth
|
||||
.auth_login(params.into_inner(), session)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("login successful")))
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/logout",
|
||||
tag = "Auth",
|
||||
operation_id = "authLogout",
|
||||
summary = "Log out",
|
||||
description = "Clear the user identity and all temporary authentication data from the current session, including captcha, temporary RSA keys, and pending 2FA state. This endpoint is idempotent: unauthenticated users also receive a success response.",
|
||||
responses(
|
||||
(status = 200, description = "Logged out successfully, or the session was already unauthenticated.", body = ApiEmptyResponse),
|
||||
(status = 500, description = "Session persistence failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service.auth.auth_logout(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("logout successful")))
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::me::ContextMe;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/auth/me",
|
||||
tag = "Auth",
|
||||
operation_id = "authGetCurrentUser",
|
||||
summary = "Get current signed-in user context",
|
||||
description = "Return the current user's basic profile, preferred language, timezone, and notification summary using the user_uid bound to the session. This endpoint is typically used to restore the login state when the frontend app starts.",
|
||||
responses(
|
||||
(status = 200, description = "The current session is authenticated. Returns the user context.", body = ApiResponse<ContextMe>),
|
||||
(status = 401, description = "The current session is unauthenticated or the login state has expired.", body = ApiErrorResponse),
|
||||
(status = 404, description = "The user in the session no longer exists, has been disabled, or has been deleted.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let data = service.auth.auth_me(session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
pub mod captcha;
|
||||
pub mod disable_2fa;
|
||||
pub mod enable_2fa;
|
||||
pub mod get_2fa_status;
|
||||
pub mod get_email;
|
||||
pub mod login;
|
||||
pub mod logout;
|
||||
pub mod me;
|
||||
pub mod regenerate_2fa_backup_codes;
|
||||
pub mod register;
|
||||
pub mod register_email_code;
|
||||
pub mod request_email_change;
|
||||
pub mod request_reset_password;
|
||||
pub mod rsa;
|
||||
pub mod verify_2fa;
|
||||
pub mod verify_email;
|
||||
pub mod verify_reset_password;
|
||||
|
||||
use actix_web::web;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/auth")
|
||||
.route("/rsa", web::get().to(rsa::handle))
|
||||
.route("/captcha", web::get().to(captcha::handle))
|
||||
.route("/login", web::post().to(login::handle))
|
||||
.route("/logout", web::post().to(logout::handle))
|
||||
.route("/me", web::get().to(me::handle))
|
||||
.route(
|
||||
"/register/email-code",
|
||||
web::post().to(register_email_code::handle),
|
||||
)
|
||||
.route("/register", web::post().to(register::handle))
|
||||
.route("/email", web::get().to(get_email::handle))
|
||||
.route(
|
||||
"/email/change",
|
||||
web::post().to(request_email_change::handle),
|
||||
)
|
||||
.route("/email/verify", web::post().to(verify_email::handle))
|
||||
.route(
|
||||
"/reset-password",
|
||||
web::post().to(request_reset_password::handle),
|
||||
)
|
||||
.route(
|
||||
"/reset-password/verify",
|
||||
web::post().to(verify_reset_password::handle),
|
||||
)
|
||||
.route("/2fa/status", web::get().to(get_2fa_status::handle))
|
||||
.route("/2fa/enable", web::post().to(enable_2fa::handle))
|
||||
.route("/2fa/verify", web::post().to(verify_2fa::handle))
|
||||
.route("/2fa/disable", web::post().to(disable_2fa::handle))
|
||||
.route(
|
||||
"/2fa/backup-codes/regenerate",
|
||||
web::post().to(regenerate_2fa_backup_codes::handle),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct Regenerate2FABackupCodesRequest {
|
||||
/// Current account password encrypted with the session RSA public key.
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
||||
pub struct Regenerate2FABackupCodesResponse {
|
||||
/// Newly generated one-time backup codes. Old backup codes become invalid.
|
||||
pub backup_codes: Vec<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/2fa/backup-codes/regenerate",
|
||||
tag = "Auth",
|
||||
operation_id = "authRegenerateTwoFactorBackupCodes",
|
||||
summary = "Regenerate 2FA backup codes",
|
||||
description = "After verifying the current password, generate a new set of backup codes for a user with 2FA enabled and replace the old backup codes. password must be encrypted with the current session RSA public key. Backup codes are returned in plaintext only once in this response; clients must prompt users to store them securely.",
|
||||
request_body(
|
||||
content = Regenerate2FABackupCodesRequest,
|
||||
description = "The current account password encrypted with RSA.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Backup codes have been regenerated; old backup codes are immediately invalidated.", body = ApiResponse<Regenerate2FABackupCodesResponse>),
|
||||
(status = 400, description = "2FA is not enabled, the password is incorrect, or RSA decryption failed.", body = ApiErrorResponse),
|
||||
(status = 401, description = "The current session is not authenticated.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database write or backup code hashing failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<Regenerate2FABackupCodesRequest>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let backup_codes = service
|
||||
.auth
|
||||
.auth_2fa_regenerate_backup_codes(&session, params.into_inner().password)
|
||||
.await?;
|
||||
Ok(
|
||||
HttpResponse::Ok().json(ApiResponse::new(Regenerate2FABackupCodesResponse {
|
||||
backup_codes,
|
||||
})),
|
||||
)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::users::User;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::register::RegisterParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct RegisterResponse {
|
||||
/// Newly created user id.
|
||||
pub id: Uuid,
|
||||
/// Unique username used for login and profile URL.
|
||||
pub username: String,
|
||||
/// Display name initialized from username.
|
||||
pub display_name: Option<String>,
|
||||
/// Avatar URL; usually absent right after registration.
|
||||
pub avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
impl From<User> for RegisterResponse {
|
||||
fn from(user: User) -> Self {
|
||||
Self {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
display_name: user.display_name,
|
||||
avatar_url: user.avatar_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/register",
|
||||
tag = "Auth",
|
||||
operation_id = "authRegister",
|
||||
summary = "Register a new account",
|
||||
description = "Create an account after validating username, email, password, captcha, and email verification code. password must be encrypted with the current session RSA public key; captcha and email_code are one-time credentials. On successful registration, the new user is written to the session and does not need to log in again.",
|
||||
request_body(
|
||||
content = RegisterParams,
|
||||
description = "Registration parameters. email_code comes from /auth/register/email-code; password is a Base64 ciphertext encrypted with RSA-OAEP-SHA256.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Registration succeeded; the current session is automatically signed in as the new user.", body = ApiResponse<RegisterResponse>),
|
||||
(status = 400, description = "Captcha error, email verification code error, weak password, RSA decryption failure, or missing required fields.", body = ApiErrorResponse),
|
||||
(status = 409, description = "The username or email is already in use.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction, password hashing, cache, or session write failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<RegisterParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user = service
|
||||
.auth
|
||||
.auth_register(params.into_inner(), &session)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(RegisterResponse::from(user))))
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::register::{RegisterEmailCodeParams, RegisterEmailCodeResponse};
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/register/email-code",
|
||||
tag = "Auth",
|
||||
operation_id = "authSendRegisterEmailCode",
|
||||
summary = "Send registration email verification code",
|
||||
description = "After validating the captcha in the current session, send a 6-digit registration code to the target email address. The endpoint checks whether a verified email already exists and applies a per-email cooldown to prevent email bombing. The code is valid for 10 minutes by default.",
|
||||
request_body(
|
||||
content = RegisterEmailCodeParams,
|
||||
description = "The target email address and captcha from the current session.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "The verification email has been queued for delivery. Returns the code expiration time.", body = ApiResponse<RegisterEmailCodeResponse>),
|
||||
(status = 400, description = "The captcha is incorrect, the email is empty, or requests are too frequent.", body = ApiErrorResponse),
|
||||
(status = 409, description = "The email is already used by another verified account.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Cache write failed or the email service is unavailable.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<RegisterEmailCodeParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let data = service
|
||||
.auth
|
||||
.auth_register_email_code(params.into_inner(), &session)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::email::EmailChangeRequest;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/email/change",
|
||||
tag = "Auth",
|
||||
operation_id = "authRequestEmailChange",
|
||||
summary = "Request login email change",
|
||||
description = "After verifying the current user password, send a confirmation link to the new email address. password must be encrypted with the current session RSA public key. The token in the confirmation link is valid for 1 hour by default; the actual email switch is completed by calling /auth/email/verify.",
|
||||
request_body(
|
||||
content = EmailChangeRequest,
|
||||
description = "The new email address and encrypted current account password.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "The confirmation email has been queued for delivery.", body = ApiEmptyResponse),
|
||||
(status = 400, description = "The new email is empty, the password is incorrect, or RSA decryption failed.", body = ApiErrorResponse),
|
||||
(status = 401, description = "The current session is not authenticated.", body = ApiErrorResponse),
|
||||
(status = 409, description = "The new email is already in use.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Cache, email service, or database read failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<EmailChangeRequest>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.auth
|
||||
.auth_email_change_request(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("email change verification sent")))
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::reset_pass::ResetPasswordRequest;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/reset-password",
|
||||
tag = "Auth",
|
||||
operation_id = "authRequestPasswordReset",
|
||||
summary = "Request password reset email",
|
||||
description = "Submit an email address to send a password reset link if it belongs to an active user. To prevent user enumeration, the business logic attempts to return success whether the email exists, rate limits are triggered, or email delivery fails. Internally, the endpoint enforces a 60-second cooldown and a daily limit of 5 requests per email.",
|
||||
request_body(
|
||||
content = ResetPasswordRequest,
|
||||
description = "The email address that should receive the password reset link.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "The request has been accepted; if the email exists, a reset email will be sent.", body = ApiEmptyResponse),
|
||||
(status = 500, description = "Rare unrecoverable server-side error.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
params: web::Json<ResetPasswordRequest>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.auth
|
||||
.auth_reset_password_request(params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("password reset request accepted")))
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::rsa::RsaResponse;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/auth/rsa",
|
||||
tag = "Auth",
|
||||
operation_id = "authGetRsaPublicKey",
|
||||
summary = "Get login form RSA public key",
|
||||
description = "Generate or reuse a temporary RSA-2048 key pair for the current browser session and return the public key in PKCS#1 PEM format. Clients should use this public key to encrypt sensitive fields such as passwords with RSA-OAEP-SHA256 before submitting login, registration, password reset, or 2FA disable requests. The private key is encrypted with AEAD and stored only in the server-side session; it is never returned to clients.",
|
||||
responses(
|
||||
(status = 200, description = "Return the RSA public key available for the current session; if an unexpired key already exists in the session, reuse the existing public key.", body = ApiResponse<RsaResponse>),
|
||||
(status = 500, description = "APP_SESSION_SECRET is missing, RSA generation failed, or session write failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let data = service.auth.auth_rsa(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(data)))
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::totp::Verify2FAParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/2fa/verify",
|
||||
tag = "Auth",
|
||||
operation_id = "authVerifyAndEnableTwoFactor",
|
||||
summary = "Verify and enable two-factor authentication",
|
||||
description = "After initializing with /auth/2fa/enable, submit the 6-digit TOTP code generated by the authenticator app. On success, the current user's 2FA status is set to enabled. A small clock drift of one 30-second window before or after is allowed.",
|
||||
request_body(
|
||||
content = Verify2FAParams,
|
||||
description = "The 6-digit TOTP code generated by the authenticator app.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "2FA has been enabled.", body = ApiEmptyResponse),
|
||||
(status = 400, description = "2FA has not been initialized, is already enabled, or the verification code is incorrect.", body = ApiErrorResponse),
|
||||
(status = 401, description = "The current session is not authenticated.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database write failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<Verify2FAParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.auth
|
||||
.auth_2fa_verify_and_enable(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("two-factor authentication enabled")))
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::email::EmailVerifyRequest;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/email/verify",
|
||||
tag = "Auth",
|
||||
operation_id = "authVerifyEmailChange",
|
||||
summary = "Confirm email change",
|
||||
description = "Complete an email change using the token from the confirmation email. The endpoint checks again whether the target email is already taken, then marks old emails as unverified and inserts the new verified primary email in a transaction.",
|
||||
request_body(
|
||||
content = EmailVerifyRequest,
|
||||
description = "Email change confirmation token.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Email changed successfully.", body = ApiEmptyResponse),
|
||||
(status = 400, description = "The token is empty.", body = ApiErrorResponse),
|
||||
(status = 404, description = "The token is invalid or expired.", body = ApiErrorResponse),
|
||||
(status = 409, description = "The target email was taken by another account before confirmation.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database transaction failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
params: web::Json<EmailVerifyRequest>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service.auth.auth_email_verify(params.into_inner()).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("email verified")))
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::reset_pass::ResetPasswordVerifyParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/reset-password/verify",
|
||||
tag = "Auth",
|
||||
operation_id = "authVerifyPasswordReset",
|
||||
summary = "Confirm password reset",
|
||||
description = "Set a new password using the token from the password reset email. password must be encrypted with the current session RSA public key; the new password is strength-checked and rehashed with Argon2id. The token is deleted immediately after successful use; expired or missing tokens fail.",
|
||||
request_body(
|
||||
content = ResetPasswordVerifyParams,
|
||||
description = "The reset token and new password encrypted with RSA.",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Password reset succeeded.", body = ApiEmptyResponse),
|
||||
(status = 400, description = "The token is invalid or expired, RSA decryption failed, or the password is too weak.", body = ApiErrorResponse),
|
||||
(status = 500, description = "Database update or password hashing failed.", body = ApiErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn handle(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<ResetPasswordVerifyParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.auth
|
||||
.auth_reset_password_verify(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("password reset successful")))
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueAssignee;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
/// User ID (UUID) to assign
|
||||
pub user_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Assign a user to an issue
|
||||
///
|
||||
/// Assigns a workspace member to the given issue.
|
||||
/// Requires write access to the issue (author or workspace member).
|
||||
///
|
||||
/// Effects:
|
||||
/// - User is assigned to the issue
|
||||
/// - Assignee is automatically subscribed to the issue
|
||||
/// - Issue assignee count is incremented
|
||||
///
|
||||
/// Returns the created assignment record.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/assignees/{user_id}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueAssign",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 201, description = "User assigned successfully. Returns the created assignment record.", body = ApiResponse<IssueAssignee>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Issue or user not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "User is already assigned to this issue", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn assign_issue(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let assignee = service
|
||||
.issue
|
||||
.issue_assign(&session, &path.workspace_name, path.number, path.user_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(assignee)))
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueLabelRelation;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
/// Label ID (UUID) to assign
|
||||
pub label_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Assign a label to an issue
|
||||
///
|
||||
/// Attaches a label to the given issue. The label must belong to a repository in the same workspace.
|
||||
/// Requires write access to the issue (author or workspace member).
|
||||
///
|
||||
/// Effects:
|
||||
/// - Label is attached to the issue
|
||||
/// - Issue label count is incremented
|
||||
///
|
||||
/// Returns the created label relation.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/labels/{label_id}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueAssignLabel",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Label assigned successfully. Returns the created label relation.", body = ApiResponse<IssueLabelRelation>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Issue or label not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "Label is already assigned to this issue", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn assign_label(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let rel = service
|
||||
.issue
|
||||
.issue_assign_label(&session, &path.workspace_name, path.number, path.label_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(rel)))
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::Issue;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
/// Close an issue
|
||||
///
|
||||
/// Closes an open issue. The issue is marked as closed and the closing user is recorded.
|
||||
/// Requires write access to the issue (author or workspace member).
|
||||
///
|
||||
/// Effects:
|
||||
/// - Issue state changes to "closed"
|
||||
/// - Closed by and closed at are recorded
|
||||
/// - A "Closed" event is logged
|
||||
///
|
||||
/// Returns the closed issue with updated metadata.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/close",
|
||||
tag = "Issues",
|
||||
operation_id = "issueClose",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Issue closed successfully. Returns the closed issue with updated metadata.", body = ApiResponse<Issue>),
|
||||
(status = 400, description = "Issue is already closed", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to close this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn close(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let issue = service
|
||||
.issue
|
||||
.issue_close(&session, &path.workspace_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(issue)))
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::Issue;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::core::CreateIssueParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
}
|
||||
|
||||
/// Create an issue
|
||||
///
|
||||
/// Creates a new issue in the specified workspace.
|
||||
/// Requires at least Member role in the workspace.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - title: Issue title (required)
|
||||
/// - body: Issue body in markdown (optional)
|
||||
/// - priority: Priority level (optional, defaults to "none")
|
||||
/// - visibility: Visibility setting (optional, defaults to "public")
|
||||
/// - due_at: Due date (optional)
|
||||
/// - repo_ids: Related repository IDs
|
||||
/// - label_ids: Label IDs to apply
|
||||
/// - assignee_ids: User IDs to assign
|
||||
/// - milestone_id: Milestone ID to attach
|
||||
///
|
||||
/// Effects:
|
||||
/// - Issue is created with auto-incrementing number
|
||||
/// - Author is automatically subscribed
|
||||
/// - Relations, labels, and assignees are attached
|
||||
/// - Workspace stats are updated
|
||||
///
|
||||
/// Returns the created issue with full metadata.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues",
|
||||
tag = "Issues",
|
||||
operation_id = "issueCreate",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CreateIssueParams,
|
||||
description = "Issue creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Issue created successfully. Returns the newly created issue with full metadata.", body = ApiResponse<Issue>),
|
||||
(status = 400, description = "Invalid parameters: empty title, invalid repository/label/milestone references", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Member role or higher)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or referenced resource not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn create(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CreateIssueParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let issue = service
|
||||
.issue
|
||||
.issue_create(&session, &path.workspace_name, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(issue)))
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueComment;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::comments::CreateCommentParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
/// Create a comment on an issue
|
||||
///
|
||||
/// Adds a new comment to an issue. Users with read access can comment unless the issue is locked
|
||||
/// (in which case only users with write access can comment).
|
||||
///
|
||||
/// Parameters:
|
||||
/// - body: Comment body in markdown format (required)
|
||||
/// - reply_to_comment_id: ID of parent comment for threaded replies (optional)
|
||||
///
|
||||
/// Effects:
|
||||
/// - Comment is created and attached to the issue
|
||||
/// - Commenter is automatically subscribed to the issue
|
||||
/// - Issue comment count is incremented
|
||||
///
|
||||
/// Returns the created comment with metadata.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/comments",
|
||||
tag = "Issues",
|
||||
operation_id = "issueCreateComment",
|
||||
params(PathParams),
|
||||
request_body(content = CreateCommentParams, description = "Comment creation parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 201, description = "Comment created successfully.", body = ApiResponse<IssueComment>),
|
||||
(status = 400, description = "Invalid parameters: empty body or issue is locked", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (issue locked and user lacks write access)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn create_comment(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CreateCommentParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let comment = service
|
||||
.issue
|
||||
.issue_create_comment(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(comment)))
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueLabel;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::labels::CreateLabelParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
/// Create a label
|
||||
///
|
||||
/// Creates a new issue label in a repository.
|
||||
/// Requires at least Member role in the repository.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - name: Label name (required, e.g., "bug", "feature")
|
||||
/// - color: Hex color code (required, e.g., "#FF0000")
|
||||
/// - description: Label description (optional)
|
||||
///
|
||||
/// Returns the created label with metadata.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/labels",
|
||||
tag = "Issues",
|
||||
operation_id = "issueCreateLabel",
|
||||
params(PathParams),
|
||||
request_body(content = CreateLabelParams, description = "Label creation parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 201, description = "Label created successfully.", body = ApiResponse<IssueLabel>),
|
||||
(status = 400, description = "Invalid parameters: empty name or invalid color", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Member role)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn create_label(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CreateLabelParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let label = service
|
||||
.issue
|
||||
.issue_create_label(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(label)))
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueMilestone;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::milestones::CreateMilestoneParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
/// Create a milestone
|
||||
///
|
||||
/// Creates a new milestone in a repository for tracking issue progress.
|
||||
/// Requires at least Member role in the repository.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - title: Milestone title (required)
|
||||
/// - description: Description of the milestone (optional)
|
||||
/// - due_at: Target due date (optional)
|
||||
///
|
||||
/// Returns the created milestone with metadata.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/milestones",
|
||||
tag = "Issues",
|
||||
operation_id = "issueCreateMilestone",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CreateMilestoneParams,
|
||||
description = "Milestone creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Milestone created successfully. Returns the newly created milestone with metadata.", body = ApiResponse<IssueMilestone>),
|
||||
(status = 400, description = "Invalid parameters: empty title", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Member role or higher)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn create_milestone(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CreateMilestoneParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let milestone = service
|
||||
.issue
|
||||
.issue_create_milestone(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(milestone)))
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
/// Delete an issue
|
||||
///
|
||||
/// Soft-deletes an issue. The issue is marked as deleted but remains in the database.
|
||||
/// Requires Admin role in the workspace (or issue author).
|
||||
///
|
||||
/// Effects:
|
||||
/// - Issue is marked as deleted (soft-delete)
|
||||
/// - Workspace issue count is decremented
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueDelete",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Issue deleted successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Admin role or issue author)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn delete(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.issue
|
||||
.issue_delete(&session, &path.workspace_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Issue deleted successfully".to_string())))
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub number: i64,
|
||||
pub comment_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Delete an issue comment
|
||||
///
|
||||
/// Soft-deletes a comment. The comment author can delete their own comments.
|
||||
/// Workspace admins can delete any comment.
|
||||
///
|
||||
/// Effects:
|
||||
/// - Comment is marked as deleted
|
||||
/// - Issue comment count is decremented
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/comments/{comment_id}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueDeleteComment",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Comment deleted successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Cannot delete other users' comments (requires admin)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace, issue, or comment not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn delete_comment(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.issue
|
||||
.issue_delete_comment(&session, &path.workspace_name, path.number, path.comment_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Comment deleted successfully".to_string())))
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub label_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Delete a label
|
||||
///
|
||||
/// Permanently removes an issue label from a repository.
|
||||
/// Requires Admin role in the repository.
|
||||
///
|
||||
/// Effects:
|
||||
/// - Label is permanently deleted
|
||||
/// - All issue-label relations using this label are removed
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/labels/{label_id}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueDeleteLabel",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Label deleted successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Admin role)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository, workspace, or label not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn delete_label(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.issue
|
||||
.issue_delete_label(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.label_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Label deleted successfully".to_string())))
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// Milestone ID (UUID)
|
||||
pub milestone_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Delete a milestone
|
||||
///
|
||||
/// Permanently removes a milestone from the repository.
|
||||
/// Requires Admin role in the repository.
|
||||
///
|
||||
/// Effects:
|
||||
/// - Milestone is permanently deleted
|
||||
/// - Issues attached to this milestone lose their milestone association
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/milestones/{milestone_id}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueDeleteMilestone",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Milestone deleted successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Admin role)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository, workspace, or milestone not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn delete_milestone(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.issue
|
||||
.issue_delete_milestone(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.milestone_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Milestone deleted".to_string())))
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::Issue;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
/// Get an issue by number
|
||||
///
|
||||
/// Returns detailed information about a specific issue, identified by workspace name and issue number.
|
||||
/// Requires read access to the issue (public or workspace member).
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueGet",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Issue retrieved successfully. Returns complete issue with all metadata.", body = ApiResponse<Issue>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let issue = service
|
||||
.issue
|
||||
.issue_get(&session, &path.workspace_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(issue)))
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::Issue;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::core::IssueListFilters;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Filter by issue state ("open" or "closed")
|
||||
pub state: Option<String>,
|
||||
/// Filter by priority level
|
||||
pub priority: Option<String>,
|
||||
/// Filter by author user ID
|
||||
pub author_id: Option<uuid::Uuid>,
|
||||
/// Filter by assignee user ID
|
||||
pub assignee_id: Option<uuid::Uuid>,
|
||||
/// Filter by milestone ID
|
||||
pub milestone_id: Option<uuid::Uuid>,
|
||||
/// Filter by label ID
|
||||
pub label_id: Option<uuid::Uuid>,
|
||||
/// Maximum number of issues to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of issues to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List issues in a workspace
|
||||
///
|
||||
/// Returns a paginated list of issues in the workspace, sorted by issue number (newest first).
|
||||
/// Supports filtering by state, priority, author, assignee, milestone, and label.
|
||||
/// Only returns issues visible to the authenticated user (public + workspace member access).
|
||||
/// Requires authentication.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues",
|
||||
tag = "Issues",
|
||||
operation_id = "issueList",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Issues listed successfully. Returns filtered array of issue objects with metadata.", body = ApiResponse<Vec<Issue>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let filters = IssueListFilters {
|
||||
state: query.state.clone(),
|
||||
priority: query.priority.clone(),
|
||||
author_id: query.author_id,
|
||||
assignee_id: query.assignee_id,
|
||||
milestone_id: query.milestone_id,
|
||||
label_id: query.label_id,
|
||||
};
|
||||
let issues = service
|
||||
.issue
|
||||
.issue_list(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
filters,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(issues)))
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueAssignee;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of assignees to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of assignees to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List assignees of an issue
|
||||
///
|
||||
/// Returns a paginated list of all users assigned to the given issue.
|
||||
/// Shows who is assigned, when they were assigned, and who assigned them.
|
||||
/// Requires read access to the issue.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/assignees",
|
||||
tag = "Issues",
|
||||
operation_id = "issueListAssignees",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Assignees listed successfully. Returns array of assignee objects with assignment metadata.", body = ApiResponse<Vec<IssueAssignee>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_assignees(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let assignees = service
|
||||
.issue
|
||||
.issue_assignees(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(assignees)))
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueComment;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List issue comments
|
||||
///
|
||||
/// Returns a paginated list of comments on an issue, sorted by creation date (oldest first).
|
||||
/// Requires read access to the issue.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/comments",
|
||||
tag = "Issues",
|
||||
operation_id = "issueListComments",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Comments listed successfully.", body = ApiResponse<Vec<IssueComment>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_comments(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let comments = service
|
||||
.issue
|
||||
.issue_comments(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(comments)))
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueEvent;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of events to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of events to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List issue events
|
||||
///
|
||||
/// Returns a chronological timeline of all events for the given issue.
|
||||
/// Events include creation, updates, state changes, assignments, label changes, etc.
|
||||
/// Sorted by creation date (oldest first for timeline display).
|
||||
/// Requires read access to the issue.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/events",
|
||||
tag = "Issues",
|
||||
operation_id = "issueListEvents",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Events listed successfully. Returns chronological array of event objects.", body = ApiResponse<Vec<IssueEvent>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_events(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let events = service
|
||||
.issue
|
||||
.issue_list_events(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(events)))
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueLabelRelation;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of label relations to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of label relations to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List labels assigned to an issue
|
||||
///
|
||||
/// Returns a paginated list of all label relations for the given issue.
|
||||
/// Shows which labels are attached to the issue, with assignment metadata.
|
||||
/// Requires read access to the issue.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/labels",
|
||||
tag = "Issues",
|
||||
operation_id = "issueListLabelRelations",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Label relations listed successfully. Returns array of label relation objects with metadata.", body = ApiResponse<Vec<IssueLabelRelation>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_issue_labels(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let rels = service
|
||||
.issue
|
||||
.issue_label_relations(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(rels)))
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueLabel;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
/// List labels in a repository
|
||||
///
|
||||
/// Returns all issue labels defined in the repository, sorted alphabetically.
|
||||
/// Requires read access to the repository.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/labels",
|
||||
tag = "Issues",
|
||||
operation_id = "issueListLabels",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Labels listed successfully.", body = ApiResponse<Vec<IssueLabel>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_labels(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let labels = service
|
||||
.issue
|
||||
.issue_labels(&session, &path.workspace_name, &path.repo_name)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(labels)))
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueMilestone;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of milestones to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of milestones to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List milestones in a repository
|
||||
///
|
||||
/// Returns a paginated list of milestones in the repository, sorted by state (open first) then by due date.
|
||||
/// Includes milestone metadata such as title, description, state, due date, and progress.
|
||||
/// Requires read access to the repository.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/milestones",
|
||||
tag = "Issues",
|
||||
operation_id = "issueListMilestones",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Milestones listed successfully. Returns array of milestone objects with metadata.", body = ApiResponse<Vec<IssueMilestone>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to access this repository", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_milestones(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let milestones = service
|
||||
.issue
|
||||
.issue_milestones(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(milestones)))
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::Issue;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct LockIssueParams {
|
||||
/// Whether to lock (true) or unlock (false) the issue
|
||||
pub locked: bool,
|
||||
}
|
||||
|
||||
/// Lock or unlock an issue
|
||||
///
|
||||
/// Locks or unlocks conversation on an issue. When locked, only users with write access can comment.
|
||||
/// Requires write access to the issue (author or workspace member).
|
||||
///
|
||||
/// Returns the updated issue with lock status.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/lock",
|
||||
tag = "Issues",
|
||||
operation_id = "issueLock",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = LockIssueParams,
|
||||
description = "Lock/unlock parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Issue lock status updated successfully.", body = ApiResponse<Issue>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to manage this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn lock(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<LockIssueParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let issue = service
|
||||
.issue
|
||||
.issue_lock(&session, &path.workspace_name, path.number, params.locked)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(issue)))
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssuePrRelation;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::pr_relations::LinkPrParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of relations to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of relations to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List pull request relations for an issue
|
||||
///
|
||||
/// Returns a paginated list of all pull requests linked to the given issue.
|
||||
/// Shows relation type (closes, references, depends_on, etc.) and link metadata.
|
||||
/// Requires read access to the issue.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/prs",
|
||||
tag = "Issues",
|
||||
operation_id = "issueListPrRelations",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "PR relations listed successfully. Returns array of PR relation objects.", body = ApiResponse<Vec<IssuePrRelation>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_pr_relations(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let relations = service
|
||||
.issue
|
||||
.issue_pr_relations(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(relations)))
|
||||
}
|
||||
|
||||
/// Link a pull request to an issue
|
||||
///
|
||||
/// Creates a relation between the given issue and a pull request.
|
||||
/// Commonly used to mark a PR as closing or referencing an issue.
|
||||
/// Requires write access to the issue.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - pull_request_id: Pull request ID (UUID) to link
|
||||
/// - relation_type: Relation type ("closes", "references", "depends_on", default: "references")
|
||||
///
|
||||
/// Returns the created relation.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/prs",
|
||||
tag = "Issues",
|
||||
operation_id = "issueLinkPr",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = LinkPrParams,
|
||||
description = "Link pull request parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Pull request linked successfully. Returns the created relation.", body = ApiResponse<IssuePrRelation>),
|
||||
(status = 400, description = "Invalid parameters: invalid relation type", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Issue or pull request not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "Pull request is already linked to this issue", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn link_pr(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<LinkPrParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let relation = service
|
||||
.issue
|
||||
.issue_link_pr(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(relation)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct RelationPathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
/// Relation ID (UUID)
|
||||
pub relation_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Unlink a pull request from an issue
|
||||
///
|
||||
/// Removes a pull request relation from the given issue.
|
||||
/// Requires write access to the issue.
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/prs/{relation_id}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueUnlinkPr",
|
||||
params(RelationPathParams),
|
||||
responses(
|
||||
(status = 200, description = "Pull request unlinked successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR relation not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn unlink_pr(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<RelationPathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.issue
|
||||
.issue_unlink_pr(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
path.relation_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("PR unlinked".to_string())))
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueReaction;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::reactions::CreateIssueReactionParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of reactions to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of reactions to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List reactions on an issue
|
||||
///
|
||||
/// Returns a paginated list of all emoji reactions on the given issue.
|
||||
/// Includes reaction content, target type, and user who added each reaction.
|
||||
/// Requires read access to the issue.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/reactions",
|
||||
tag = "Issues",
|
||||
operation_id = "issueListReactions",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Reactions listed successfully. Returns array of reaction objects.", body = ApiResponse<Vec<IssueReaction>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_reactions(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let reactions = service
|
||||
.issue
|
||||
.issue_reactions(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(reactions)))
|
||||
}
|
||||
|
||||
/// Add a reaction to an issue
|
||||
///
|
||||
/// Adds an emoji reaction to the given issue.
|
||||
/// Requires read access to the issue.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - content: Reaction content (e.g., "👍", "❤️", "🎉")
|
||||
/// - target_type: Target type for the reaction (defaults to "Issue")
|
||||
/// - target_id: Target ID for reactions on specific comments (optional)
|
||||
///
|
||||
/// Returns the created reaction.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/reactions",
|
||||
tag = "Issues",
|
||||
operation_id = "issueAddReaction",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CreateIssueReactionParams,
|
||||
description = "Reaction creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Reaction added successfully. Returns the created reaction.", body = ApiResponse<IssueReaction>),
|
||||
(status = 400, description = "Invalid parameters: empty content or invalid target type", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "Issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn add_reaction(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CreateIssueReactionParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let reaction = service
|
||||
.issue
|
||||
.issue_add_reaction(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(reaction)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct ReactionPathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
/// Reaction ID (UUID)
|
||||
pub reaction_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Remove a reaction from an issue
|
||||
///
|
||||
/// Removes a previously added reaction. Only the user who added the reaction can remove it.
|
||||
/// Requires read access to the issue.
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/reactions/{reaction_id}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueRemoveReaction",
|
||||
params(ReactionPathParams),
|
||||
responses(
|
||||
(status = 200, description = "Reaction removed successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Cannot remove another user's reaction", body = ApiErrorResponse),
|
||||
(status = 404, description = "Reaction not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn remove_reaction(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<ReactionPathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.issue
|
||||
.issue_remove_reaction(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
path.reaction_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Reaction removed".to_string())))
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::Issue;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
/// Reopen an issue
|
||||
///
|
||||
/// Reopens a closed issue. The issue state changes back to "open" and closed metadata is cleared.
|
||||
/// Requires write access to the issue (author or workspace member).
|
||||
///
|
||||
/// Returns the reopened issue with updated metadata.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/reopen",
|
||||
tag = "Issues",
|
||||
operation_id = "issueReopen",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Issue reopened successfully. Returns the reopened issue with updated metadata.", body = ApiResponse<Issue>),
|
||||
(status = 400, description = "Issue is not closed", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to reopen this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn reopen(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let issue = service
|
||||
.issue
|
||||
.issue_reopen(&session, &path.workspace_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(issue)))
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueRepoRelation;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::repo_relations::LinkRepoParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of relations to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of relations to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List repository relations for an issue
|
||||
///
|
||||
/// Returns a paginated list of all repositories linked to the given issue.
|
||||
/// Shows relation type (references, duplicates, blocks, etc.) and link metadata.
|
||||
/// Requires read access to the issue.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/repos",
|
||||
tag = "Issues",
|
||||
operation_id = "issueListRepoRelations",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Repository relations listed successfully. Returns array of relation objects.", body = ApiResponse<Vec<IssueRepoRelation>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_repo_relations(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let relations = service
|
||||
.issue
|
||||
.issue_repo_relations(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(relations)))
|
||||
}
|
||||
|
||||
/// Link a repository to an issue
|
||||
///
|
||||
/// Creates a relation between the given issue and a repository.
|
||||
/// Requires write access to the issue.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - repo_id: Repository ID (UUID) to link
|
||||
/// - relation_type: Relation type ("references", "duplicates", "blocks", "depends_on", default: "references")
|
||||
///
|
||||
/// Returns the created relation.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/repos",
|
||||
tag = "Issues",
|
||||
operation_id = "issueLinkRepo",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = LinkRepoParams,
|
||||
description = "Link repository parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Repository linked successfully. Returns the created relation.", body = ApiResponse<IssueRepoRelation>),
|
||||
(status = 400, description = "Invalid parameters: invalid relation type", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Issue or repository not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "Repository is already linked to this issue", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn link_repo(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<LinkRepoParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let relation = service
|
||||
.issue
|
||||
.issue_link_repo(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(relation)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct RelationPathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
/// Relation ID (UUID)
|
||||
pub relation_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Unlink a repository from an issue
|
||||
///
|
||||
/// Removes a repository relation from the given issue.
|
||||
/// Requires write access to the issue.
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/repos/{relation_id}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueUnlinkRepo",
|
||||
params(RelationPathParams),
|
||||
responses(
|
||||
(status = 200, description = "Repository unlinked successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository relation not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn unlink_repo(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<RelationPathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.issue
|
||||
.issue_unlink_repo(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
path.relation_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Repo unlinked".to_string())))
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueSubscriber;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of subscribers to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of subscribers to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List subscribers of an issue
|
||||
///
|
||||
/// Returns a paginated list of all users subscribed to the given issue.
|
||||
/// Shows who receives notifications and their subscription reason (author, assignee, manual).
|
||||
/// Requires read access to the issue.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/subscribers",
|
||||
tag = "Issues",
|
||||
operation_id = "issueListSubscribers",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Subscribers listed successfully. Returns array of subscriber objects.", body = ApiResponse<Vec<IssueSubscriber>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_subscribers(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let subscribers = service
|
||||
.issue
|
||||
.issue_subscribers(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(subscribers)))
|
||||
}
|
||||
|
||||
/// Subscribe to an issue
|
||||
///
|
||||
/// Subscribes the authenticated user to the given issue to receive notifications.
|
||||
/// Requires read access to the issue.
|
||||
///
|
||||
/// Effects:
|
||||
/// - User is added as a subscriber with "manual" reason
|
||||
/// - User receives notifications for all issue activity
|
||||
///
|
||||
/// Returns the created subscription record.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/subscribe",
|
||||
tag = "Issues",
|
||||
operation_id = "issueSubscribe",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Subscribed successfully. Returns the subscription record.", body = ApiResponse<IssueSubscriber>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Issue not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "Already subscribed to this issue", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn subscribe(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let sub = service
|
||||
.issue
|
||||
.issue_subscribe(&session, &path.workspace_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(sub)))
|
||||
}
|
||||
|
||||
/// Unsubscribe from an issue
|
||||
///
|
||||
/// Removes the authenticated user's subscription to the given issue.
|
||||
/// Stops all notifications for this issue.
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/subscribe",
|
||||
tag = "Issues",
|
||||
operation_id = "issueUnsubscribe",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Unsubscribed successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Not currently subscribed to this issue", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn unsubscribe(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.issue
|
||||
.issue_unsubscribe(&session, &path.workspace_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Unsubscribed".to_string())))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct MuteIssueParams {
|
||||
/// Whether to mute (true) or unmute (false) notifications
|
||||
pub muted: bool,
|
||||
}
|
||||
|
||||
/// Mute or unmute issue notifications
|
||||
///
|
||||
/// Mutes or unmutes notifications for the given issue without unsubscribing.
|
||||
/// Requires an active subscription to the issue.
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/mute",
|
||||
tag = "Issues",
|
||||
operation_id = "issueMute",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = MuteIssueParams,
|
||||
description = "Mute/unmute parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Mute status updated successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Not currently subscribed to this issue", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn mute(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<MuteIssueParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.issue
|
||||
.issue_mute(&session, &path.workspace_name, path.number, params.muted)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Mute status updated".to_string())))
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueTemplate;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::templates::{CreateTemplateParams, UpdateTemplateParams};
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of templates to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of templates to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List issue templates in a repository
|
||||
///
|
||||
/// Returns a paginated list of all active issue templates in the repository.
|
||||
/// Templates provide pre-filled content for creating new issues.
|
||||
/// Sorted alphabetically by name.
|
||||
/// Requires read access to the repository.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/templates",
|
||||
tag = "Issues",
|
||||
operation_id = "issueListTemplates",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Templates listed successfully. Returns array of template objects.", body = ApiResponse<Vec<IssueTemplate>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to access this repository", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_templates(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let templates = service
|
||||
.issue
|
||||
.issue_templates(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(templates)))
|
||||
}
|
||||
|
||||
/// Create an issue template
|
||||
///
|
||||
/// Creates a new issue template in the repository.
|
||||
/// Requires at least Member role in the repository.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - name: Template name (required)
|
||||
/// - description: Template description (optional)
|
||||
/// - title_template: Default title for issues (optional, supports placeholders)
|
||||
/// - body_template: Default body content in markdown (required)
|
||||
/// - labels: List of label names to auto-apply (optional)
|
||||
///
|
||||
/// Returns the created template with metadata.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/templates",
|
||||
tag = "Issues",
|
||||
operation_id = "issueCreateTemplate",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CreateTemplateParams,
|
||||
description = "Template creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Template created successfully. Returns the newly created template.", body = ApiResponse<IssueTemplate>),
|
||||
(status = 400, description = "Invalid parameters: empty name or body template", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Member role or higher)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn create_template(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CreateTemplateParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let template = service
|
||||
.issue
|
||||
.issue_create_template(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(template)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct TemplatePathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// Template ID (UUID)
|
||||
pub template_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Update an issue template
|
||||
///
|
||||
/// Updates an existing issue template's metadata and content.
|
||||
/// Requires Admin role in the repository.
|
||||
///
|
||||
/// All fields are optional; only provided fields are updated.
|
||||
/// Returns the updated template.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/templates/{template_id}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueUpdateTemplate",
|
||||
params(TemplatePathParams),
|
||||
request_body(
|
||||
content = UpdateTemplateParams,
|
||||
description = "Template update parameters (all fields optional)",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Template updated successfully. Returns the updated template.", body = ApiResponse<IssueTemplate>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Admin role)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository, workspace, or template not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn update_template(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<TemplatePathParams>,
|
||||
params: web::Json<UpdateTemplateParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let template = service
|
||||
.issue
|
||||
.issue_update_template(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.template_id,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(template)))
|
||||
}
|
||||
|
||||
/// Delete an issue template
|
||||
///
|
||||
/// Permanently removes an issue template from the repository.
|
||||
/// Requires Admin role in the repository.
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/templates/{template_id}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueDeleteTemplate",
|
||||
params(TemplatePathParams),
|
||||
responses(
|
||||
(status = 200, description = "Template deleted successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Admin role)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Template not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn delete_template(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<TemplatePathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.issue
|
||||
.issue_delete_template(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.template_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Template deleted".to_string())))
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::Issue;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct TransferIssueParams {
|
||||
/// Target workspace name to transfer the issue to
|
||||
pub target_workspace_name: String,
|
||||
}
|
||||
|
||||
/// Transfer an issue to another workspace
|
||||
///
|
||||
/// Moves an issue from the current workspace to a different workspace.
|
||||
/// Requires Admin role in both the source and target workspaces.
|
||||
///
|
||||
/// Effects:
|
||||
/// - Issue is transferred to the target workspace with a new number
|
||||
/// - Source workspace issue count is decremented
|
||||
/// - Target workspace issue count is incremented
|
||||
///
|
||||
/// Returns the transferred issue with updated workspace and number.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/transfer",
|
||||
tag = "Issues",
|
||||
operation_id = "issueTransfer",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = TransferIssueParams,
|
||||
description = "Transfer parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Issue transferred successfully. Returns the issue with new workspace assignment.", body = ApiResponse<Issue>),
|
||||
(status = 400, description = "Invalid target workspace", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions in source or target workspace", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn transfer(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<TransferIssueParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let issue = service
|
||||
.issue
|
||||
.issue_transfer(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
¶ms.target_workspace_name,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(issue)))
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
/// User ID (UUID) to unassign
|
||||
pub user_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Unassign a user from an issue
|
||||
///
|
||||
/// Removes a user from the issue's assignee list.
|
||||
/// Requires write access to the issue (author or workspace member).
|
||||
///
|
||||
/// Effects:
|
||||
/// - User is removed from the issue's assignees
|
||||
/// - Issue assignee count is decremented
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/assignees/{user_id}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueUnassign",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "User unassigned successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "User is not assigned to this issue or not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn unassign_issue(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.issue
|
||||
.issue_unassign(&session, &path.workspace_name, path.number, path.user_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("User unassigned".to_string())))
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
/// Label ID (UUID) to unassign
|
||||
pub label_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Unassign a label from an issue
|
||||
///
|
||||
/// Removes a label from the given issue.
|
||||
/// Requires write access to the issue (author or workspace member).
|
||||
///
|
||||
/// Effects:
|
||||
/// - Label relation is removed from the issue
|
||||
/// - Issue label count is decremented
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/labels/{label_id}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueUnassignLabel",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Label unassigned successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Label is not assigned to this issue or not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn unassign_label(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.issue
|
||||
.issue_unassign_label(&session, &path.workspace_name, path.number, path.label_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Label unassigned".to_string())))
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::Issue;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::core::UpdateIssueParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Issue number (unique within the workspace)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
/// Update an issue
|
||||
///
|
||||
/// Updates an existing issue's metadata such as title, body, priority, visibility, due date, and milestone.
|
||||
/// Requires write access to the issue (author or workspace member).
|
||||
///
|
||||
/// All fields are optional; only provided fields are updated.
|
||||
/// Returns the updated issue with full metadata.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueUpdate",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = UpdateIssueParams,
|
||||
description = "Issue update parameters (all fields optional)",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Issue updated successfully. Returns the updated issue with full metadata.", body = ApiResponse<Issue>),
|
||||
(status = 400, description = "Invalid parameters: invalid priority, visibility, or milestone", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to edit this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace, issue, or referenced resource not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn update(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<UpdateIssueParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let issue = service
|
||||
.issue
|
||||
.issue_update(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(issue)))
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueComment;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::comments::UpdateCommentParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub number: i64,
|
||||
pub comment_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Update an issue comment
|
||||
///
|
||||
/// Updates the body of an existing comment. Only the comment author can update their own comments.
|
||||
/// Requires read access to the issue.
|
||||
///
|
||||
/// Returns the updated comment with edit timestamp.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/issues/{number}/comments/{comment_id}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueUpdateComment",
|
||||
params(PathParams),
|
||||
request_body(content = UpdateCommentParams, description = "Comment update parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 200, description = "Comment updated successfully.", body = ApiResponse<IssueComment>),
|
||||
(status = 400, description = "Invalid parameters: empty body", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Cannot edit other users' comments", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace, issue, or comment not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn update_comment(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<UpdateCommentParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let comment = service
|
||||
.issue
|
||||
.issue_update_comment(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
path.number,
|
||||
path.comment_id,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(comment)))
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueLabel;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::labels::UpdateLabelParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub label_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Update a label
|
||||
///
|
||||
/// Updates an existing issue label's name, color, or description.
|
||||
/// Requires Admin role in the repository.
|
||||
///
|
||||
/// All fields are optional; only provided fields are updated.
|
||||
/// Returns the updated label.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/labels/{label_id}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueUpdateLabel",
|
||||
params(PathParams),
|
||||
request_body(content = UpdateLabelParams, description = "Label update parameters (all fields optional)", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 200, description = "Label updated successfully.", body = ApiResponse<IssueLabel>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Admin role)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository, workspace, or label not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn update_label(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<UpdateLabelParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let label = service
|
||||
.issue
|
||||
.issue_update_label(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.label_id,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(label)))
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueMilestone;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::milestones::UpdateMilestoneParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// Milestone ID (UUID)
|
||||
pub milestone_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Update a milestone
|
||||
///
|
||||
/// Updates an existing milestone's metadata. Can also close or reopen the milestone via the state field.
|
||||
/// Requires at least Member role in the repository.
|
||||
///
|
||||
/// Updatable fields:
|
||||
/// - title: Milestone title (optional)
|
||||
/// - description: Description (optional)
|
||||
/// - due_at: Target due date (optional)
|
||||
/// - state: State ("open" or "closed") for closing/reopening the milestone (optional)
|
||||
///
|
||||
/// All fields are optional; only provided fields are updated.
|
||||
/// Returns the updated milestone with full metadata.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/issues/milestones/{milestone_id}",
|
||||
tag = "Issues",
|
||||
operation_id = "issueUpdateMilestone",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = UpdateMilestoneParams,
|
||||
description = "Milestone update parameters (all fields optional)",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Milestone updated successfully. Returns the updated milestone with full metadata.", body = ApiResponse<IssueMilestone>),
|
||||
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Member role or higher)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository, workspace, or milestone not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn update_milestone(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<UpdateMilestoneParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let milestone = service
|
||||
.issue
|
||||
.issue_update_milestone(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.milestone_id,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(milestone)))
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
pub mod auth;
|
||||
pub mod issue;
|
||||
pub mod openapi;
|
||||
pub mod pr;
|
||||
pub mod repo;
|
||||
pub mod response;
|
||||
pub mod routes;
|
||||
pub mod user;
|
||||
pub mod wiki;
|
||||
pub mod workspace;
|
||||
-702
@@ -1,702 +0,0 @@
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use crate::api::auth::regenerate_2fa_backup_codes::{
|
||||
Regenerate2FABackupCodesRequest, Regenerate2FABackupCodesResponse,
|
||||
};
|
||||
use crate::api::auth::register::RegisterResponse;
|
||||
use crate::api::issue::lock::LockIssueParams;
|
||||
use crate::api::issue::subscribers::MuteIssueParams;
|
||||
use crate::api::issue::transfer::TransferIssueParams;
|
||||
use crate::api::pr::lock::LockPrParams;
|
||||
use crate::api::pr::subscriptions::MutePrParams;
|
||||
use crate::api::repo::accept_invitation::AcceptInvitationParams;
|
||||
use crate::api::repo::set_branch_protection::SetBranchProtectionParams;
|
||||
use crate::api::repo::transfer_owner::TransferOwnerParams;
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse, ApiResponse};
|
||||
use crate::api::wiki::compare_revisions::WikiCompareResult;
|
||||
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::prs::{
|
||||
PrAssignee, PrCheckRun, PrCommit, PrEvent, PrFile, PrLabel, PrLabelRelation, PrMergeStrategy,
|
||||
PrReaction, PrReview, PrReviewComment, PrStatus, PrSubscription, PullRequest,
|
||||
};
|
||||
use crate::models::repos::{
|
||||
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::wiki::{WikiPage, WikiPageRevision};
|
||||
use crate::models::workspaces::{
|
||||
Workspace, WorkspaceAuditLog, WorkspaceBilling, WorkspaceCustomBranding, WorkspaceDomain,
|
||||
WorkspaceIntegration, WorkspaceInvitation, WorkspaceMember, WorkspacePendingApproval,
|
||||
WorkspaceSettings, WorkspaceStats, WorkspaceWebhook,
|
||||
};
|
||||
use crate::service::auth::captcha::{CaptchaQuery, CaptchaResponse};
|
||||
use crate::service::auth::email::{EmailChangeRequest, EmailResponse, EmailVerifyRequest};
|
||||
use crate::service::auth::login::LoginParams;
|
||||
use crate::service::auth::me::ContextMe;
|
||||
use crate::service::auth::register::{
|
||||
RegisterEmailCodeParams, RegisterEmailCodeResponse, RegisterParams,
|
||||
};
|
||||
use crate::service::auth::reset_pass::{ResetPasswordRequest, ResetPasswordVerifyParams};
|
||||
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::pr::check_runs::{CreateCheckRunParams, UpdateCheckRunParams};
|
||||
use crate::service::pr::core::{CreatePrParams, MergePrParams, PrListFilters, UpdatePrParams};
|
||||
use crate::service::pr::labels::{CreatePrLabelParams, UpdatePrLabelParams};
|
||||
use crate::service::pr::merge_strategy::UpdateMergeStrategyParams;
|
||||
use crate::service::pr::reactions::CreateReactionParams;
|
||||
use crate::service::pr::reviews::{
|
||||
AddReplyParams, CreateReviewParams, DismissReviewParams, ReviewCommentParams,
|
||||
SubmitReviewParams,
|
||||
};
|
||||
use crate::service::repo::branches::CreateBranchParams;
|
||||
use crate::service::repo::commit_status::{CreateCommitCommentParams, CreateCommitStatusParams};
|
||||
use crate::service::repo::core::{CreateRepoParams, UpdateRepoParams};
|
||||
use crate::service::repo::deploy_keys::AddDeployKeyParams;
|
||||
use crate::service::repo::fork::ForkRepoParams;
|
||||
use crate::service::repo::invitations::CreateRepoInvitationParams;
|
||||
use crate::service::repo::members::{AddRepoMemberParams, UpdateRepoMemberRoleParams};
|
||||
use crate::service::repo::protection::{
|
||||
BranchMergeCheck, CreateProtectionRuleParams, UpdateProtectionRuleParams,
|
||||
};
|
||||
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::wiki::core::{CreateWikiPageParams, UpdateWikiPageParams};
|
||||
use crate::service::workspace::approvals::RequestApprovalParams;
|
||||
use crate::service::workspace::billing::UpdateBillingParams;
|
||||
use crate::service::workspace::branding::UpdateBrandingParams;
|
||||
use crate::service::workspace::core::{CreateWorkspaceParams, UpdateWorkspaceParams};
|
||||
use crate::service::workspace::domains::AddDomainParams;
|
||||
use crate::service::workspace::integrations::{CreateIntegrationParams, UpdateIntegrationParams};
|
||||
use crate::service::workspace::invitations::{CreateInvitationParams, CreateInvitationResponse};
|
||||
use crate::service::workspace::members::{AddMemberParams, UpdateMemberRoleParams};
|
||||
use crate::service::workspace::settings::UpdateWorkspaceSettingsParams;
|
||||
use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookParams};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
info(
|
||||
title = "AppKS API",
|
||||
version = "0.1.0",
|
||||
description = "AppKS collaborative development platform HTTP API."
|
||||
),
|
||||
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."),
|
||||
(name = "Pull Requests", description = "Pull request lifecycle including reviews, check runs, merge strategies, labels, assignees, events, reactions, and subscriptions."),
|
||||
(name = "Wiki", description = "Wiki page management including CRUD operations, revision history, version comparison, and page reversion."),
|
||||
),
|
||||
paths(
|
||||
// Auth
|
||||
crate::api::auth::rsa::handle,
|
||||
crate::api::auth::captcha::handle,
|
||||
crate::api::auth::login::handle,
|
||||
crate::api::auth::logout::handle,
|
||||
crate::api::auth::me::handle,
|
||||
crate::api::auth::register_email_code::handle,
|
||||
crate::api::auth::register::handle,
|
||||
crate::api::auth::get_email::handle,
|
||||
crate::api::auth::request_email_change::handle,
|
||||
crate::api::auth::verify_email::handle,
|
||||
crate::api::auth::request_reset_password::handle,
|
||||
crate::api::auth::verify_reset_password::handle,
|
||||
crate::api::auth::get_2fa_status::handle,
|
||||
crate::api::auth::enable_2fa::handle,
|
||||
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
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
crate::api::issue::list_issue_labels::list_issue_labels,
|
||||
crate::api::issue::assign_label::assign_label,
|
||||
crate::api::issue::unassign_label::unassign_label,
|
||||
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,
|
||||
crate::api::issue::list_assignees::list_assignees,
|
||||
crate::api::issue::assign_issue::assign_issue,
|
||||
crate::api::issue::unassign_issue::unassign_issue,
|
||||
crate::api::issue::list_events::list_events,
|
||||
crate::api::issue::reactions::list_reactions,
|
||||
crate::api::issue::reactions::add_reaction,
|
||||
crate::api::issue::reactions::remove_reaction,
|
||||
crate::api::issue::subscribers::list_subscribers,
|
||||
crate::api::issue::subscribers::subscribe,
|
||||
crate::api::issue::subscribers::unsubscribe,
|
||||
crate::api::issue::subscribers::mute,
|
||||
crate::api::issue::templates::list_templates,
|
||||
crate::api::issue::templates::create_template,
|
||||
crate::api::issue::templates::update_template,
|
||||
crate::api::issue::templates::delete_template,
|
||||
crate::api::issue::repo_relations::list_repo_relations,
|
||||
crate::api::issue::repo_relations::link_repo,
|
||||
crate::api::issue::repo_relations::unlink_repo,
|
||||
crate::api::issue::pr_relations::list_pr_relations,
|
||||
crate::api::issue::pr_relations::link_pr,
|
||||
crate::api::issue::pr_relations::unlink_pr,
|
||||
// Pull Requests - Core
|
||||
crate::api::pr::list::list,
|
||||
crate::api::pr::get::get,
|
||||
crate::api::pr::create::create,
|
||||
crate::api::pr::update::update,
|
||||
crate::api::pr::mark_ready::mark_ready,
|
||||
crate::api::pr::close::close,
|
||||
crate::api::pr::reopen::reopen,
|
||||
crate::api::pr::delete::delete,
|
||||
crate::api::pr::lock::lock,
|
||||
crate::api::pr::merge::merge,
|
||||
// Pull Requests - Commits & Files
|
||||
crate::api::pr::list_commits::list_commits,
|
||||
crate::api::pr::list_files::list_files,
|
||||
// Pull Requests - Status & Merge Strategy
|
||||
crate::api::pr::get_status::get_status,
|
||||
crate::api::pr::merge_strategy::get_merge_strategy,
|
||||
crate::api::pr::merge_strategy::update_merge_strategy,
|
||||
// Pull Requests - Labels
|
||||
crate::api::pr::labels::list_labels,
|
||||
crate::api::pr::labels::create_label,
|
||||
crate::api::pr::labels::update_label,
|
||||
crate::api::pr::labels::delete_label,
|
||||
crate::api::pr::labels::list_label_relations,
|
||||
crate::api::pr::labels::assign_label,
|
||||
crate::api::pr::labels::unassign_label,
|
||||
// Pull Requests - Assignees
|
||||
crate::api::pr::assignees::list_assignees,
|
||||
crate::api::pr::assignees::assign_user,
|
||||
crate::api::pr::assignees::unassign_user,
|
||||
// Pull Requests - Reviews
|
||||
crate::api::pr::reviews::list_reviews,
|
||||
crate::api::pr::reviews::create_review,
|
||||
crate::api::pr::reviews::submit_review,
|
||||
crate::api::pr::reviews::dismiss_review,
|
||||
crate::api::pr::reviews::list_review_comments,
|
||||
crate::api::pr::reviews::add_review_reply,
|
||||
crate::api::pr::reviews::update_review_comment,
|
||||
crate::api::pr::reviews::delete_review_comment,
|
||||
// Pull Requests - Check Runs
|
||||
crate::api::pr::check_runs::list_check_runs,
|
||||
crate::api::pr::check_runs::create_check_run,
|
||||
crate::api::pr::check_runs::update_check_run,
|
||||
crate::api::pr::check_runs::delete_check_run,
|
||||
// Pull Requests - Events
|
||||
crate::api::pr::events::list_events,
|
||||
// Pull Requests - Reactions
|
||||
crate::api::pr::reactions::list_reactions,
|
||||
crate::api::pr::reactions::add_reaction,
|
||||
crate::api::pr::reactions::remove_reaction,
|
||||
// Pull Requests - Subscriptions
|
||||
crate::api::pr::subscriptions::list_subscriptions,
|
||||
crate::api::pr::subscriptions::subscribe,
|
||||
crate::api::pr::subscriptions::unsubscribe,
|
||||
crate::api::pr::subscriptions::mute,
|
||||
// Wiki
|
||||
crate::api::wiki::list_pages::list_pages,
|
||||
crate::api::wiki::get_page::get_page,
|
||||
crate::api::wiki::create_page::create_page,
|
||||
crate::api::wiki::update_page::update_page,
|
||||
crate::api::wiki::delete_page::delete_page,
|
||||
crate::api::wiki::revert_page::revert_page,
|
||||
crate::api::wiki::list_revisions::list_revisions,
|
||||
crate::api::wiki::get_revision::get_revision,
|
||||
crate::api::wiki::compare_revisions::compare_revisions,
|
||||
// Workspaces
|
||||
crate::api::workspace::list::handle,
|
||||
crate::api::workspace::get::handle,
|
||||
crate::api::workspace::create::handle,
|
||||
crate::api::workspace::update::handle,
|
||||
crate::api::workspace::archive::handle,
|
||||
crate::api::workspace::unarchive::handle,
|
||||
crate::api::workspace::delete::handle,
|
||||
crate::api::workspace::transfer_owner::handle,
|
||||
crate::api::workspace::upload_avatar::handle,
|
||||
crate::api::workspace::list_members::handle,
|
||||
crate::api::workspace::add_member::handle,
|
||||
crate::api::workspace::update_member_role::handle,
|
||||
crate::api::workspace::remove_member::handle,
|
||||
crate::api::workspace::leave::handle,
|
||||
crate::api::workspace::list_invitations::handle,
|
||||
crate::api::workspace::create_invitation::handle,
|
||||
crate::api::workspace::revoke_invitation::handle,
|
||||
crate::api::workspace::accept_invitation::handle,
|
||||
crate::api::workspace::get_billing::handle,
|
||||
crate::api::workspace::update_billing::handle,
|
||||
crate::api::workspace::get_branding::handle,
|
||||
crate::api::workspace::update_branding::handle,
|
||||
crate::api::workspace::get_settings::handle,
|
||||
crate::api::workspace::update_settings::handle,
|
||||
crate::api::workspace::get_stats::handle,
|
||||
crate::api::workspace::refresh_stats::handle,
|
||||
crate::api::workspace::list_integrations::handle,
|
||||
crate::api::workspace::create_integration::handle,
|
||||
crate::api::workspace::update_integration::handle,
|
||||
crate::api::workspace::delete_integration::handle,
|
||||
crate::api::workspace::list_webhooks::handle,
|
||||
crate::api::workspace::create_webhook::handle,
|
||||
crate::api::workspace::update_webhook::handle,
|
||||
crate::api::workspace::delete_webhook::handle,
|
||||
crate::api::workspace::list_domains::handle,
|
||||
crate::api::workspace::add_domain::handle,
|
||||
crate::api::workspace::verify_domain::handle,
|
||||
crate::api::workspace::set_primary_domain::handle,
|
||||
crate::api::workspace::delete_domain::handle,
|
||||
crate::api::workspace::list_approvals::handle,
|
||||
crate::api::workspace::request_approval::handle,
|
||||
crate::api::workspace::review_approval::handle,
|
||||
crate::api::workspace::audit_logs::handle,
|
||||
// Repos
|
||||
crate::api::repo::list::list,
|
||||
crate::api::repo::get::get,
|
||||
crate::api::repo::create::create,
|
||||
crate::api::repo::update::update,
|
||||
crate::api::repo::archive::archive,
|
||||
crate::api::repo::unarchive::unarchive,
|
||||
crate::api::repo::delete::delete,
|
||||
crate::api::repo::transfer_owner::transfer_owner,
|
||||
crate::api::repo::list_branches::list_branches,
|
||||
crate::api::repo::create_branch::create_branch,
|
||||
crate::api::repo::set_default_branch::set_default_branch,
|
||||
crate::api::repo::set_branch_protection::set_branch_protection,
|
||||
crate::api::repo::delete_branch::delete_branch,
|
||||
crate::api::repo::list_tags::list_tags,
|
||||
crate::api::repo::create_tag::create_tag,
|
||||
crate::api::repo::delete_tag::delete_tag,
|
||||
crate::api::repo::list_releases::list_releases,
|
||||
crate::api::repo::create_release::create_release,
|
||||
crate::api::repo::update_release::update_release,
|
||||
crate::api::repo::delete_release::delete_release,
|
||||
crate::api::repo::list_forks::list_forks,
|
||||
crate::api::repo::fork_repo::fork_repo,
|
||||
crate::api::repo::sync_fork::sync_fork,
|
||||
crate::api::repo::star_repo::star_repo,
|
||||
crate::api::repo::unstar_repo::unstar_repo,
|
||||
crate::api::repo::list_stargazers::list_stargazers,
|
||||
crate::api::repo::watch_repo::watch_repo,
|
||||
crate::api::repo::unwatch_repo::unwatch_repo,
|
||||
crate::api::repo::list_watchers::list_watchers,
|
||||
crate::api::repo::list_members::list_members,
|
||||
crate::api::repo::add_member::add_member,
|
||||
crate::api::repo::update_member_role::update_member_role,
|
||||
crate::api::repo::remove_member::remove_member,
|
||||
crate::api::repo::leave_repo::leave_repo,
|
||||
crate::api::repo::list_invitations::list_invitations,
|
||||
crate::api::repo::create_invitation::create_invitation,
|
||||
crate::api::repo::revoke_invitation::revoke_invitation,
|
||||
crate::api::repo::accept_invitation::accept_invitation,
|
||||
crate::api::repo::list_deploy_keys::list_deploy_keys,
|
||||
crate::api::repo::add_deploy_key::add_deploy_key,
|
||||
crate::api::repo::delete_deploy_key::delete_deploy_key,
|
||||
crate::api::repo::list_webhooks::list_webhooks,
|
||||
crate::api::repo::create_webhook::create_webhook,
|
||||
crate::api::repo::update_webhook::update_webhook,
|
||||
crate::api::repo::delete_webhook::delete_webhook,
|
||||
crate::api::repo::list_protection_rules::list_protection_rules,
|
||||
crate::api::repo::get_protection_rule::get_protection_rule,
|
||||
crate::api::repo::match_protection::match_protection,
|
||||
crate::api::repo::create_protection_rule::create_protection_rule,
|
||||
crate::api::repo::update_protection_rule::update_protection_rule,
|
||||
crate::api::repo::delete_protection_rule::delete_protection_rule,
|
||||
crate::api::repo::check_branch_merge::check_branch_merge,
|
||||
crate::api::repo::list_commit_statuses::list_commit_statuses,
|
||||
crate::api::repo::create_commit_status::create_commit_status,
|
||||
crate::api::repo::list_commit_comments::list_commit_comments,
|
||||
crate::api::repo::create_commit_comment::create_commit_comment,
|
||||
crate::api::repo::resolve_commit_comment::resolve_commit_comment,
|
||||
crate::api::repo::get_stats::get_stats,
|
||||
crate::api::repo::refresh_stats::refresh_stats,
|
||||
),
|
||||
components(schemas(
|
||||
ApiEmptyResponse,
|
||||
ApiErrorResponse,
|
||||
ApiResponse<RsaResponse>,
|
||||
ApiResponse<CaptchaResponse>,
|
||||
ApiResponse<ContextMe>,
|
||||
ApiResponse<RegisterEmailCodeResponse>,
|
||||
ApiResponse<RegisterResponse>,
|
||||
ApiResponse<EmailResponse>,
|
||||
ApiResponse<Get2FAStatusResponse>,
|
||||
ApiResponse<Enable2FAResponse>,
|
||||
ApiResponse<Regenerate2FABackupCodesResponse>,
|
||||
RsaResponse,
|
||||
CaptchaQuery,
|
||||
CaptchaResponse,
|
||||
LoginParams,
|
||||
ContextMe,
|
||||
RegisterEmailCodeParams,
|
||||
RegisterEmailCodeResponse,
|
||||
RegisterParams,
|
||||
RegisterResponse,
|
||||
EmailResponse,
|
||||
EmailChangeRequest,
|
||||
EmailVerifyRequest,
|
||||
ResetPasswordRequest,
|
||||
ResetPasswordVerifyParams,
|
||||
Get2FAStatusResponse,
|
||||
Enable2FAResponse,
|
||||
Verify2FAParams,
|
||||
Disable2FAParams,
|
||||
Regenerate2FABackupCodesRequest,
|
||||
Regenerate2FABackupCodesResponse,
|
||||
// User
|
||||
ApiResponse<User>,
|
||||
ApiResponse<UserAvatarResponse>,
|
||||
ApiResponse<String>,
|
||||
User,
|
||||
UpdateUserAccountParams,
|
||||
UploadUserAvatarParams,
|
||||
UserAvatarResponse,
|
||||
ApiResponse<UserAppearance>,
|
||||
UserAppearance,
|
||||
UpdateUserAppearanceParams,
|
||||
ApiResponse<UserProfile>,
|
||||
UserProfile,
|
||||
UpdateUserProfileParams,
|
||||
ApiResponse<UserNotifySetting>,
|
||||
UserNotifySetting,
|
||||
UpdateUserNotifySettingParams,
|
||||
ApiResponse<UserSshKey>,
|
||||
ApiResponse<Vec<UserSshKey>>,
|
||||
UserSshKey,
|
||||
AddSshKeyParams,
|
||||
ApiResponse<UserGpgKey>,
|
||||
ApiResponse<Vec<UserGpgKey>>,
|
||||
UserGpgKey,
|
||||
AddGpgKeyParams,
|
||||
ApiResponse<UserSessionInfo>,
|
||||
ApiResponse<Vec<UserSessionInfo>>,
|
||||
UserSessionInfo,
|
||||
ApiResponse<UserDevice>,
|
||||
ApiResponse<Vec<UserDevice>>,
|
||||
UserDevice,
|
||||
ApiResponse<UserOAuthInfo>,
|
||||
ApiResponse<Vec<UserOAuthInfo>>,
|
||||
UserOAuthInfo,
|
||||
ApiResponse<UserSecurityLog>,
|
||||
ApiResponse<Vec<UserSecurityLog>>,
|
||||
UserSecurityLog,
|
||||
ApiResponse<UserPersonalAccessTokenInfo>,
|
||||
ApiResponse<Vec<UserPersonalAccessTokenInfo>>,
|
||||
UserPersonalAccessTokenInfo,
|
||||
// Issues
|
||||
ApiResponse<Issue>,
|
||||
ApiResponse<Vec<Issue>>,
|
||||
Issue,
|
||||
CreateIssueParams,
|
||||
IssueListFilters,
|
||||
UpdateIssueParams,
|
||||
LockIssueParams,
|
||||
TransferIssueParams,
|
||||
ApiResponse<IssueComment>,
|
||||
ApiResponse<Vec<IssueComment>>,
|
||||
IssueComment,
|
||||
CreateCommentParams,
|
||||
UpdateCommentParams,
|
||||
ApiResponse<IssueLabel>,
|
||||
ApiResponse<Vec<IssueLabel>>,
|
||||
IssueLabel,
|
||||
CreateLabelParams,
|
||||
UpdateLabelParams,
|
||||
ApiResponse<IssueLabelRelation>,
|
||||
ApiResponse<Vec<IssueLabelRelation>>,
|
||||
IssueLabelRelation,
|
||||
ApiResponse<IssueMilestone>,
|
||||
ApiResponse<Vec<IssueMilestone>>,
|
||||
IssueMilestone,
|
||||
CreateMilestoneParams,
|
||||
UpdateMilestoneParams,
|
||||
ApiResponse<IssueAssignee>,
|
||||
ApiResponse<Vec<IssueAssignee>>,
|
||||
IssueAssignee,
|
||||
ApiResponse<IssueEvent>,
|
||||
ApiResponse<Vec<IssueEvent>>,
|
||||
IssueEvent,
|
||||
ApiResponse<IssueReaction>,
|
||||
ApiResponse<Vec<IssueReaction>>,
|
||||
IssueReaction,
|
||||
CreateIssueReactionParams,
|
||||
ApiResponse<IssueSubscriber>,
|
||||
ApiResponse<Vec<IssueSubscriber>>,
|
||||
IssueSubscriber,
|
||||
MuteIssueParams,
|
||||
ApiResponse<IssueTemplate>,
|
||||
ApiResponse<Vec<IssueTemplate>>,
|
||||
IssueTemplate,
|
||||
CreateTemplateParams,
|
||||
UpdateTemplateParams,
|
||||
ApiResponse<IssueRepoRelation>,
|
||||
ApiResponse<Vec<IssueRepoRelation>>,
|
||||
IssueRepoRelation,
|
||||
LinkRepoParams,
|
||||
ApiResponse<IssuePrRelation>,
|
||||
ApiResponse<Vec<IssuePrRelation>>,
|
||||
IssuePrRelation,
|
||||
LinkPrParams,
|
||||
// Pull Requests
|
||||
ApiResponse<PullRequest>,
|
||||
ApiResponse<Vec<PullRequest>>,
|
||||
PullRequest,
|
||||
CreatePrParams,
|
||||
UpdatePrParams,
|
||||
PrListFilters,
|
||||
MergePrParams,
|
||||
LockPrParams,
|
||||
ApiResponse<PrCommit>,
|
||||
ApiResponse<Vec<PrCommit>>,
|
||||
PrCommit,
|
||||
ApiResponse<PrFile>,
|
||||
ApiResponse<Vec<PrFile>>,
|
||||
PrFile,
|
||||
ApiResponse<PrStatus>,
|
||||
PrStatus,
|
||||
ApiResponse<PrMergeStrategy>,
|
||||
PrMergeStrategy,
|
||||
UpdateMergeStrategyParams,
|
||||
ApiResponse<PrLabel>,
|
||||
ApiResponse<Vec<PrLabel>>,
|
||||
PrLabel,
|
||||
CreatePrLabelParams,
|
||||
UpdatePrLabelParams,
|
||||
ApiResponse<PrLabelRelation>,
|
||||
ApiResponse<Vec<PrLabelRelation>>,
|
||||
PrLabelRelation,
|
||||
ApiResponse<PrAssignee>,
|
||||
ApiResponse<Vec<PrAssignee>>,
|
||||
PrAssignee,
|
||||
ApiResponse<PrReview>,
|
||||
ApiResponse<Vec<PrReview>>,
|
||||
PrReview,
|
||||
CreateReviewParams,
|
||||
ReviewCommentParams,
|
||||
SubmitReviewParams,
|
||||
DismissReviewParams,
|
||||
AddReplyParams,
|
||||
ApiResponse<PrReviewComment>,
|
||||
ApiResponse<Vec<PrReviewComment>>,
|
||||
PrReviewComment,
|
||||
ApiResponse<PrCheckRun>,
|
||||
ApiResponse<Vec<PrCheckRun>>,
|
||||
PrCheckRun,
|
||||
CreateCheckRunParams,
|
||||
UpdateCheckRunParams,
|
||||
ApiResponse<PrEvent>,
|
||||
ApiResponse<Vec<PrEvent>>,
|
||||
PrEvent,
|
||||
ApiResponse<PrReaction>,
|
||||
ApiResponse<Vec<PrReaction>>,
|
||||
PrReaction,
|
||||
CreateReactionParams,
|
||||
ApiResponse<PrSubscription>,
|
||||
ApiResponse<Vec<PrSubscription>>,
|
||||
PrSubscription,
|
||||
MutePrParams,
|
||||
// Wiki
|
||||
ApiResponse<WikiPage>,
|
||||
ApiResponse<Vec<WikiPage>>,
|
||||
WikiPage,
|
||||
CreateWikiPageParams,
|
||||
UpdateWikiPageParams,
|
||||
ApiResponse<WikiPageRevision>,
|
||||
ApiResponse<Vec<WikiPageRevision>>,
|
||||
ApiResponse<WikiCompareResult>,
|
||||
WikiCompareResult,
|
||||
WikiPageRevision,
|
||||
// Workspaces
|
||||
ApiResponse<Workspace>,
|
||||
ApiResponse<Vec<Workspace>>,
|
||||
ApiResponse<WorkspaceMember>,
|
||||
ApiResponse<Vec<WorkspaceMember>>,
|
||||
ApiResponse<CreateInvitationResponse>,
|
||||
ApiResponse<Vec<WorkspaceInvitation>>,
|
||||
ApiResponse<WorkspaceInvitation>,
|
||||
ApiResponse<WorkspaceBilling>,
|
||||
ApiResponse<WorkspaceCustomBranding>,
|
||||
ApiResponse<WorkspaceSettings>,
|
||||
ApiResponse<WorkspaceStats>,
|
||||
ApiResponse<WorkspaceIntegration>,
|
||||
ApiResponse<Vec<WorkspaceIntegration>>,
|
||||
ApiResponse<WorkspaceWebhook>,
|
||||
ApiResponse<Vec<WorkspaceWebhook>>,
|
||||
ApiResponse<WorkspaceDomain>,
|
||||
ApiResponse<Vec<WorkspaceDomain>>,
|
||||
ApiResponse<WorkspacePendingApproval>,
|
||||
ApiResponse<Vec<WorkspacePendingApproval>>,
|
||||
ApiResponse<Vec<WorkspaceAuditLog>>,
|
||||
Workspace,
|
||||
CreateWorkspaceParams,
|
||||
UpdateWorkspaceParams,
|
||||
TransferOwnerRequest,
|
||||
WorkspaceMember,
|
||||
AddMemberParams,
|
||||
UpdateMemberRoleParams,
|
||||
WorkspaceInvitation,
|
||||
CreateInvitationParams,
|
||||
CreateInvitationResponse,
|
||||
AcceptInvitationRequest,
|
||||
WorkspaceBilling,
|
||||
UpdateBillingParams,
|
||||
WorkspaceCustomBranding,
|
||||
UpdateBrandingParams,
|
||||
WorkspaceSettings,
|
||||
UpdateWorkspaceSettingsParams,
|
||||
WorkspaceStats,
|
||||
WorkspaceIntegration,
|
||||
CreateIntegrationParams,
|
||||
UpdateIntegrationParams,
|
||||
WorkspaceWebhook,
|
||||
CreateWebhookParams,
|
||||
UpdateWebhookParams,
|
||||
WorkspaceDomain,
|
||||
AddDomainParams,
|
||||
WorkspacePendingApproval,
|
||||
RequestApprovalParams,
|
||||
ReviewApprovalRequest,
|
||||
WorkspaceAuditLog,
|
||||
// Repos
|
||||
ApiResponse<Repo>,
|
||||
ApiResponse<Vec<Repo>>,
|
||||
ApiResponse<RepoBranch>,
|
||||
ApiResponse<Vec<RepoBranch>>,
|
||||
ApiResponse<RepoTag>,
|
||||
ApiResponse<Vec<RepoTag>>,
|
||||
ApiResponse<RepoRelease>,
|
||||
ApiResponse<Vec<RepoRelease>>,
|
||||
ApiResponse<RepoFork>,
|
||||
ApiResponse<Vec<RepoFork>>,
|
||||
ApiResponse<RepoStar>,
|
||||
ApiResponse<Vec<RepoStar>>,
|
||||
ApiResponse<RepoWatch>,
|
||||
ApiResponse<Vec<RepoWatch>>,
|
||||
ApiResponse<RepoMember>,
|
||||
ApiResponse<Vec<RepoMember>>,
|
||||
ApiResponse<RepoInvitation>,
|
||||
ApiResponse<Vec<RepoInvitation>>,
|
||||
ApiResponse<RepoDeployKey>,
|
||||
ApiResponse<Vec<RepoDeployKey>>,
|
||||
ApiResponse<RepoWebhook>,
|
||||
ApiResponse<Vec<RepoWebhook>>,
|
||||
ApiResponse<BranchProtectionRule>,
|
||||
ApiResponse<Vec<BranchProtectionRule>>,
|
||||
ApiResponse<Option<BranchProtectionRule>>,
|
||||
ApiResponse<BranchMergeCheck>,
|
||||
ApiResponse<RepoCommitStatus>,
|
||||
ApiResponse<Vec<RepoCommitStatus>>,
|
||||
ApiResponse<RepoCommitComment>,
|
||||
ApiResponse<Vec<RepoCommitComment>>,
|
||||
ApiResponse<RepoStats>,
|
||||
Repo,
|
||||
CreateRepoParams,
|
||||
UpdateRepoParams,
|
||||
TransferOwnerParams,
|
||||
RepoBranch,
|
||||
CreateBranchParams,
|
||||
SetBranchProtectionParams,
|
||||
RepoTag,
|
||||
CreateTagParams,
|
||||
RepoRelease,
|
||||
CreateReleaseParams,
|
||||
UpdateReleaseParams,
|
||||
RepoFork,
|
||||
ForkRepoParams,
|
||||
RepoStar,
|
||||
RepoWatch,
|
||||
WatchParams,
|
||||
RepoMember,
|
||||
AddRepoMemberParams,
|
||||
UpdateRepoMemberRoleParams,
|
||||
RepoInvitation,
|
||||
CreateRepoInvitationParams,
|
||||
AcceptInvitationParams,
|
||||
RepoDeployKey,
|
||||
AddDeployKeyParams,
|
||||
RepoWebhook,
|
||||
RepoCreateWebhookParams,
|
||||
RepoUpdateWebhookParams,
|
||||
BranchProtectionRule,
|
||||
CreateProtectionRuleParams,
|
||||
UpdateProtectionRuleParams,
|
||||
BranchMergeCheck,
|
||||
RepoCommitStatus,
|
||||
CreateCommitStatusParams,
|
||||
RepoCommitComment,
|
||||
CreateCommitCommentParams,
|
||||
RepoStats,
|
||||
))
|
||||
)]
|
||||
pub struct OpenApiDoc;
|
||||
@@ -1,141 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PrAssignee;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PrPath {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct UserPath {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
/// User ID (UUID) to assign/unassign
|
||||
pub user_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QP {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List assignees of a PR
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/assignees",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prListAssignees",
|
||||
params(PrPath, QP),
|
||||
responses(
|
||||
(status = 200, description = "Assignees listed.", body = ApiResponse<Vec<PrAssignee>>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_assignees(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PrPath>,
|
||||
query: web::Query<QP>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let assignees = service
|
||||
.pr
|
||||
.pr_assignees(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(assignees)))
|
||||
}
|
||||
|
||||
/// Assign a user to a PR. Assignee is auto-subscribed.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/assignees/{user_id}",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prAssign",
|
||||
params(UserPath),
|
||||
responses(
|
||||
(status = 201, description = "User assigned.", body = ApiResponse<PrAssignee>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "User already assigned", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn assign_user(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<UserPath>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let assignee = service
|
||||
.pr
|
||||
.pr_assign(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
path.user_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(assignee)))
|
||||
}
|
||||
|
||||
/// Unassign a user from a PR
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/assignees/{user_id}",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prUnassign",
|
||||
params(UserPath),
|
||||
responses(
|
||||
(status = 200, description = "User unassigned.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "Assignee not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn unassign_user(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<UserPath>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.pr
|
||||
.pr_unassign(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
path.user_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("User unassigned".to_string())))
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PrCheckRun;
|
||||
use crate::service::AppService;
|
||||
use crate::service::pr::check_runs::{CreateCheckRunParams, UpdateCheckRunParams};
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PrPath {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct CheckRunPath {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
/// Check run ID (UUID)
|
||||
pub check_run_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QP {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List check runs for a PR
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/check-runs",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prListCheckRuns",
|
||||
params(PrPath, QP),
|
||||
responses(
|
||||
(status = 200, description = "Check runs listed.", body = ApiResponse<Vec<PrCheckRun>>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_check_runs(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PrPath>,
|
||||
query: web::Query<QP>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let runs = service
|
||||
.pr
|
||||
.pr_check_runs(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(runs)))
|
||||
}
|
||||
|
||||
/// Create a check run. Requires Member role.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/check-runs",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prCreateCheckRun",
|
||||
params(PrPath),
|
||||
request_body(content = CreateCheckRunParams, description = "Check run parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 201, description = "Check run created.", body = ApiResponse<PrCheckRun>),
|
||||
(status = 400, description = "Invalid status", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn create_check_run(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PrPath>,
|
||||
params: web::Json<CreateCheckRunParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let run = service
|
||||
.pr
|
||||
.pr_create_check_run(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(run)))
|
||||
}
|
||||
|
||||
/// Update a check run. Requires Member role.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/check-runs/{check_run_id}",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prUpdateCheckRun",
|
||||
params(CheckRunPath),
|
||||
request_body(content = UpdateCheckRunParams, description = "Check run update parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 200, description = "Check run updated.", body = ApiResponse<PrCheckRun>),
|
||||
(status = 400, description = "Invalid status", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "Check run not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn update_check_run(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<CheckRunPath>,
|
||||
params: web::Json<UpdateCheckRunParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let run = service
|
||||
.pr
|
||||
.pr_update_check_run(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
path.check_run_id,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(run)))
|
||||
}
|
||||
|
||||
/// Delete a check run. Requires Admin role.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/check-runs/{check_run_id}",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prDeleteCheckRun",
|
||||
params(CheckRunPath),
|
||||
responses(
|
||||
(status = 200, description = "Check run deleted.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (Admin required)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Check run not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn delete_check_run(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<CheckRunPath>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.pr
|
||||
.pr_delete_check_run(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
path.check_run_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Check run deleted".to_string())))
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PullRequest;
|
||||
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,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
/// Close a pull request
|
||||
///
|
||||
/// Closes an open PR without merging.
|
||||
/// Requires write access to the PR.
|
||||
///
|
||||
/// Returns the closed PR.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/close",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prClose",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "PR closed successfully.", body = ApiResponse<PullRequest>),
|
||||
(status = 400, description = "PR is not open", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn close(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let pr = service
|
||||
.pr
|
||||
.pr_close(&session, &path.workspace_name, &path.repo_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(pr)))
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PullRequest;
|
||||
use crate::service::AppService;
|
||||
use crate::service::pr::core::CreatePrParams;
|
||||
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 pull request
|
||||
///
|
||||
/// Creates a new pull request proposing changes from a source branch to a target branch.
|
||||
/// Requires at least Member role in the repository.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - title: PR title (required)
|
||||
/// - body: PR description in markdown (optional)
|
||||
/// - source_repo_id: Source repository ID (supports cross-repo PRs from forks)
|
||||
/// - source_branch: Source branch name (must exist)
|
||||
/// - target_branch: Target branch name (must exist in the repo)
|
||||
/// - head_commit_sha: Head commit SHA from the source branch
|
||||
/// - base_commit_sha: Base commit SHA for diff calculation (optional)
|
||||
/// - draft: Whether this is a draft PR (optional, defaults to false)
|
||||
///
|
||||
/// Effects:
|
||||
/// - PR is created with auto-incrementing number
|
||||
/// - PR status tracking is initialized
|
||||
/// - Author is automatically subscribed
|
||||
/// - Repository stats are updated
|
||||
///
|
||||
/// Returns the created PR with full metadata.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prCreate",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CreatePrParams,
|
||||
description = "PR creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "PR created successfully. Returns the newly created PR with full metadata.", body = ApiResponse<PullRequest>),
|
||||
(status = 400, description = "Invalid parameters: empty title, non-existent branch/commit, or invalid fork relationship", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 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(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CreatePrParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let pr = service
|
||||
.pr
|
||||
.pr_create(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(pr)))
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
/// Delete a pull request
|
||||
///
|
||||
/// Soft-deletes a PR. Requires Admin role in the repository.
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prDelete",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "PR deleted successfully.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Admin role)", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn delete(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.pr
|
||||
.pr_delete(&session, &path.workspace_name, &path.repo_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("PR deleted".to_string())))
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PrEvent;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PrPath {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QP {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List PR events (timeline). Returns chronological activity log.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/events",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prListEvents",
|
||||
params(PrPath, QP),
|
||||
responses(
|
||||
(status = 200, description = "Events listed.", body = ApiResponse<Vec<PrEvent>>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_events(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PrPath>,
|
||||
query: web::Query<QP>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let events = service
|
||||
.pr
|
||||
.pr_list_events(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(events)))
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PullRequest;
|
||||
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,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
/// Get a pull request by number
|
||||
///
|
||||
/// Returns detailed information about a specific pull request.
|
||||
/// Requires read access to the repository.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prGet",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "PR retrieved successfully. Returns complete PR with all metadata.", body = ApiResponse<PullRequest>),
|
||||
(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, workspace, or PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let pr = service
|
||||
.pr
|
||||
.pr_get(&session, &path.workspace_name, &path.repo_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(pr)))
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PrStatus;
|
||||
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,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
/// Get PR status summary
|
||||
///
|
||||
/// Returns the current status of a PR including checks state, mergeability,
|
||||
/// approval count, and file change statistics.
|
||||
/// Requires read access to the repository.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/status",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prGetStatus",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "PR status retrieved successfully.", body = ApiResponse<PrStatus>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR or status not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn get_status(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let status = service
|
||||
.pr
|
||||
.pr_status(&session, &path.workspace_name, &path.repo_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(status)))
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::{PrLabel, PrLabelRelation};
|
||||
use crate::service::AppService;
|
||||
use crate::service::pr::labels::{CreatePrLabelParams, UpdatePrLabelParams};
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct RepoPath {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PrPath {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct LabelPath {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
/// Label ID (UUID)
|
||||
pub label_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct LabelIdPath {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// Label ID (UUID)
|
||||
pub label_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QP {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
// ── Repo-level labels ──
|
||||
|
||||
/// List PR labels in a repository
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/labels",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prListLabels",
|
||||
params(RepoPath),
|
||||
responses(
|
||||
(status = 200, description = "Labels listed successfully.", body = ApiResponse<Vec<PrLabel>>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repo not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_labels(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<RepoPath>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let labels = service
|
||||
.pr
|
||||
.pr_labels(&session, &path.workspace_name, &path.repo_name)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(labels)))
|
||||
}
|
||||
|
||||
/// Create a PR label. Requires Member role.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/labels",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prCreateLabel",
|
||||
params(RepoPath),
|
||||
request_body(content = CreatePrLabelParams, description = "Label creation parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 201, description = "Label created.", body = ApiResponse<PrLabel>),
|
||||
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repo not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn create_label(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<RepoPath>,
|
||||
params: web::Json<CreatePrLabelParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let label = service
|
||||
.pr
|
||||
.pr_create_label(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(label)))
|
||||
}
|
||||
|
||||
/// Update a PR label. Requires Admin role.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/labels/{label_id}",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prUpdateLabel",
|
||||
params(LabelIdPath),
|
||||
request_body(content = UpdatePrLabelParams, description = "Label update parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 200, description = "Label updated.", body = ApiResponse<PrLabel>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (Admin required)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Label not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn update_label(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<LabelIdPath>,
|
||||
params: web::Json<UpdatePrLabelParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let label = service
|
||||
.pr
|
||||
.pr_update_label(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.label_id,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(label)))
|
||||
}
|
||||
|
||||
/// Delete a PR label. Requires Admin role.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/labels/{label_id}",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prDeleteLabel",
|
||||
params(LabelIdPath),
|
||||
responses(
|
||||
(status = 200, description = "Label deleted.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (Admin required)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Label not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn delete_label(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<LabelIdPath>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.pr
|
||||
.pr_delete_label(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.label_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Label deleted".to_string())))
|
||||
}
|
||||
|
||||
// ── PR-level label relations ──
|
||||
|
||||
/// List labels assigned to a PR
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/labels",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prListLabelRelations",
|
||||
params(PrPath, QP),
|
||||
responses(
|
||||
(status = 200, description = "Label relations listed.", body = ApiResponse<Vec<PrLabelRelation>>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_label_relations(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PrPath>,
|
||||
query: web::Query<QP>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let rels = service
|
||||
.pr
|
||||
.pr_label_relations(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(rels)))
|
||||
}
|
||||
|
||||
/// Assign a label to a PR
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/labels/{label_id}",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prAssignLabel",
|
||||
params(LabelPath),
|
||||
responses(
|
||||
(status = 200, description = "Label assigned.", body = ApiResponse<PrLabelRelation>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR or label not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn assign_label(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<LabelPath>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let rel = service
|
||||
.pr
|
||||
.pr_assign_label(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
path.label_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(rel)))
|
||||
}
|
||||
|
||||
/// Unassign a label from a PR
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/labels/{label_id}",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prUnassignLabel",
|
||||
params(LabelPath),
|
||||
responses(
|
||||
(status = 200, description = "Label unassigned.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "Label not assigned", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn unassign_label(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<LabelPath>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.pr
|
||||
.pr_unassign_label(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
path.label_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Label unassigned".to_string())))
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PullRequest;
|
||||
use crate::service::AppService;
|
||||
use crate::service::pr::core::PrListFilters;
|
||||
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 {
|
||||
/// Filter by PR state ("open", "closed", "merged")
|
||||
pub state: Option<String>,
|
||||
/// Filter by author user ID
|
||||
pub author_id: Option<uuid::Uuid>,
|
||||
/// Filter by draft status
|
||||
pub draft: Option<bool>,
|
||||
/// Maximum number of PRs to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of PRs to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List pull requests in a repository
|
||||
///
|
||||
/// Returns a paginated list of pull requests, sorted by number (newest first).
|
||||
/// Supports filtering by state, author, and draft status.
|
||||
/// Requires read access to the repository.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prList",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "PRs listed successfully. Returns filtered array of PR objects.", body = ApiResponse<Vec<PullRequest>>),
|
||||
(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(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let filters = PrListFilters {
|
||||
state: query.state.clone(),
|
||||
author_id: query.author_id,
|
||||
draft: query.draft,
|
||||
};
|
||||
let prs = service
|
||||
.pr
|
||||
.pr_list(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
filters,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(prs)))
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PrCommit;
|
||||
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,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of commits to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of commits to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List commits in a pull request
|
||||
///
|
||||
/// Returns a paginated list of all commits included in the PR, sorted by position.
|
||||
/// Requires read access to the repository.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/commits",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prListCommits",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Commits listed successfully.", body = ApiResponse<Vec<PrCommit>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_commits(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let commits = service
|
||||
.pr
|
||||
.pr_commits(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(commits)))
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PrFile;
|
||||
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,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
/// Maximum number of files to return (default: 50, max: 100)
|
||||
pub limit: Option<i64>,
|
||||
/// Number of files to skip for pagination (default: 0)
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List changed files in a pull request
|
||||
///
|
||||
/// Returns a paginated list of all files changed in the PR, sorted by path.
|
||||
/// Includes additions, deletions, and patch diffs for each file.
|
||||
/// Requires read access to the repository.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/files",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prListFiles",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Files listed successfully.", body = ApiResponse<Vec<PrFile>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_files(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let files = service
|
||||
.pr
|
||||
.pr_files(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(files)))
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PullRequest;
|
||||
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,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct LockPrParams {
|
||||
/// Whether to lock (true) or unlock (false) the PR conversation
|
||||
pub locked: bool,
|
||||
}
|
||||
|
||||
/// Lock or unlock a pull request conversation
|
||||
///
|
||||
/// When locked, only repository maintainers and admins can comment on the PR.
|
||||
/// Requires write access to the PR.
|
||||
///
|
||||
/// Returns the updated PR.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/lock",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prLock",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = LockPrParams,
|
||||
description = "Lock/unlock parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "PR lock status updated.", body = ApiResponse<PullRequest>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn lock(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<LockPrParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let pr = service
|
||||
.pr
|
||||
.pr_lock(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
params.locked,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(pr)))
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PullRequest;
|
||||
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,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
/// Mark a draft PR as ready for review
|
||||
///
|
||||
/// Converts a draft pull request to a ready-for-review state.
|
||||
/// Requires write access to the PR.
|
||||
///
|
||||
/// Effects:
|
||||
/// - Draft flag is set to false
|
||||
/// - A "DraftReady" event is logged
|
||||
/// - Reviewers are notified that the PR is ready
|
||||
///
|
||||
/// Returns the updated PR.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/ready",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prMarkReady",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "PR marked as ready for review.", body = ApiResponse<PullRequest>),
|
||||
(status = 400, description = "PR is already ready for review", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn mark_ready(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let pr = service
|
||||
.pr
|
||||
.pr_mark_ready(&session, &path.workspace_name, &path.repo_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(pr)))
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PullRequest;
|
||||
use crate::service::AppService;
|
||||
use crate::service::pr::core::MergePrParams;
|
||||
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,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
/// Merge a pull request
|
||||
///
|
||||
/// Merges the source branch into the target branch.
|
||||
/// Requires at least Maintainer role in the repository.
|
||||
///
|
||||
/// Branch protection rules are enforced:
|
||||
/// - Required approvals count (self-approval not allowed)
|
||||
/// - Required status checks must pass
|
||||
/// - Admins can bypass protection rules
|
||||
///
|
||||
/// Parameters:
|
||||
/// - strategy: Merge strategy ("merge", "squash", "rebase", default: "merge")
|
||||
/// - squash_title: Custom title for squash merge (optional)
|
||||
/// - squash_message: Custom message for squash merge (optional)
|
||||
/// - delete_source_branch: Delete source branch after merge (optional, only same-repo)
|
||||
///
|
||||
/// Returns the merged PR with merge commit SHA.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/merge",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prMerge",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = MergePrParams,
|
||||
description = "Merge parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "PR merged successfully. Returns the merged PR with merge commit SHA.", body = ApiResponse<PullRequest>),
|
||||
(status = 400, description = "Cannot merge: PR not open, is draft, or branch protection requirements not met", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Maintainer role or higher)", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error or git merge failure", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn merge(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<MergePrParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let pr = service
|
||||
.pr
|
||||
.pr_merge(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(pr)))
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PrMergeStrategy;
|
||||
use crate::service::AppService;
|
||||
use crate::service::pr::merge_strategy::UpdateMergeStrategyParams;
|
||||
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,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
/// Get PR merge strategy
|
||||
///
|
||||
/// Returns the current merge strategy settings for a PR.
|
||||
/// Requires read access to the repository.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/merge-strategy",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prGetMergeStrategy",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Merge strategy retrieved successfully.", body = ApiResponse<PrMergeStrategy>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR or merge strategy not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn get_merge_strategy(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let strategy = service
|
||||
.pr
|
||||
.pr_merge_strategy(&session, &path.workspace_name, &path.repo_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(strategy)))
|
||||
}
|
||||
|
||||
/// Update PR merge strategy
|
||||
///
|
||||
/// Updates the merge strategy settings for a PR.
|
||||
/// Requires write access to the PR.
|
||||
///
|
||||
/// Updatable fields:
|
||||
/// - strategy: Merge strategy type ("merge", "squash", "rebase")
|
||||
/// - auto_merge: Enable auto-merge when all checks pass
|
||||
/// - squash_title: Custom title for squash merge
|
||||
/// - squash_message: Custom message for squash merge
|
||||
/// - delete_source_branch: Delete source branch after merge
|
||||
/// - merge_when_checks_pass: Auto-merge when checks pass
|
||||
///
|
||||
/// All fields are optional; only provided fields are updated.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/merge-strategy",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prUpdateMergeStrategy",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = UpdateMergeStrategyParams,
|
||||
description = "Merge strategy update parameters (all fields optional)",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Merge strategy updated successfully.", body = ApiResponse<PrMergeStrategy>),
|
||||
(status = 400, description = "Invalid parameters: unsupported strategy", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn update_merge_strategy(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<UpdateMergeStrategyParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let strategy = service
|
||||
.pr
|
||||
.pr_update_merge_strategy(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(strategy)))
|
||||
}
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
pub mod assignees;
|
||||
pub mod check_runs;
|
||||
pub mod close;
|
||||
pub mod create;
|
||||
pub mod delete;
|
||||
pub mod events;
|
||||
pub mod get;
|
||||
pub mod get_status;
|
||||
pub mod labels;
|
||||
pub mod list;
|
||||
pub mod list_commits;
|
||||
pub mod list_files;
|
||||
pub mod lock;
|
||||
pub mod mark_ready;
|
||||
pub mod merge;
|
||||
pub mod merge_strategy;
|
||||
pub mod reactions;
|
||||
pub mod reopen;
|
||||
pub mod reviews;
|
||||
pub mod subscriptions;
|
||||
pub mod update;
|
||||
|
||||
use actix_web::web;
|
||||
|
||||
/// Configure PR-level routes under `/workspaces/{workspace_name}/repos/{repo_name}/prs`
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/prs")
|
||||
// Repo-level labels
|
||||
.route("/labels", web::get().to(labels::list_labels))
|
||||
.route("/labels", web::post().to(labels::create_label))
|
||||
.route("/labels/{label_id}", web::put().to(labels::update_label))
|
||||
.route("/labels/{label_id}", web::delete().to(labels::delete_label))
|
||||
// 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}/ready", web::post().to(mark_ready::mark_ready))
|
||||
.route("/{number}/lock", web::put().to(lock::lock))
|
||||
.route("/{number}/merge", web::post().to(merge::merge))
|
||||
// Commits & Files
|
||||
.route(
|
||||
"/{number}/commits",
|
||||
web::get().to(list_commits::list_commits),
|
||||
)
|
||||
.route("/{number}/files", web::get().to(list_files::list_files))
|
||||
// Status & Merge Strategy
|
||||
.route("/{number}/status", web::get().to(get_status::get_status))
|
||||
.route(
|
||||
"/{number}/merge-strategy",
|
||||
web::get().to(merge_strategy::get_merge_strategy),
|
||||
)
|
||||
.route(
|
||||
"/{number}/merge-strategy",
|
||||
web::put().to(merge_strategy::update_merge_strategy),
|
||||
)
|
||||
// Labels (PR-level)
|
||||
.route(
|
||||
"/{number}/labels",
|
||||
web::get().to(labels::list_label_relations),
|
||||
)
|
||||
.route(
|
||||
"/{number}/labels/{label_id}",
|
||||
web::post().to(labels::assign_label),
|
||||
)
|
||||
.route(
|
||||
"/{number}/labels/{label_id}",
|
||||
web::delete().to(labels::unassign_label),
|
||||
)
|
||||
// Assignees
|
||||
.route(
|
||||
"/{number}/assignees",
|
||||
web::get().to(assignees::list_assignees),
|
||||
)
|
||||
.route(
|
||||
"/{number}/assignees/{user_id}",
|
||||
web::post().to(assignees::assign_user),
|
||||
)
|
||||
.route(
|
||||
"/{number}/assignees/{user_id}",
|
||||
web::delete().to(assignees::unassign_user),
|
||||
)
|
||||
// Reviews
|
||||
.route("/{number}/reviews", web::get().to(reviews::list_reviews))
|
||||
.route("/{number}/reviews", web::post().to(reviews::create_review))
|
||||
.route(
|
||||
"/{number}/reviews/{review_id}/submit",
|
||||
web::post().to(reviews::submit_review),
|
||||
)
|
||||
.route(
|
||||
"/{number}/reviews/{review_id}/dismiss",
|
||||
web::post().to(reviews::dismiss_review),
|
||||
)
|
||||
.route(
|
||||
"/{number}/reviews/{review_id}/comments",
|
||||
web::get().to(reviews::list_review_comments),
|
||||
)
|
||||
.route(
|
||||
"/{number}/comments/{comment_id}/reply",
|
||||
web::post().to(reviews::add_review_reply),
|
||||
)
|
||||
.route(
|
||||
"/{number}/comments/{comment_id}",
|
||||
web::put().to(reviews::update_review_comment),
|
||||
)
|
||||
.route(
|
||||
"/{number}/comments/{comment_id}",
|
||||
web::delete().to(reviews::delete_review_comment),
|
||||
)
|
||||
// Check Runs
|
||||
.route(
|
||||
"/{number}/check-runs",
|
||||
web::get().to(check_runs::list_check_runs),
|
||||
)
|
||||
.route(
|
||||
"/{number}/check-runs",
|
||||
web::post().to(check_runs::create_check_run),
|
||||
)
|
||||
.route(
|
||||
"/{number}/check-runs/{check_run_id}",
|
||||
web::put().to(check_runs::update_check_run),
|
||||
)
|
||||
.route(
|
||||
"/{number}/check-runs/{check_run_id}",
|
||||
web::delete().to(check_runs::delete_check_run),
|
||||
)
|
||||
// Events
|
||||
.route("/{number}/events", web::get().to(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),
|
||||
)
|
||||
// Subscriptions
|
||||
.route(
|
||||
"/{number}/subscriptions",
|
||||
web::get().to(subscriptions::list_subscriptions),
|
||||
)
|
||||
.route(
|
||||
"/{number}/subscribe",
|
||||
web::post().to(subscriptions::subscribe),
|
||||
)
|
||||
.route(
|
||||
"/{number}/subscribe",
|
||||
web::delete().to(subscriptions::unsubscribe),
|
||||
)
|
||||
.route("/{number}/mute", web::put().to(subscriptions::mute)),
|
||||
);
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PrReaction;
|
||||
use crate::service::AppService;
|
||||
use crate::service::pr::reactions::CreateReactionParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PrPath {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct ReactionPath {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
/// Reaction ID (UUID)
|
||||
pub reaction_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QP {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List reactions on a PR
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/reactions",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prListReactions",
|
||||
params(PrPath, QP),
|
||||
responses(
|
||||
(status = 200, description = "Reactions listed.", body = ApiResponse<Vec<PrReaction>>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_reactions(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PrPath>,
|
||||
query: web::Query<QP>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let reactions = service
|
||||
.pr
|
||||
.pr_reactions(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_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 a PR
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/reactions",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prAddReaction",
|
||||
params(PrPath),
|
||||
request_body(content = CreateReactionParams, description = "Reaction parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 201, description = "Reaction added.", body = ApiResponse<PrReaction>),
|
||||
(status = 400, description = "Invalid content or target type", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn add_reaction(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PrPath>,
|
||||
params: web::Json<CreateReactionParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let reaction = service
|
||||
.pr
|
||||
.pr_add_reaction(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(reaction)))
|
||||
}
|
||||
|
||||
/// Remove a reaction. Only the reaction author can remove it.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/reactions/{reaction_id}",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prRemoveReaction",
|
||||
params(ReactionPath),
|
||||
responses(
|
||||
(status = 200, description = "Reaction removed.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Cannot remove another user's reaction", body = ApiErrorResponse),
|
||||
(status = 404, description = "Reaction not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn remove_reaction(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<ReactionPath>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.pr
|
||||
.pr_remove_reaction(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
path.reaction_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Reaction removed".to_string())))
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PullRequest;
|
||||
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,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
/// Reopen a pull request
|
||||
///
|
||||
/// Reopens a closed (but not merged) PR.
|
||||
/// Requires write access to the PR.
|
||||
///
|
||||
/// Returns the reopened PR.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/reopen",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prReopen",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "PR reopened successfully.", body = ApiResponse<PullRequest>),
|
||||
(status = 400, description = "PR is not closed or already merged", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn reopen(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let pr = service
|
||||
.pr
|
||||
.pr_reopen(&session, &path.workspace_name, &path.repo_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(pr)))
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::{PrReview, PrReviewComment};
|
||||
use crate::service::AppService;
|
||||
use crate::service::pr::reviews::{
|
||||
AddReplyParams, CreateReviewParams, DismissReviewParams, SubmitReviewParams,
|
||||
};
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PrPath {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct ReviewPath {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
/// Review ID (UUID)
|
||||
pub review_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct CommentPath {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
/// Comment ID (UUID)
|
||||
pub comment_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QP {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List reviews on a PR
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/reviews",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prListReviews",
|
||||
params(PrPath, QP),
|
||||
responses(
|
||||
(status = 200, description = "Reviews listed.", body = ApiResponse<Vec<PrReview>>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_reviews(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PrPath>,
|
||||
query: web::Query<QP>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let reviews = service
|
||||
.pr
|
||||
.pr_list_reviews(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(reviews)))
|
||||
}
|
||||
|
||||
/// Create a review. States: pending, approved, changes_requested, commented. Authors cannot approve their own PRs.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/reviews",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prCreateReview",
|
||||
params(PrPath),
|
||||
request_body(content = CreateReviewParams, description = "Review parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 201, description = "Review created.", body = ApiResponse<PrReview>),
|
||||
(status = 400, description = "Invalid state or self-approval", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn create_review(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PrPath>,
|
||||
params: web::Json<CreateReviewParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let review = service
|
||||
.pr
|
||||
.pr_create_review(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(review)))
|
||||
}
|
||||
|
||||
/// Submit a pending review. Changes its state to approved, changes_requested, or commented.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/reviews/{review_id}/submit",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prSubmitReview",
|
||||
params(ReviewPath),
|
||||
request_body(content = SubmitReviewParams, description = "Submit parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 200, description = "Review submitted.", body = ApiResponse<PrReview>),
|
||||
(status = 400, description = "Invalid state or self-approval", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
(status = 404, description = "Review not found or already submitted", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn submit_review(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<ReviewPath>,
|
||||
params: web::Json<SubmitReviewParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let review = service
|
||||
.pr
|
||||
.pr_submit_review(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
path.review_id,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(review)))
|
||||
}
|
||||
|
||||
/// Dismiss a submitted review. Requires Admin role.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/reviews/{review_id}/dismiss",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prDismissReview",
|
||||
params(ReviewPath),
|
||||
request_body(content = DismissReviewParams, description = "Dismiss parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 200, description = "Review dismissed.", body = ApiResponse<PrReview>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (Admin required)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Review not found or not submitted", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn dismiss_review(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<ReviewPath>,
|
||||
params: web::Json<DismissReviewParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let review = service
|
||||
.pr
|
||||
.pr_dismiss_review(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
path.review_id,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(review)))
|
||||
}
|
||||
|
||||
/// List comments for a specific review
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/reviews/{review_id}/comments",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prListReviewComments",
|
||||
params(ReviewPath, QP),
|
||||
responses(
|
||||
(status = 200, description = "Review comments listed.", body = ApiResponse<Vec<PrReviewComment>>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Review not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_review_comments(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<ReviewPath>,
|
||||
query: web::Query<QP>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let comments = service
|
||||
.pr
|
||||
.pr_review_comments(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
path.review_id,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(comments)))
|
||||
}
|
||||
|
||||
/// Reply to a review comment
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/comments/{comment_id}/reply",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prAddReviewReply",
|
||||
params(CommentPath),
|
||||
request_body(content = AddReplyParams, description = "Reply parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 201, description = "Reply added.", body = ApiResponse<PrReviewComment>),
|
||||
(status = 400, description = "Empty body", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Comment not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn add_review_reply(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<CommentPath>,
|
||||
params: web::Json<AddReplyParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let comment = service
|
||||
.pr
|
||||
.pr_add_review_reply(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
path.comment_id,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(comment)))
|
||||
}
|
||||
|
||||
/// Update a review comment (own comments only)
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/comments/{comment_id}",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prUpdateReviewComment",
|
||||
params(CommentPath),
|
||||
request_body(content = AddReplyParams, description = "Update parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 200, description = "Comment updated.", body = ApiResponse<PrReviewComment>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Cannot edit other users' comments", body = ApiErrorResponse),
|
||||
(status = 404, description = "Comment not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn update_review_comment(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<CommentPath>,
|
||||
params: web::Json<AddReplyParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let comment = service
|
||||
.pr
|
||||
.pr_update_review_comment(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
path.comment_id,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(comment)))
|
||||
}
|
||||
|
||||
/// Delete a review comment. Own comments or Admin role.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/comments/{comment_id}",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prDeleteReviewComment",
|
||||
params(CommentPath),
|
||||
responses(
|
||||
(status = 200, description = "Comment deleted.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Cannot delete other users' comments (Admin required)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Comment not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn delete_review_comment(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<CommentPath>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.pr
|
||||
.pr_delete_review_comment(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
path.comment_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Comment deleted".to_string())))
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PrSubscription;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PrPath {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
/// Repository name (unique within the workspace)
|
||||
pub repo_name: String,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QP {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List subscriptions on a PR
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/subscriptions",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prListSubscriptions",
|
||||
params(PrPath, QP),
|
||||
responses(
|
||||
(status = 200, description = "Subscriptions listed.", body = ApiResponse<Vec<PrSubscription>>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_subscriptions(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PrPath>,
|
||||
query: web::Query<QP>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let subs = service
|
||||
.pr
|
||||
.pr_subscriptions(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(subs)))
|
||||
}
|
||||
|
||||
/// Subscribe to a PR to receive notifications
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/subscribe",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prSubscribe",
|
||||
params(PrPath),
|
||||
responses(
|
||||
(status = 200, description = "Subscribed.", body = ApiResponse<PrSubscription>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "Already subscribed", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn subscribe(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PrPath>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let sub = service
|
||||
.pr
|
||||
.pr_subscribe(&session, &path.workspace_name, &path.repo_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(sub)))
|
||||
}
|
||||
|
||||
/// Unsubscribe from a PR
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/subscribe",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prUnsubscribe",
|
||||
params(PrPath),
|
||||
responses(
|
||||
(status = 200, description = "Unsubscribed.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Not subscribed", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn unsubscribe(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PrPath>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.pr
|
||||
.pr_unsubscribe(&session, &path.workspace_name, &path.repo_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Unsubscribed".to_string())))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct MutePrParams {
|
||||
/// Whether to mute (true) or unmute (false) notifications
|
||||
pub muted: bool,
|
||||
}
|
||||
|
||||
/// Mute or unmute notifications for a PR
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/mute",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prMute",
|
||||
params(PrPath),
|
||||
request_body(content = MutePrParams, description = "Mute/unmute parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 200, description = "Mute status updated.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Not subscribed", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn mute(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PrPath>,
|
||||
params: web::Json<MutePrParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.pr
|
||||
.pr_mute(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
params.muted,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Mute status updated".to_string())))
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PullRequest;
|
||||
use crate::service::AppService;
|
||||
use crate::service::pr::core::UpdatePrParams;
|
||||
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,
|
||||
/// PR number (unique within the repository)
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
/// Update a pull request
|
||||
///
|
||||
/// Updates an existing pull request's metadata such as title, body, target branch, and draft status.
|
||||
/// Requires write access to the PR (author or repository member).
|
||||
///
|
||||
/// All fields are optional; only provided fields are updated.
|
||||
/// Returns the updated PR with full metadata.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}",
|
||||
tag = "Pull Requests",
|
||||
operation_id = "prUpdate",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = UpdatePrParams,
|
||||
description = "PR update parameters (all fields optional)",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "PR updated successfully. Returns the updated PR with full metadata.", body = ApiResponse<PullRequest>),
|
||||
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to edit this PR", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository, workspace, or PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn update(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<UpdatePrParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let pr = service
|
||||
.pr
|
||||
.pr_update(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(pr)))
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::RepoInvitation;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct AcceptInvitationParams {
|
||||
/// Invitation token (received via email)
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
/// Accept a repository invitation
|
||||
///
|
||||
/// 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,
|
||||
path = "/api/v1/repos/invitations/accept",
|
||||
tag = "Repos",
|
||||
operation_id = "repoAcceptInvitation",
|
||||
request_body(
|
||||
content = AcceptInvitationParams,
|
||||
description = "Invitation acceptance parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Invitation accepted successfully. User is now a member of the repository.", body = ApiResponse<RepoInvitation>),
|
||||
(status = 400, description = "Invalid or expired token, or email doesn't match invitation", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Invitation not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "User is already a member of this repository", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn accept_invitation(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<AcceptInvitationParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let invitation = service
|
||||
.repo
|
||||
.repo_accept_invitation(&session, ¶ms.token)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(invitation)))
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::RepoDeployKey;
|
||||
use crate::service::AppService;
|
||||
use crate::service::repo::deploy_keys::AddDeployKeyParams;
|
||||
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,
|
||||
}
|
||||
|
||||
/// Add a deploy key to a repository
|
||||
///
|
||||
/// 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,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/deploy-keys",
|
||||
tag = "Repos",
|
||||
operation_id = "repoAddDeployKey",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = AddDeployKeyParams,
|
||||
description = "Deploy key addition parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Deploy key added successfully. Returns the newly created deploy key with metadata.", body = ApiResponse<RepoDeployKey>),
|
||||
(status = 400, description = "Invalid parameters: title too long or invalid SSH key format", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "Deploy key with this fingerprint already exists", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn add_deploy_key(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<AddDeployKeyParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let key = service
|
||||
.repo
|
||||
.repo_add_deploy_key(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(key)))
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::RepoMember;
|
||||
use crate::service::AppService;
|
||||
use crate::service::repo::members::AddRepoMemberParams;
|
||||
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,
|
||||
}
|
||||
|
||||
/// Add a member to a repository
|
||||
///
|
||||
/// 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,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/members",
|
||||
tag = "Repos",
|
||||
operation_id = "repoAddMember",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = AddRepoMemberParams,
|
||||
description = "Member addition parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Member added successfully. Returns the newly created member record with user information and role.", body = ApiResponse<RepoMember>),
|
||||
(status = 400, description = "Invalid parameters: invalid role or user doesn't exist", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository, workspace, or user not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "User is already a member of this repository", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn add_member(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<AddRepoMemberParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let member = service
|
||||
.repo
|
||||
.repo_add_member(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(member)))
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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,
|
||||
}
|
||||
|
||||
/// Archive a repository
|
||||
///
|
||||
/// 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,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/archive",
|
||||
tag = "Repos",
|
||||
operation_id = "repoArchive",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Repository archived successfully. Repository is now read-only.", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Owner role)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "Repository is already archived", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn archive(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.repo
|
||||
.repo_archive(&session, &path.workspace_name, &path.repo_name)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Repository archived".to_string())))
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::repo::protection::BranchMergeCheck;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub target_branch: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub pr_number: i64,
|
||||
}
|
||||
|
||||
/// Check if a branch meets merge requirements
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches/{target_branch}/merge-check",
|
||||
tag = "Repos",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Merge check completed successfully", body = ApiResponse<BranchMergeCheck>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 403, description = "Forbidden", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository or pull request not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn check_branch_merge(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let check = service
|
||||
.repo
|
||||
.repo_check_branch_merge_allowed(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&path.target_branch,
|
||||
query.pr_number,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(check)))
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::Repo;
|
||||
use crate::service::AppService;
|
||||
use crate::service::repo::core::CreateRepoParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
/// Workspace name (unique identifier)
|
||||
pub workspace_name: String,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos",
|
||||
tag = "Repos",
|
||||
operation_id = "repoCreate",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CreateRepoParams,
|
||||
description = "Repository creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Repository created successfully. Returns the newly created repository with full metadata.", body = ApiResponse<Repo>),
|
||||
(status = 400, description = "Invalid parameters: name too long, invalid characters, or invalid visibility", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to create repositories in this workspace", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "Repository with this name already exists in the workspace", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error or Git initialization failed", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn create(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CreateRepoParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let repo = service
|
||||
.repo
|
||||
.repo_create(&session, &path.workspace_name, params.into_inner())
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(repo)))
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
use crate::models::common::DEFAULT_REVISION;
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct CreateBranchBody {
|
||||
pub name: String,
|
||||
pub start_point: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches",
|
||||
tag = "Repos",
|
||||
operation_id = "repoCreateBranch",
|
||||
params(PathParams),
|
||||
request_body(content = CreateBranchBody),
|
||||
responses(
|
||||
(status = 201, description = "Branch created", body = ApiResponse<crate::pb::repo::Branch>),
|
||||
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository not found", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn create_branch(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
body: web::Json<CreateBranchBody>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let start_point = body.start_point.as_deref().unwrap_or(DEFAULT_REVISION);
|
||||
let result = service
|
||||
.repo
|
||||
.git_create_branch(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&body.name,
|
||||
start_point,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::RepoCommitComment;
|
||||
use crate::service::AppService;
|
||||
use crate::service::repo::commit_status::CreateCommitCommentParams;
|
||||
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 commit comment
|
||||
///
|
||||
/// 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,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/comments",
|
||||
tag = "Repos",
|
||||
operation_id = "repoCreateCommitComment",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CreateCommitCommentParams,
|
||||
description = "Commit comment creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Commit comment created successfully. Returns the newly created comment with metadata.", body = ApiResponse<RepoCommitComment>),
|
||||
(status = 400, description = "Invalid parameters: body too long, line without path, or commit doesn't exist", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Write role or higher)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository, workspace, commit, or file path not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn create_commit_comment(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CreateCommitCommentParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let comment = service
|
||||
.repo
|
||||
.repo_create_commit_comment(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(comment)))
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::RepoCommitStatus;
|
||||
use crate::service::AppService;
|
||||
use crate::service::repo::commit_status::CreateCommitStatusParams;
|
||||
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 commit status
|
||||
///
|
||||
/// 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,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/statuses",
|
||||
tag = "Repos",
|
||||
operation_id = "repoCreateCommitStatus",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CreateCommitStatusParams,
|
||||
description = "Commit status creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Commit status created successfully. Returns the newly created status with metadata.", body = ApiResponse<RepoCommitStatus>),
|
||||
(status = 400, description = "Invalid parameters: invalid state, context too long, or commit doesn't exist", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Write role or higher)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository, workspace, or commit not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "Status with this context already exists for this commit (use update instead)", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn create_commit_status(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CreateCommitStatusParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let status = service
|
||||
.repo
|
||||
.repo_create_commit_status(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(status)))
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::RepoInvitation;
|
||||
use crate::service::AppService;
|
||||
use crate::service::repo::invitations::CreateRepoInvitationParams;
|
||||
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 repository invitation
|
||||
///
|
||||
/// 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,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/invitations",
|
||||
tag = "Repos",
|
||||
operation_id = "repoCreateInvitation",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CreateRepoInvitationParams,
|
||||
description = "Invitation creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Invitation created successfully. Returns the newly created invitation with metadata.", body = ApiResponse<RepoInvitation>),
|
||||
(status = 400, description = "Invalid parameters: invalid email format or invalid role", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "User is already a member or has a pending invitation", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error or email sending failed", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn create_invitation(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CreateRepoInvitationParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let invitation = service
|
||||
.repo
|
||||
.repo_create_invitation(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(invitation)))
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::BranchProtectionRule;
|
||||
use crate::service::AppService;
|
||||
use crate::service::repo::protection::CreateProtectionRuleParams;
|
||||
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 branch protection rule
|
||||
///
|
||||
/// 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)
|
||||
/// - require_status_checks: Whether status checks must pass
|
||||
/// - required_status_checks: List of required status check contexts
|
||||
/// - 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,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/protection-rules",
|
||||
tag = "Repos",
|
||||
operation_id = "repoCreateProtectionRule",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CreateProtectionRuleParams,
|
||||
description = "Protection rule creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Protection rule created successfully. Returns the newly created protection rule with full configuration.", body = ApiResponse<BranchProtectionRule>),
|
||||
(status = 400, description = "Invalid parameters: invalid pattern, negative approvals count, or conflicting settings", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||
(status = 409, description = "Protection rule with this pattern already exists", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn create_protection_rule(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CreateProtectionRuleParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let rule = service
|
||||
.repo
|
||||
.repo_create_protection_rule(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(rule)))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user