Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f4e9bdfa7 | |||
| b797e360c0 | |||
| 1ccfd3d626 | |||
| dbbfb747a4 | |||
| a0bea36041 | |||
| 63ca1151ae | |||
| 1000f8a80d | |||
| 9eb77ab98b | |||
| 420dedbc1e | |||
| cec6dce955 |
@@ -0,0 +1,8 @@
|
||||
.codegraph
|
||||
.claude
|
||||
target
|
||||
.git
|
||||
.idea
|
||||
*.md
|
||||
LICENSE
|
||||
.env
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
# HTTP Server
|
||||
APP_HTTP_HOST=0.0.0.0
|
||||
APP_HTTP_PORT=8000
|
||||
APP_HTTP_WORKERS=4
|
||||
APP_HTTP_JSON_LIMIT_BYTES=10485760
|
||||
|
||||
# App
|
||||
APP_URL=http://localhost:8000
|
||||
APP_MAIN_DOMAIN=localhost
|
||||
|
||||
# Session
|
||||
APP_SESSION_SECRET=change-me-to-a-secure-random-string-at-least-32-bytes
|
||||
APP_SESSION_COOKIE_NAME=sid
|
||||
APP_SESSION_COOKIE_SECURE=false
|
||||
APP_SESSION_COOKIE_HTTP_ONLY=true
|
||||
APP_SESSION_COOKIE_SAME_SITE=Lax
|
||||
APP_SESSION_COOKIE_PATH=/
|
||||
APP_SESSION_COOKIE_DOMAIN=
|
||||
APP_SESSION_TTL_SECS=86400
|
||||
APP_SESSION_MAX_AGE_SECS=86400
|
||||
|
||||
# PostgreSQL
|
||||
DATABASE_URL=postgres://appks:appks@localhost:5432/appks
|
||||
APP_DATABASE_URL=postgres://appks:appks@localhost:5432/appks
|
||||
APP_DATABASE_MAX_CONNECTIONS=10
|
||||
APP_DATABASE_MIN_CONNECTIONS=2
|
||||
APP_DATABASE_IDLE_TIMEOUT=600
|
||||
APP_DATABASE_MAX_LIFETIME=3600
|
||||
APP_DATABASE_CONNECTION_TIMEOUT=8
|
||||
APP_DATABASE_SCHEMA_SEARCH_PATH=public
|
||||
APP_DATABASE_READ_WRITE_SPLIT=false
|
||||
APP_DATABASE_RETRY_ATTEMPTS=3
|
||||
APP_DATABASE_RETRY_DELAY=5
|
||||
|
||||
# Redis
|
||||
# Single-node mode (set APP_REDIS_CLUSTER_ENABLED=false)
|
||||
APP_REDIS_URL=redis://localhost:6379/0
|
||||
# Cluster mode (set APP_REDIS_CLUSTER_ENABLED=true)
|
||||
APP_REDIS_CLUSTER_ENABLED=true
|
||||
APP_REDIS_CLUSTER_NODES=redis://localhost:6379,redis://localhost:6380,redis://localhost:6381,redis://localhost:6382,redis://localhost:6383,redis://localhost:6384
|
||||
APP_REDIS_READ_FROM_REPLICAS=false
|
||||
APP_REDIS_USERNAME=
|
||||
APP_REDIS_PASSWORD=
|
||||
APP_REDIS_MAX_CONNECTIONS=20
|
||||
APP_REDIS_MIN_CONNECTIONS=2
|
||||
APP_REDIS_IDLE_TIMEOUT=300
|
||||
APP_REDIS_CONNECTION_TIMEOUT=5
|
||||
APP_REDIS_MAX_RETRIES=3
|
||||
APP_REDIS_RETRY_DELAY_MS=100
|
||||
APP_REDIS_TLS_ENABLED=false
|
||||
APP_REDIS_KEY_PREFIX=appks:
|
||||
|
||||
# etcd
|
||||
APP_ETCD_ENDPOINTS=http://localhost:2379
|
||||
APP_ETCD_KEY_PREFIX=/appks/
|
||||
APP_ETCD_CONNECT_TIMEOUT=5
|
||||
APP_ETCD_REQUEST_TIMEOUT=10
|
||||
APP_ETCD_KEEP_ALIVE_INTERVAL=10
|
||||
APP_ETCD_LEASE_TTL=15
|
||||
APP_ETCD_MAX_RETRIES=3
|
||||
APP_ETCD_REGISTER_SELF=false
|
||||
|
||||
# NATS
|
||||
APP_NATS_URL=nats://localhost:4222
|
||||
APP_NATS_CONNECTION_TIMEOUT=5
|
||||
APP_NATS_PING_INTERVAL=20
|
||||
APP_NATS_RECONNECT_DELAY=2
|
||||
APP_NATS_MAX_RECONNECTS=60
|
||||
APP_NATS_STREAM_PREFIX=APPKS
|
||||
APP_NATS_ACK_WAIT_SECS=30
|
||||
APP_NATS_MAX_DELIVER=5
|
||||
|
||||
# S3 / MinIO
|
||||
APP_S3_ENDPOINT=http://localhost:9000
|
||||
APP_S3_REGION=us-east-1
|
||||
APP_S3_ACCESS_KEY=admin
|
||||
APP_S3_SECRET_KEY=mysecret123
|
||||
APP_S3_BUCKET=appks
|
||||
APP_S3_PATH_STYLE=true
|
||||
APP_S3_FORCE_PATH_STYLE=true
|
||||
APP_S3_PUBLIC_URL=http://localhost:9000/appks
|
||||
APP_S3_MAX_CONNECTIONS=50
|
||||
APP_S3_IDLE_TIMEOUT=90
|
||||
APP_S3_CONNECTION_TIMEOUT=10
|
||||
APP_S3_MAX_RETRIES=3
|
||||
APP_S3_UPLOAD_PART_SIZE=8388608
|
||||
APP_S3_MAX_UPLOAD_SIZE=104857600
|
||||
APP_S3_PRESIGNED_URL_EXPIRY=3600
|
||||
|
||||
# LRU Cache
|
||||
APP_LRU_DEFAULT_CAPACITY=1000
|
||||
APP_LRU_DEFAULT_TTL_SECS=300
|
||||
APP_LRU_CLEANUP_INTERVAL_SECS=60
|
||||
|
||||
# gRPC Server
|
||||
APP_RPC_SELF_HOST=0.0.0.0
|
||||
APP_RPC_SELF_PORT=50049
|
||||
APP_RPC_SELF_REFLECTION=false
|
||||
APP_RPC_SELF_SERVICE_NAME=appks
|
||||
APP_RPC_DEFAULT_TIMEOUT_SECS=10
|
||||
|
||||
# AI Provider
|
||||
APP_AI_PROVIDER_API_KEY=
|
||||
APP_AI_PROVIDER_URL=
|
||||
|
||||
# Qdrant
|
||||
APP_QDRANT_URL=http://localhost:6334
|
||||
APP_QDRANT_COLLECTION=appks_embeddings
|
||||
APP_QDRANT_VECTOR_SIZE=1536
|
||||
APP_QDRANT_DISTANCE=Cosine
|
||||
APP_QDRANT_MAX_CONNECTIONS=10
|
||||
APP_QDRANT_IDLE_TIMEOUT=300
|
||||
APP_QDRANT_CONNECTION_TIMEOUT=10
|
||||
APP_QDRANT_MAX_RETRIES=3
|
||||
APP_QDRANT_TLS_ENABLED=false
|
||||
APP_QDRANT_SEARCH_LIMIT=10
|
||||
APP_QDRANT_SCORE_THRESHOLD=0.7
|
||||
|
||||
# Email RPC
|
||||
APP_EMAIL_RPC_ADDR=http://localhost:50050
|
||||
@@ -0,0 +1,654 @@
|
||||
# AGENTS.md — 开发规范 / Development Guidelines
|
||||
|
||||
> 本文件为所有 AI 编码助手(Claude Code、pi、Cursor 等)提供统一的开发指导。
|
||||
> This file provides unified development guidelines for all AI coding assistants.
|
||||
|
||||
**最后更新 / Last Updated**: 2026-06-10
|
||||
|
||||
---
|
||||
|
||||
## 目录 / Table of Contents
|
||||
|
||||
1. [语言 / Language](#1-语言--language)
|
||||
2. [代码风格 / Code Style](#2-代码风格--code-style)
|
||||
3. [禁止模式 / Forbidden Patterns](#3-禁止模式--forbidden-patterns)
|
||||
4. [错误处理 / Error Handling](#4-错误处理--error-handling)
|
||||
5. [安全规范 / Security](#5-安全规范--security)
|
||||
6. [数据库规范 / Database](#6-数据库规范--database)
|
||||
7. [API 设计规范 / API Design](#7-api-设计规范--api-design)
|
||||
8. [日志与可观测性 / Logging & Observability](#8-日志与可观测性--logging--observability)
|
||||
9. [性能规范 / Performance](#9-性能规范--performance)
|
||||
10. [测试规范 / Testing](#10-测试规范--testing)
|
||||
11. [Git 规范 / Git Workflow](#11-git-规范--git-workflow)
|
||||
12. [工作流程 / Workflow](#12-工作流程--workflow)
|
||||
13. [架构决策记录 / ADR](#13-架构决策记录--adr)
|
||||
14. [审查清单 / Review Checklist](#14-审查清单--review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 1. 语言 / Language
|
||||
|
||||
**Always respond in Chinese (中文).** Use the user's language for all conversations and explanations. Code, commands, and technical terms can remain in English.
|
||||
|
||||
始终使用中文回复。代码、命令和技术术语可以保留英文。
|
||||
|
||||
---
|
||||
|
||||
## 2. 代码风格 / Code Style
|
||||
|
||||
### 2.1 基本原则 / Basic Principles
|
||||
|
||||
| 规则 / Rule | 说明 / Description |
|
||||
|-----------|-----------------------------------------------------------------------------------------|
|
||||
| 遵循现有风格 | Follow existing project conventions |
|
||||
| 有意义命名 | Use meaningful variable names; avoid single-letter names except loop counters |
|
||||
| 函数长度 | Keep functions under **50 lines**; split complex logic into smaller functions |
|
||||
| 嵌套深度 | Maximum nesting depth: **3 levels**; use early returns to flatten logic |
|
||||
| 圈复杂度 | Function cyclomatic complexity should not exceed **10** |
|
||||
| 注释 | Add comments for complex logic only; prefer self-documenting code |
|
||||
| 文档注释 | Public items must have `///` doc comments; private items only when logic is non-obvious |
|
||||
|
||||
### 2.2 Rust 最佳实践 / Rust Best Practices
|
||||
|
||||
```rust
|
||||
// ✅ 正确 / Correct
|
||||
fn get_user(id: i64) -> AppResult<User> {
|
||||
let user = db.find_user(id).await?; // 使用 ? 传播错误
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
// ❌ 错误 / Incorrect
|
||||
fn get_user(id: i64) -> User {
|
||||
db.find_user(id).await.unwrap() // 禁止 unwrap()
|
||||
}
|
||||
```
|
||||
|
||||
| 规则 / Rule | 说明 / Description |
|
||||
|-----------|---------------------------------------------------------------------------------------------|
|
||||
| 错误传播 | Use `?` operator for error propagation; never use `unwrap()` or `expect()` in non-test code |
|
||||
| `unsafe` | Avoid `unsafe` blocks; if necessary, add a `// SAFETY:` comment explaining why |
|
||||
| `clone()` | Minimize `clone()` usage; prefer references or `Rc`/`Arc` for shared ownership |
|
||||
| 魔法数字 | No magic numbers; define named constants with `const` |
|
||||
| 硬编码字符串 | No hardcoded strings for config/status; use enums or constants |
|
||||
| 死代码 | Remove dead code; don't leave commented-out code blocks |
|
||||
| 未完成代码 | Don't commit `unimplemented!()`, `todo!()`, or `FIXME` without a tracking issue |
|
||||
|
||||
### 2.3 导入规范 / Import Guidelines
|
||||
|
||||
```rust
|
||||
// 标准库 → 第三方 crate → 本地模块
|
||||
// stdlib → third-party crates → local modules
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::models::common::Status;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 禁止模式 / Forbidden Patterns
|
||||
|
||||
以下代码模式在项目中严格禁止:
|
||||
|
||||
The following code patterns are strictly forbidden in this project:
|
||||
|
||||
| 禁止项 / Forbidden | 说明 / Reason |
|
||||
|-------------------------------|------------------------------------------------|
|
||||
| `// ── xxxx ──────────` | 禁止使用此类分隔线注释;使用 `// Section: xxx` 格式替代 |
|
||||
| `unwrap()` / `expect()` (非测试) | 在非测试代码中禁止使用;使用 `?` 或 `unwrap_or` 等安全替代 |
|
||||
| `panic!()` / `unreachable!()` | 除极少数不可能到达的分支外禁止使用;使用 `AppError` 替代 |
|
||||
| 未处理的 `todo!()` | 不得提交包含 `todo!()` 的代码,除非有对应的 issue 追踪 |
|
||||
| 注释掉的代码 | 不得提交被注释的代码块;使用 Git 历史追溯 |
|
||||
| 过深嵌套 (≥4层) | 使用 early return、`match`、`map`/`and_then` 扁平化逻辑 |
|
||||
| 过长函数 (>50行) | 拆分为更小的、职责单一的函数 |
|
||||
| 魔法数字 | 使用 `const` 定义命名常量 |
|
||||
| 硬编码字符串 | 使用枚举或常量定义配置值/状态值 |
|
||||
| 死代码 | 删除未使用的代码、导入和变量 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 错误处理 / Error Handling
|
||||
|
||||
### 4.1 错误类型体系 / Error Type System
|
||||
|
||||
```rust
|
||||
// 统一使用 AppError 和 AppResult
|
||||
// Use AppError and AppResult consistently
|
||||
use crate::error::{AppError, AppResult};
|
||||
|
||||
pub async fn create_user(req: CreateUserReq) -> AppResult<User> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
- **AppError**: 统一错误枚举,包含领域错误和外部库包装
|
||||
- **AppResult<T>**: `Result<T, AppError>` 的类型别名
|
||||
|
||||
### 4.2 错误处理原则 / Error Handling Principles
|
||||
|
||||
| 原则 / Principle | 说明 / Description |
|
||||
|----------------|---------------------------------------------------------------------------------|
|
||||
| 显式处理 | Handle all errors explicitly; no silent failures |
|
||||
| 用户友好 | Internal errors are logged and masked; user-facing messages should be helpful |
|
||||
| 错误上下文 | Use `.context()` or `.map_err()` to add meaningful context to errors |
|
||||
| 错误分类 | Domain errors (UserNotFound) vs Infrastructure errors (DatabaseError) |
|
||||
| Postgres 映射 | Map Postgres error codes (23505 unique, 23503 FK, 23514 check) to HTTP statuses |
|
||||
|
||||
### 4.3 错误日志格式 / Error Logging Format
|
||||
|
||||
```rust
|
||||
// 记录错误时包含完整上下文
|
||||
// Log errors with full context
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
user_id = %user_id,
|
||||
operation = "create_user",
|
||||
"Failed to create user"
|
||||
);
|
||||
```
|
||||
|
||||
### 4.4 错误恢复策略 / Error Recovery
|
||||
|
||||
| 场景 / Scenario | 策略 / Strategy |
|
||||
|---------------|---------------|
|
||||
| 数据库连接失败 | 重试 + 降级到只读模式 |
|
||||
| 外部服务超时 | 断路器 + 降级响应 |
|
||||
| 缓存 miss | 回退到数据库查询 |
|
||||
| 队列积压 | 背压控制 + 告警 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 安全规范 / Security
|
||||
|
||||
### 5.1 基础安全 / Basic Security
|
||||
|
||||
| 规则 / Rule | 说明 / Description |
|
||||
|-----------|---------------------------------------------------------------|
|
||||
| 密钥管理 | Never hardcode secrets or API keys; use environment variables |
|
||||
| 输入验证 | Always validate and sanitize user input |
|
||||
| SQL 注入 | Use parameterized queries (sqlx handles this automatically) |
|
||||
| XSS 防护 | Escape output; use Content-Security-Policy headers |
|
||||
| CSRF 防护 | Use CSRF tokens for state-changing operations |
|
||||
| 密码安全 | Argon2 hashing with session-scoped RSA-2048 OAEP-SHA256 |
|
||||
| 2FA | TOTP with HMAC-SHA1, base32 secrets, backup codes |
|
||||
|
||||
### 5.2 OWASP Top 10 防护 / OWASP Top 10 Protection
|
||||
|
||||
| 风险 / Risk | 防护措施 / Mitigation |
|
||||
|-----------|------------------------------------------------------|
|
||||
| 注入 | Parameterized queries, input validation |
|
||||
| 失效认证 | Strong password policy, 2FA, session management |
|
||||
| 敏感数据暴露 | Encryption at rest and in transit, data masking |
|
||||
| XML 外部实体 | Disable XML external entity processing |
|
||||
| 失效访问控制 | Role-based access control, resource ownership checks |
|
||||
| 安全配置错误 | Secure defaults, environment-based config |
|
||||
| XSS | Output encoding, CSP headers |
|
||||
| 不安全反序列化 | Validate serialized data, use safe formats |
|
||||
| 使用含漏洞组件 | Regular dependency updates, `cargo audit` |
|
||||
| 日志和监控不足 | Comprehensive logging, alerting |
|
||||
|
||||
### 5.3 企业级安全 / Enterprise Security
|
||||
|
||||
| 要求 / Requirement | 说明 / Description |
|
||||
|------------------|----------------------------------------------------------------------|
|
||||
| 安全审计日志 | Log all sensitive operations with actor, action, resource, timestamp |
|
||||
| 访问控制 | Implement RBAC/ABAC; check permissions at service layer |
|
||||
| 数据脱敏 | Mask PII in logs; encrypt sensitive fields in database |
|
||||
| 依赖安全 | Run `cargo audit` in CI; review new dependencies |
|
||||
| 安全头 | Set HSTS, X-Frame-Options, X-Content-Type-Options, etc. |
|
||||
| 速率限制 | Implement rate limiting for auth endpoints and API calls |
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据库规范 / Database
|
||||
|
||||
### 6.1 基础规范 / Basic Rules
|
||||
|
||||
| 规则 / Rule | 说明 / Description |
|
||||
|-----------|----------------------------------------------------------------------|
|
||||
| 参数化查询 | Always use parameterized queries (sqlx does this by default) |
|
||||
| 事务管理 | Use `ServiceContext::run_in_transaction()` for multi-step operations |
|
||||
| 读写分离 | Use `AppDatabase` read/write pool methods appropriately |
|
||||
| 迁移规范 | All schema changes must go through migration files in `migrate/` |
|
||||
|
||||
### 6.2 性能优化 / Performance Optimization
|
||||
|
||||
| 规则 / Rule | 说明 / Description |
|
||||
|-----------|----------------------------------------------------------------------|
|
||||
| N+1 防护 | Use `JOIN` or batch queries instead of N+1 patterns |
|
||||
| 批量操作 | Use `INSERT ... ON CONFLICT`, `UPDATE ... FROM`, bulk operations |
|
||||
| 索引规范 | Add indexes for frequently queried columns; document index rationale |
|
||||
| 查询分析 | Use `EXPLAIN ANALYZE` to verify query plans for complex queries |
|
||||
| 连接池 | Configure pool sizes based on workload; monitor connection usage |
|
||||
| 慢查询 | Log queries >100ms; investigate and optimize |
|
||||
|
||||
### 6.3 数据一致性 / Data Consistency
|
||||
|
||||
| 规则 / Rule | 说明 / Description |
|
||||
|-----------|-----------------------------------------------------------|
|
||||
| 事务边界 | Keep transactions short; avoid long-running transactions |
|
||||
| 幂等性 | Design operations to be idempotent where possible |
|
||||
| 乐观锁 | Use version columns for concurrent update protection |
|
||||
| 外键约束 | Use database-level foreign keys for referential integrity |
|
||||
|
||||
---
|
||||
|
||||
## 7. API 设计规范 / API Design
|
||||
|
||||
### 7.1 RESTful 规范 / RESTful Conventions
|
||||
|
||||
| 规则 / Rule | 示例 / Example |
|
||||
|-----------|-----------------------------------------------------------------------------------------|
|
||||
| 资源命名 | `/api/v1/workspaces/{id}/repos` (复数名词) |
|
||||
| HTTP 方法 | GET (读取), POST (创建), PUT/PATCH (更新), DELETE (删除) |
|
||||
| 状态码 | 200 (成功), 201 (创建), 204 (无内容), 400 (客户端错误), 401 (未认证), 403 (禁止), 404 (未找到), 500 (服务器错误) |
|
||||
| 版本管理 | URL path versioning: `/api/v1/...` |
|
||||
|
||||
### 7.2 响应格式 / Response Format
|
||||
|
||||
```rust
|
||||
// 统一响应类型
|
||||
// Unified response types
|
||||
ApiResponse<T> // 单个数据 / Single payload
|
||||
ApiListResponse<T> // 分页列表 / Paginated list { data, total, page, per_page }
|
||||
ApiEmptyResponse // 空响应 / Empty response
|
||||
ApiErrorResponse // 错误响应 / Error response { code, message, details }
|
||||
```
|
||||
|
||||
### 7.3 OpenAPI 文档 / OpenAPI Documentation
|
||||
|
||||
```rust
|
||||
// 每个端点必须添加 OpenAPI 注解
|
||||
// Every endpoint must have OpenAPI annotations
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/login",
|
||||
request_body = LoginReq,
|
||||
responses(
|
||||
(status = 200, description = "Login successful", body = ApiResponse<LoginResp>),
|
||||
(status = 401, description = "Invalid credentials", body = ApiErrorResponse)
|
||||
),
|
||||
tag = "auth"
|
||||
)]
|
||||
pub async fn login(...) -> HttpResponse { ... }
|
||||
```
|
||||
|
||||
### 7.4 API 治理 / API Governance
|
||||
|
||||
| 规则 / Rule | 说明 / Description |
|
||||
|---|---|
|
||||
| 请求验证 | Validate all request bodies and query parameters |
|
||||
| 速率限制 | Apply rate limiting to auth and resource-intensive endpoints |
|
||||
| 幂等性 | POST operations with same idempotency key should produce same result |
|
||||
| 缓存策略 | Use ETag/Last-Modified for cacheable resources |
|
||||
| 错误码体系 | Consistent error codes across all endpoints |
|
||||
| 分页 | Default page size 20, max 100; use cursor-based pagination for large datasets |
|
||||
|
||||
---
|
||||
|
||||
## 8. 日志与可观测性 / Logging & Observability
|
||||
|
||||
### 8.1 日志规范 / Logging Standards
|
||||
|
||||
```rust
|
||||
// 使用 tracing crate 进行结构化日志
|
||||
// Use tracing crate for structured logging
|
||||
use tracing::{info, warn, error, debug, instrument};
|
||||
|
||||
#[instrument(skip(db), fields(user_id = %req.user_id))]
|
||||
pub async fn create_user(req: CreateUserReq) -> AppResult<User> {
|
||||
info!("Creating new user");
|
||||
// ...
|
||||
error!(error = %err, "Failed to create user");
|
||||
}
|
||||
```
|
||||
|
||||
| 级别 / Level | 用途 / Usage |
|
||||
|---|---|
|
||||
| `error` | 错误需要立即关注 / Errors requiring immediate attention |
|
||||
| `warn` | 异常但可恢复的情况 / Abnormal but recoverable situations |
|
||||
| `info` | 关键业务操作记录 / Key business operation records |
|
||||
| `debug` | 开发调试信息 / Development debugging info |
|
||||
| `trace` | 详细执行路径 / Detailed execution paths |
|
||||
|
||||
### 8.2 敏感信息脱敏 / Data Masking
|
||||
|
||||
| 数据类型 / Data Type | 脱敏规则 / Masking Rule |
|
||||
|---|---|
|
||||
| 密码 | 完全隐藏 / Never log |
|
||||
| Token/密钥 | 只显示前 4 位 / Show first 4 chars only |
|
||||
| 邮箱 | `u***@example.com` |
|
||||
| IP 地址 | 保留网段 / Keep subnet |
|
||||
| 个人信息 | 根据最小必要原则 / Minimum necessary principle |
|
||||
|
||||
### 8.3 性能指标 / Metrics
|
||||
|
||||
| 指标 / Metric | 说明 / Description |
|
||||
|---|---|
|
||||
| 请求延迟 | HTTP request latency (P50, P95, P99) |
|
||||
| 错误率 | Error rate by endpoint and status code |
|
||||
| 吞吐量 | Requests per second |
|
||||
| 数据库连接 | Active/idle connections in pool |
|
||||
| 缓存命中率 | Cache hit/miss ratio |
|
||||
| 队列积压 | Queue depth and processing rate |
|
||||
| 内存使用 | Heap usage, allocation rate |
|
||||
| 活跃会话 | Active WebSocket sessions |
|
||||
|
||||
### 8.4 健康检查 / Health Checks
|
||||
|
||||
```rust
|
||||
// 端点: GET /health
|
||||
// Endpoint: GET /health
|
||||
{
|
||||
"status": "healthy", // healthy | degraded | unhealthy
|
||||
"version": "1.0.0",
|
||||
"uptime": 3600,
|
||||
"checks": {
|
||||
"database": { "status": "up", "latency_ms": 5 },
|
||||
"redis": { "status": "up", "latency_ms": 2 },
|
||||
"nats": { "status": "up", "latency_ms": 1 },
|
||||
"etcd": { "status": "up", "latency_ms": 3 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.5 告警规则 / Alerting Rules
|
||||
|
||||
| 条件 / Condition | 级别 / Level |
|
||||
|---|---|
|
||||
| 错误率 > 5% | Critical |
|
||||
| P99 延迟 > 500ms | Warning |
|
||||
| 数据库连接池 > 80% | Warning |
|
||||
| 队列积压 > 1000 | Critical |
|
||||
| 内存使用 > 85% | Warning |
|
||||
| 健康检查失败 | Critical |
|
||||
|
||||
### 8.6 请求链路追踪 / Request Tracing
|
||||
|
||||
```rust
|
||||
// 每个请求分配唯一 trace_id
|
||||
// Each request gets a unique trace_id
|
||||
tracing::info!(
|
||||
trace_id = %request_id,
|
||||
user_id = %session.user_id,
|
||||
method = %req.method(),
|
||||
path = %req.path(),
|
||||
"Request started"
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 性能规范 / Performance
|
||||
|
||||
### 9.1 SLA 目标 / SLA Targets
|
||||
|
||||
| 指标 / Metric | 目标 / Target |
|
||||
|---|---|
|
||||
| 可用性 | 99.9% (每月宕机 <43 分钟) |
|
||||
| P50 延迟 | <50ms |
|
||||
| P95 延迟 | <200ms |
|
||||
| P99 延迟 | <500ms |
|
||||
| 错误率 | <0.1% |
|
||||
| 数据库查询 | <100ms (常规查询) |
|
||||
| 缓存命中率 | >90% |
|
||||
|
||||
### 9.2 性能原则 / Performance Principles
|
||||
|
||||
| 原则 / Principle | 说明 / Description |
|
||||
|---|---|
|
||||
| 基准测试 | Establish performance baselines before optimization |
|
||||
| 测量优先 | Profile before optimizing; don't guess |
|
||||
| 渐进优化 | Optimize iteratively; measure impact of each change |
|
||||
| 容量规划 | Plan for 3x current load |
|
||||
|
||||
### 9.3 优化策略 / Optimization Strategies
|
||||
|
||||
| 场景 / Scenario | 策略 / Strategy |
|
||||
|---|---|
|
||||
| 热点查询 | Add caching (L1 + L2) |
|
||||
| 大量读取 | Use read replicas |
|
||||
| 批量操作 | Batch database operations |
|
||||
| 高并发 | Use connection pooling, async I/O |
|
||||
| 大数据量 | Use cursor-based pagination |
|
||||
|
||||
---
|
||||
|
||||
## 10. 测试规范 / Testing
|
||||
|
||||
### 10.1 基础要求 / Basic Requirements
|
||||
|
||||
| 规则 / Rule | 说明 / Description |
|
||||
|---|---|
|
||||
| 新功能 | All new features must have unit tests |
|
||||
| Bug 修复 | Bug fixes must include regression tests |
|
||||
| 关键路径 | Critical business logic must have integration tests |
|
||||
| 测试隔离 | Tests must be independent and not depend on execution order |
|
||||
|
||||
### 10.2 测试命令 / Test Commands
|
||||
|
||||
```bash
|
||||
cargo test # 运行所有测试 / Run all tests
|
||||
cargo test -- <test_name> # 按名称运行 / Run by name
|
||||
cargo test lru::tests # 运行特定模块 / Run module tests
|
||||
cargo test -- --nocapture # 显示输出 / Show output
|
||||
```
|
||||
|
||||
### 10.3 测试命名 / Test Naming
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_user_with_valid_input() { ... }
|
||||
|
||||
#[test]
|
||||
fn test_create_user_with_duplicate_email_returns_error() { ... }
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_async_operation_handles_timeout() { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Git 规范 / Git Workflow
|
||||
|
||||
### 11.1 提交信息格式 / Commit Message Format
|
||||
|
||||
使用 Angular 风格,全部英文:
|
||||
|
||||
Use Angular style, all English:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
| Type | 说明 / Description |
|
||||
|---|---|
|
||||
| `feat` | 新功能 / New feature |
|
||||
| `fix` | Bug 修复 / Bug fix |
|
||||
| `refactor` | 重构 / Code refactoring |
|
||||
| `docs` | 文档 / Documentation |
|
||||
| `test` | 测试 / Tests |
|
||||
| `chore` | 构建/工具 / Build/tooling |
|
||||
| `perf` | 性能优化 / Performance improvement |
|
||||
| `style` | 代码格式 / Code formatting |
|
||||
| `ci` | CI/CD 相关 / CI/CD changes |
|
||||
|
||||
**示例 / Examples:**
|
||||
```
|
||||
feat(auth): add 2FA login support
|
||||
fix(api): resolve race condition in user creation
|
||||
refactor(service): extract common validation logic
|
||||
docs(readme): update API documentation
|
||||
test(cache): add unit tests for LRU eviction
|
||||
chore(deps): update sqlx to 0.8
|
||||
```
|
||||
|
||||
### 11.2 提交原则 / Commit Principles
|
||||
|
||||
| 原则 / Principle | 说明 / Description |
|
||||
|---|---|
|
||||
| 原子提交 | Each commit should address one concern |
|
||||
| 完整性 | Each commit should leave the codebase in a working state |
|
||||
| 禁止强制推送 | Never force push to main branch |
|
||||
| 提交前检查 | Run `cargo check` and `cargo test` before committing |
|
||||
|
||||
### 11.3 分支策略 / Branch Strategy
|
||||
|
||||
| 分支 / Branch | 用途 / Purpose |
|
||||
|---|---|
|
||||
| `main` | 生产就绪代码 / Production-ready code |
|
||||
| `feat/*` | 功能开发 / Feature development |
|
||||
| `fix/*` | Bug 修复 / Bug fixes |
|
||||
| `release/*` | 发布准备 / Release preparation |
|
||||
|
||||
---
|
||||
|
||||
## 12. 工作流程 / Workflow
|
||||
|
||||
### 12.1 开发流程 / Development Process
|
||||
|
||||
1. **理解先于编写** — Read before write; understand context first
|
||||
2. **最小变更** — Minimal changes; don't refactor unrelated code
|
||||
3. **验证变更** — Verify after changes; run tests or check output
|
||||
4. **文档同步** — Update documentation when changing public APIs
|
||||
|
||||
### 12.2 AI 助手工作规范 / AI Assistant Guidelines
|
||||
|
||||
| 规则 / Rule | 说明 / Description |
|
||||
|---|---|
|
||||
| 先读后写 | Always read existing code before making changes |
|
||||
| 最小侵入 | Make minimal changes; don't refactor unrelated code |
|
||||
| 验证结果 | Run `cargo check` or `cargo test` after changes |
|
||||
| 解释变更 | Explain what you changed and why |
|
||||
| 询问不确定 | Ask when unsure about requirements |
|
||||
|
||||
### 12.3 常用命令 / Common Commands
|
||||
|
||||
```bash
|
||||
cargo build # 构建 / Build
|
||||
cargo check # 快速检查 / Quick check
|
||||
cargo test # 运行测试 / Run tests
|
||||
cargo clippy # Lint 检查 / Lint checks
|
||||
cargo fmt # 格式化 / Format code
|
||||
cargo doc --no-deps # 生成文档 / Build docs
|
||||
cargo machete # 检查未使用依赖 / Check unused deps
|
||||
cargo run --bin gen_openapi # 生成 OpenAPI / Generate OpenAPI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. 架构决策记录 / ADR
|
||||
|
||||
架构决策记录存放在 `docs/adr/` 目录下,使用 Markdown 格式。
|
||||
|
||||
Architecture Decision Records are stored in `docs/adr/` directory in Markdown format.
|
||||
|
||||
### 索引 / Index
|
||||
|
||||
| ADR | 标题 / Title | 状态 / Status |
|
||||
|---|---|---|
|
||||
| [ADR-001](docs/adr/001-choice-of-web-framework.md) | 选择 Actix-web 作为 Web 框架 | Accepted |
|
||||
| [ADR-002](docs/adr/002-two-tier-caching.md) | 两级缓存架构 (L1 LRU + L2 Redis) | Accepted |
|
||||
| [ADR-003](docs/adr/003-nats-for-messaging.md) | 使用 NATS JetStream 作为消息队列 | Accepted |
|
||||
| [ADR-004](docs/adr/004-etcd-for-discovery.md) | 使用 etcd 进行服务发现 | Accepted |
|
||||
| [ADR-005](docs/adr/005-error-handling-strategy.md) | 统一错误处理策略 | Accepted |
|
||||
|
||||
### ADR 模板 / ADR Template
|
||||
|
||||
```markdown
|
||||
# ADR-NNN: 标题 / Title
|
||||
|
||||
## 状态 / Status
|
||||
Accepted | Superseded | Deprecated
|
||||
|
||||
## 背景 / Context
|
||||
描述问题背景 / Describe the context
|
||||
|
||||
## 决策 / Decision
|
||||
描述做出的决策 / Describe the decision
|
||||
|
||||
## 后果 / Consequences
|
||||
描述正面和负面影响 / Describe positive and negative impacts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. 审查清单 / Review Checklist
|
||||
|
||||
### 代码审查 / Code Review
|
||||
|
||||
- [ ] 代码风格符合项目规范 / Code style follows project conventions
|
||||
- [ ] 没有使用禁止模式 / No forbidden patterns used
|
||||
- [ ] 错误处理完整 / Error handling is complete
|
||||
- [ ] 安全考虑已处理 / Security considerations addressed
|
||||
- [ ] 性能影响已评估 / Performance impact assessed
|
||||
- [ ] 测试已添加 / Tests are added
|
||||
- [ ] 文档已更新 / Documentation is updated
|
||||
|
||||
### PR 审查 / PR Review
|
||||
|
||||
- [ ] 提交信息符合 Angular 风格 / Commit messages follow Angular style
|
||||
- [ ] 每个提交只关注一个问题 / Each commit addresses one concern
|
||||
- [ ] 变更范围合理 / Change scope is reasonable
|
||||
- [ ] 没有遗留的 TODO/FIXME / No leftover TODO/FIXME
|
||||
- [ ] CI 检查通过 / CI checks pass
|
||||
|
||||
### 发布前审查 / Pre-release Review
|
||||
|
||||
- [ ] 所有测试通过 / All tests pass
|
||||
- [ ] 性能测试完成 / Performance tests completed
|
||||
- [ ] 安全扫描通过 / Security scan passed
|
||||
- [ ] 文档完整 / Documentation is complete
|
||||
- [ ] 变更日志已更新 / Changelog is updated
|
||||
|
||||
---
|
||||
|
||||
## 附录 / Appendix
|
||||
|
||||
### 项目架构速查 / Quick Architecture Reference
|
||||
|
||||
```
|
||||
appks — 协作开发平台后端 / Collaborative Development Platform Backend
|
||||
|
||||
config/ → 环境配置 / Environment configuration
|
||||
models/ → 数据模型 / Data models (sqlx FromRow)
|
||||
service/ → 业务逻辑 / Business logic (AppService)
|
||||
api/ → HTTP 端点 / HTTP endpoints
|
||||
immediate/ → 实时 IM / Real-time IM (WebSocket)
|
||||
cache/ → 两级缓存 / Two-tier cache (L1 + L2)
|
||||
storage/ → 对象存储 / Object storage (S3)
|
||||
queue/ → 消息队列 / Message queue (NATS)
|
||||
etcd/ → 服务发现 / Service discovery
|
||||
session/ → 会话管理 / Session management
|
||||
pb/ → gRPC 客户端 / gRPC client stubs
|
||||
proto/ → Protobuf 定义 / Protobuf definitions
|
||||
migrate/ → 数据库迁移 / Database migrations
|
||||
error.rs → 统一错误类型 / Unified error types
|
||||
```
|
||||
|
||||
### 基础设施速查 / Infrastructure Quick Reference
|
||||
|
||||
| 服务 / Service | 用途 / Purpose | 协议 / Protocol |
|
||||
|--------------|-----------------------------------------|---------------|
|
||||
| Postgres | 主数据库 / Primary database | sqlx |
|
||||
| Redis | 缓存/会话/限流 / Cache/sessions/rate limiting | redis + r2d2 |
|
||||
| etcd | 服务发现 / Service discovery | etcd-client |
|
||||
| NATS | 消息队列 / Message queue | async-nats |
|
||||
| S3/MinIO | 对象存储 / Object storage | object_store |
|
||||
| Qdrant | 向量数据库 / Vector DB | config only |
|
||||
|
||||
---
|
||||
|
||||
*This document is maintained by the development team. For questions or suggestions, please open an issue.*
|
||||
Generated
+54
@@ -349,6 +349,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"actix-multipart",
|
||||
"actix-web",
|
||||
"arc-swap",
|
||||
"argon2",
|
||||
"async-nats",
|
||||
"base64 0.22.1",
|
||||
@@ -362,6 +363,7 @@ dependencies = [
|
||||
"hex",
|
||||
"hkdf 0.12.4",
|
||||
"hmac 0.12.1",
|
||||
"jsonwebtoken",
|
||||
"object_store",
|
||||
"prost",
|
||||
"prost-types",
|
||||
@@ -378,6 +380,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tonic",
|
||||
"tonic-health",
|
||||
"tonic-prost",
|
||||
"tonic-prost-build",
|
||||
"tracing",
|
||||
@@ -2420,6 +2423,21 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "9.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"js-sys",
|
||||
"pem",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language-tags"
|
||||
version = "0.3.2"
|
||||
@@ -2959,6 +2977,16 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
@@ -4118,6 +4146,18 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"thiserror",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
@@ -4597,6 +4637,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4675,6 +4716,19 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic-health"
|
||||
version = "0.14.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcfab99db777fba2802f0dfa861d1628d1ae916fb199d29819941f139ae85082"
|
||||
dependencies = [
|
||||
"prost",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tonic",
|
||||
"tonic-prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic-prost"
|
||||
version = "0.14.6"
|
||||
|
||||
+25
-22
@@ -16,20 +16,20 @@ path = "main.rs"
|
||||
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"
|
||||
sqlx = { version = "0.9", features = ["postgres","runtime-tokio","chrono","uuid","json","migrate"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1", features = [] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "1", features = ["serde","v4","v7","v5"] }
|
||||
reqwest = { version = "0.13", features = ["json"] }
|
||||
tracing = { version = "0.1", features = [] }
|
||||
tracing-subscriber = { version = "0.3", features = ["fmt"] }
|
||||
dotenvy = "0.15"
|
||||
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"] }
|
||||
redis = { version = "1", features = ["cluster","cluster-async","aio","tokio-comp","connection-manager"] }
|
||||
dashmap = "6"
|
||||
object_store = { version = "0.13", features = ["tokio","aws","cloud"] }
|
||||
argon2 = "0.5"
|
||||
rsa = "0.9"
|
||||
chacha20poly1305 = "0.10"
|
||||
@@ -37,22 +37,25 @@ hkdf = "0.12"
|
||||
sha2 = "0.10"
|
||||
sha1 = "0.10"
|
||||
hmac = "0.12"
|
||||
jsonwebtoken = "9"
|
||||
arc-swap = "1"
|
||||
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"
|
||||
tonic = { version = "0.14", features = ["transport", "channel"] }
|
||||
prost = "0.14"
|
||||
prost-types = "0.14"
|
||||
tonic-prost = "0.14"
|
||||
tonic-health = "0.14.6"
|
||||
url = "2.5"
|
||||
etcd-client = { version = "0.18.0", features = ["tls"] }
|
||||
tokio-stream = "0.1"
|
||||
etcd-client = { version = "0.18", features = ["tls"] }
|
||||
tokio-stream = { version = "0.1", features = ["net"] }
|
||||
async-nats = "0.49"
|
||||
futures-util = "0.3"
|
||||
utoipa = { version = "5.5.0", features = ["uuid","chrono","actix_extras","decimal","macros"]}
|
||||
utoipa = { version = "5", features = ["uuid","chrono","actix_extras","decimal","macros"]}
|
||||
actix-web = { version = "4", features = ["secure-cookies"] }
|
||||
actix-multipart = "0.7"
|
||||
hex = "0.4.3"
|
||||
hex = "0.4"
|
||||
|
||||
[build-dependencies]
|
||||
tonic-prost-build = "0.14.6"
|
||||
tonic-prost-build = "0.14"
|
||||
|
||||
+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 appks && \
|
||||
strip target/release/appks
|
||||
|
||||
FROM ubuntu:26.04
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /app/target/release/appks /usr/local/bin/appks
|
||||
|
||||
ENV APP_HTTP_HOST=0.0.0.0
|
||||
ENV APP_HTTP_PORT=8000
|
||||
ENV APP_RPC_SELF_HOST=0.0.0.0
|
||||
ENV APP_RPC_SELF_PORT=50049
|
||||
|
||||
EXPOSE 8000 50049
|
||||
ENTRYPOINT ["appks"]
|
||||
@@ -0,0 +1,16 @@
|
||||
FROM ubuntu:26.04
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY target/release/appks /usr/local/bin/appks
|
||||
|
||||
ENV APP_HTTP_HOST=0.0.0.0
|
||||
ENV APP_HTTP_PORT=8000
|
||||
ENV APP_RPC_SELF_HOST=0.0.0.0
|
||||
ENV APP_RPC_SELF_PORT=50049
|
||||
|
||||
EXPOSE 8000 50049
|
||||
|
||||
ENTRYPOINT ["appks"]
|
||||
@@ -0,0 +1,33 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::auth::change_password::ChangePasswordParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/auth/password/change",
|
||||
tag = "Auth",
|
||||
operation_id = "authChangePassword",
|
||||
request_body(content = ChangePasswordParams, description = "Password change parameters (passwords encrypted with session RSA public key)", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 200, description = "Password changed successfully", body = ApiEmptyResponse),
|
||||
(status = 400, description = "Invalid password", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn change_password(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<ChangePasswordParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.auth
|
||||
.auth_change_password(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("password changed successfully")))
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod captcha;
|
||||
pub mod change_password;
|
||||
pub mod disable_2fa;
|
||||
pub mod enable_2fa;
|
||||
pub mod get_2fa_status;
|
||||
@@ -52,6 +53,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.route(
|
||||
"/2fa/backup-codes/regenerate",
|
||||
web::post().to(regenerate_2fa_backup_codes::handle),
|
||||
)
|
||||
.route(
|
||||
"/password/change",
|
||||
web::post().to(change_password::change_password),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::channels::ChannelCategory;
|
||||
use crate::service::AppService;
|
||||
use crate::service::im::ImSession;
|
||||
use crate::service::im::categories::CreateCategoryParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/im/workspaces/{workspace_name}/categories",
|
||||
tag = "IM",
|
||||
operation_id = "imCategoryCreate",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CreateCategoryParams,
|
||||
description = "Category creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Category created successfully", body = ApiResponse<ChannelCategory>),
|
||||
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn category_create(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CreateCategoryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = session.user().ok_or(AppError::Unauthorized)?;
|
||||
let im_session = ImSession::new(user_id);
|
||||
let result = service
|
||||
.im
|
||||
.category_create(&im_session, &path.workspace_name, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::im::ImSession;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub category_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Delete a category
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/im/workspaces/{workspace_name}/categories/{category_id}",
|
||||
tag = "IM",
|
||||
operation_id = "imCategoryDelete",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Category deleted successfully", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or category not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn category_delete(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = session.user().ok_or(AppError::Unauthorized)?;
|
||||
let im_session = ImSession::new(user_id);
|
||||
service
|
||||
.im
|
||||
.category_delete(&im_session, &path.workspace_name, path.category_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("Category deleted")))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::channels::ChannelCategory;
|
||||
use crate::service::AppService;
|
||||
use crate::service::im::ImSession;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
}
|
||||
|
||||
/// List categories
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/im/workspaces/{workspace_name}/categories",
|
||||
tag = "IM",
|
||||
operation_id = "imCategoryList",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Categories listed successfully", body = ApiResponse<Vec<ChannelCategory>>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn category_list(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = session.user().ok_or(AppError::Unauthorized)?;
|
||||
let im_session = ImSession::new(user_id);
|
||||
let result = service
|
||||
.im
|
||||
.category_list(&im_session, &path.workspace_name)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::channels::ChannelCategory;
|
||||
use crate::service::AppService;
|
||||
use crate::service::im::ImSession;
|
||||
use crate::service::im::categories::UpdateCategoryParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub category_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Update a category
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/im/workspaces/{workspace_name}/categories/{category_id}",
|
||||
tag = "IM",
|
||||
operation_id = "imCategoryUpdate",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = UpdateCategoryParams,
|
||||
description = "Category update parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Category updated successfully", body = ApiResponse<ChannelCategory>),
|
||||
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or category not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn category_update(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<UpdateCategoryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = session.user().ok_or(AppError::Unauthorized)?;
|
||||
let im_session = ImSession::new(user_id);
|
||||
let result = service
|
||||
.im
|
||||
.category_update(
|
||||
&im_session,
|
||||
&path.workspace_name,
|
||||
path.category_id,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::base_info::resolve_users;
|
||||
use crate::models::channels::ChannelDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::service::im::ImSession;
|
||||
use crate::service::im::channels::CreateChannelParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
}
|
||||
|
||||
/// Create a channel
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/im/workspaces/{workspace_name}/channels",
|
||||
tag = "IM",
|
||||
operation_id = "imChannelCreate",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CreateChannelParams,
|
||||
description = "Channel creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Channel created successfully", body = ApiResponse<ChannelDetail>),
|
||||
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn channel_create(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CreateChannelParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = session.user().ok_or(AppError::Unauthorized)?;
|
||||
let im_session = ImSession::new(user_id);
|
||||
let request_id = uuid::Uuid::now_v7();
|
||||
let channel = service
|
||||
.im
|
||||
.channel_create(
|
||||
&im_session,
|
||||
&path.workspace_name,
|
||||
params.into_inner(),
|
||||
request_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let db = &service.ctx.db;
|
||||
let users = resolve_users(db, &[channel.created_by]).await?;
|
||||
let creator = users.get(&channel.created_by).cloned().unwrap_or_default();
|
||||
let detail = channel.into_detail(creator);
|
||||
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(detail)))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::im::ImSession;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub channel_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Delete a channel
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}",
|
||||
tag = "IM",
|
||||
operation_id = "imChannelDelete",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Channel deleted successfully", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or channel not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn channel_delete(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = session.user().ok_or(AppError::Unauthorized)?;
|
||||
let im_session = ImSession::new(user_id);
|
||||
let request_id = uuid::Uuid::now_v7();
|
||||
service
|
||||
.im
|
||||
.channel_delete(
|
||||
&im_session,
|
||||
&path.workspace_name,
|
||||
path.channel_id,
|
||||
request_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("Channel deleted")))
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::base_info::resolve_users;
|
||||
use crate::models::channels::ChannelDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::service::im::ImSession;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub channel_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Get a channel
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}",
|
||||
tag = "IM",
|
||||
operation_id = "imChannelGet",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Channel retrieved successfully", body = ApiResponse<ChannelDetail>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or channel not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn channel_get(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = session.user().ok_or(AppError::Unauthorized)?;
|
||||
let im_session = ImSession::new(user_id);
|
||||
let channel = service
|
||||
.im
|
||||
.channel_get(&im_session, &path.workspace_name, path.channel_id)
|
||||
.await?;
|
||||
|
||||
let db = &service.ctx.db;
|
||||
let users = resolve_users(db, &[channel.created_by]).await?;
|
||||
let creator = users.get(&channel.created_by).cloned().unwrap_or_default();
|
||||
let detail = channel.into_detail(creator);
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(detail)))
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::base_info::resolve_users;
|
||||
use crate::models::channels::ChannelDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::service::im::ImSession;
|
||||
use crate::service::im::channels::ChannelListFilters;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub channel_type: Option<String>,
|
||||
pub channel_kind: Option<String>,
|
||||
pub category_id: Option<uuid::Uuid>,
|
||||
pub archived: Option<bool>,
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List channels
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/im/workspaces/{workspace_name}/channels",
|
||||
tag = "IM",
|
||||
operation_id = "imChannelList",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Channels listed successfully", body = ApiResponse<Vec<ChannelDetail>>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn channel_list(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = session.user().ok_or(AppError::Unauthorized)?;
|
||||
let im_session = ImSession::new(user_id);
|
||||
let filters = ChannelListFilters {
|
||||
channel_type: query.channel_type.clone(),
|
||||
channel_kind: query.channel_kind.clone(),
|
||||
category_id: query.category_id,
|
||||
archived: query.archived,
|
||||
};
|
||||
let result = service
|
||||
.im
|
||||
.channel_list(
|
||||
&im_session,
|
||||
&path.workspace_name,
|
||||
filters,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let db = &service.ctx.db;
|
||||
let creator_ids: Vec<Uuid> = result.iter().map(|c| c.created_by).collect();
|
||||
let users = resolve_users(db, &creator_ids).await?;
|
||||
let details: Vec<ChannelDetail> = result
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
let creator = users.get(&c.created_by).cloned().unwrap_or_default();
|
||||
c.into_detail(creator)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(details)))
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::base_info::resolve_users;
|
||||
use crate::models::channels::ChannelDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::service::im::ImSession;
|
||||
use crate::service::im::channels::UpdateChannelParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub channel_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Update a channel
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}",
|
||||
tag = "IM",
|
||||
operation_id = "imChannelUpdate",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = UpdateChannelParams,
|
||||
description = "Channel update parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Channel updated successfully", body = ApiResponse<ChannelDetail>),
|
||||
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or channel not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn channel_update(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<UpdateChannelParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = session.user().ok_or(AppError::Unauthorized)?;
|
||||
let im_session = ImSession::new(user_id);
|
||||
let request_id = uuid::Uuid::now_v7();
|
||||
let channel = service
|
||||
.im
|
||||
.channel_update(
|
||||
&im_session,
|
||||
&path.workspace_name,
|
||||
path.channel_id,
|
||||
params.into_inner(),
|
||||
request_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let db = &service.ctx.db;
|
||||
let users = resolve_users(db, &[channel.created_by]).await?;
|
||||
let creator = users.get(&channel.created_by).cloned().unwrap_or_default();
|
||||
let detail = channel.into_detail(creator);
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(detail)))
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::channels::ChannelMember;
|
||||
use crate::service::AppService;
|
||||
use crate::service::im::ImSession;
|
||||
use crate::service::im::members::InviteMemberParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub channel_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Invite a member
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}/members",
|
||||
tag = "IM",
|
||||
operation_id = "imMemberInvite",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = InviteMemberParams,
|
||||
description = "Invitation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Member invited successfully", body = ApiResponse<ChannelMember>),
|
||||
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or channel not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn member_invite(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<InviteMemberParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = session.user().ok_or(AppError::Unauthorized)?;
|
||||
let im_session = ImSession::new(user_id);
|
||||
let result = service
|
||||
.im
|
||||
.member_invite(
|
||||
&im_session,
|
||||
&path.workspace_name,
|
||||
path.channel_id,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::channels::ChannelMember;
|
||||
use crate::service::AppService;
|
||||
use crate::service::im::ImSession;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub channel_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Join a channel
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}/join",
|
||||
tag = "IM",
|
||||
operation_id = "imMemberJoin",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Joined channel successfully", body = ApiResponse<ChannelMember>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or channel not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn member_join(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = session.user().ok_or(AppError::Unauthorized)?;
|
||||
let im_session = ImSession::new(user_id);
|
||||
let result = service
|
||||
.im
|
||||
.member_join(&im_session, &path.workspace_name, path.channel_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::im::ImSession;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub channel_id: uuid::Uuid,
|
||||
pub user_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Kick a member
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}/members/{user_id}",
|
||||
tag = "IM",
|
||||
operation_id = "imMemberKick",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Member kicked successfully", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace, channel or member not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn member_kick(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = session.user().ok_or(AppError::Unauthorized)?;
|
||||
let im_session = ImSession::new(user_id);
|
||||
service
|
||||
.im
|
||||
.member_kick(
|
||||
&im_session,
|
||||
&path.workspace_name,
|
||||
path.channel_id,
|
||||
path.user_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("Member kicked")))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::im::ImSession;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub channel_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Leave a channel
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}/leave",
|
||||
tag = "IM",
|
||||
operation_id = "imMemberLeave",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Left channel successfully", body = ApiEmptyResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or channel not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn member_leave(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = session.user().ok_or(AppError::Unauthorized)?;
|
||||
let im_session = ImSession::new(user_id);
|
||||
service
|
||||
.im
|
||||
.member_leave(&im_session, &path.workspace_name, path.channel_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiEmptyResponse::ok("Left channel")))
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::channels::ChannelMember;
|
||||
use crate::service::AppService;
|
||||
use crate::service::im::ImSession;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub channel_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List channel members
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}/members",
|
||||
tag = "IM",
|
||||
operation_id = "imMemberList",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Members listed successfully", body = ApiResponse<Vec<ChannelMember>>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or channel not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn member_list(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = session.user().ok_or(AppError::Unauthorized)?;
|
||||
let im_session = ImSession::new(user_id);
|
||||
let result = service
|
||||
.im
|
||||
.member_list(
|
||||
&im_session,
|
||||
&path.workspace_name,
|
||||
path.channel_id,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::channels::ChannelMember;
|
||||
use crate::service::AppService;
|
||||
use crate::service::im::ImSession;
|
||||
use crate::service::im::members::UpdateMemberParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub channel_id: uuid::Uuid,
|
||||
pub user_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Update member role
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/im/workspaces/{workspace_name}/channels/{channel_id}/members/{user_id}",
|
||||
tag = "IM",
|
||||
operation_id = "imMemberUpdate",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = UpdateMemberParams,
|
||||
description = "Member update parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Member updated successfully", body = ApiResponse<ChannelMember>),
|
||||
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace, channel or member not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn member_update(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<UpdateMemberParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = session.user().ok_or(AppError::Unauthorized)?;
|
||||
let im_session = ImSession::new(user_id);
|
||||
let result = service
|
||||
.im
|
||||
.member_update(
|
||||
&im_session,
|
||||
&path.workspace_name,
|
||||
path.channel_id,
|
||||
path.user_id,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
pub mod category_create;
|
||||
pub mod category_delete;
|
||||
pub mod category_list;
|
||||
pub mod category_update;
|
||||
pub mod channel_create;
|
||||
pub mod channel_delete;
|
||||
pub mod channel_get;
|
||||
pub mod channel_list;
|
||||
pub mod channel_update;
|
||||
pub mod member_invite;
|
||||
pub mod member_join;
|
||||
pub mod member_kick;
|
||||
pub mod member_leave;
|
||||
pub mod member_list;
|
||||
pub mod member_update;
|
||||
|
||||
use actix_web::web;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/im/workspaces/{workspace_name}")
|
||||
// Channels
|
||||
.route("/channels", web::get().to(channel_list::channel_list))
|
||||
.route("/channels", web::post().to(channel_create::channel_create))
|
||||
.route(
|
||||
"/channels/{channel_id}",
|
||||
web::get().to(channel_get::channel_get),
|
||||
)
|
||||
.route(
|
||||
"/channels/{channel_id}",
|
||||
web::put().to(channel_update::channel_update),
|
||||
)
|
||||
.route(
|
||||
"/channels/{channel_id}",
|
||||
web::delete().to(channel_delete::channel_delete),
|
||||
)
|
||||
// Members
|
||||
.route(
|
||||
"/channels/{channel_id}/members",
|
||||
web::get().to(member_list::member_list),
|
||||
)
|
||||
.route(
|
||||
"/channels/{channel_id}/members",
|
||||
web::post().to(member_invite::member_invite),
|
||||
)
|
||||
.route(
|
||||
"/channels/{channel_id}/members/{user_id}",
|
||||
web::put().to(member_update::member_update),
|
||||
)
|
||||
.route(
|
||||
"/channels/{channel_id}/members/{user_id}",
|
||||
web::delete().to(member_kick::member_kick),
|
||||
)
|
||||
.route(
|
||||
"/channels/{channel_id}/join",
|
||||
web::post().to(member_join::member_join),
|
||||
)
|
||||
.route(
|
||||
"/channels/{channel_id}/leave",
|
||||
web::post().to(member_leave::member_leave),
|
||||
)
|
||||
// Categories
|
||||
.route("/categories", web::get().to(category_list::category_list))
|
||||
.route(
|
||||
"/categories",
|
||||
web::post().to(category_create::category_create),
|
||||
)
|
||||
.route(
|
||||
"/categories/{category_id}",
|
||||
web::put().to(category_update::category_update),
|
||||
)
|
||||
.route(
|
||||
"/categories/{category_id}",
|
||||
web::delete().to(category_delete::category_delete),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::api::response::ApiResponse;
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct IssueTokenRequest {
|
||||
pub user_id: String,
|
||||
pub scopes: Vec<String>,
|
||||
pub ttl_hours: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub extra: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct IssueTokenResponse {
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
pub expires_at: i64,
|
||||
pub key_id: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/internal/tokens",
|
||||
tag = "Internal",
|
||||
operation_id = "internalIssueToken",
|
||||
request_body = IssueTokenRequest,
|
||||
responses(
|
||||
(status = 200, description = "JWT token issued", body = ApiResponse<IssueTokenResponse>),
|
||||
(status = 401, description = "Authentication required"),
|
||||
(status = 403, description = "Admin permission required"),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn issue_token(
|
||||
session: Session,
|
||||
service: web::Data<AppService>,
|
||||
body: web::Json<IssueTokenRequest>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let _user_uid = session.user().ok_or(AppError::Unauthorized)?;
|
||||
|
||||
let ttl_secs = body.ttl_hours.unwrap_or(1) * 3600;
|
||||
|
||||
let tokens = service
|
||||
.internal_auth
|
||||
.issue_token(
|
||||
&body.user_id,
|
||||
ttl_secs,
|
||||
body.scopes.clone(),
|
||||
body.extra.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(IssueTokenResponse {
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
expires_at: tokens.expires_at,
|
||||
key_id: tokens.key_id,
|
||||
})))
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
pub mod issue_api_key;
|
||||
|
||||
use actix_web::web;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/internal")
|
||||
.route("/tokens", web::post().to(issue_api_key::issue_token)),
|
||||
);
|
||||
}
|
||||
+10
-3
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::Issue;
|
||||
use crate::models::base_info::{self, UserBaseInfo};
|
||||
use crate::models::issues::IssueDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::core::CreateIssueParams;
|
||||
use crate::session::Session;
|
||||
@@ -50,7 +51,7 @@ pub struct PathParams {
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Issue created successfully. Returns the newly created issue with full metadata.", body = ApiResponse<Issue>),
|
||||
(status = 201, description = "Issue created successfully. Returns the newly created issue with full metadata.", body = ApiResponse<IssueDetail>),
|
||||
(status = 400, description = "Invalid parameters: empty title, invalid repository/label/milestone references", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Member role or higher)", body = ApiErrorResponse),
|
||||
@@ -71,5 +72,11 @@ pub async fn create(
|
||||
.issue
|
||||
.issue_create(&session, &path.workspace_name, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(issue)))
|
||||
let author_id = issue.author_id;
|
||||
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
|
||||
let author = users
|
||||
.get(&author_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(issue.into_detail(author))))
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueComment;
|
||||
use crate::models::base_info::{self, UserBaseInfo};
|
||||
use crate::models::issues::IssueCommentDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::comments::CreateCommentParams;
|
||||
use crate::session::Session;
|
||||
@@ -38,7 +39,7 @@ pub struct PathParams {
|
||||
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 = 201, description = "Comment created successfully.", body = ApiResponse<IssueCommentDetail>),
|
||||
(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),
|
||||
@@ -62,5 +63,11 @@ pub async fn create_comment(
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(comment)))
|
||||
let author_id = comment.author_id;
|
||||
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
|
||||
let author = users
|
||||
.get(&author_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(comment.into_detail(author))))
|
||||
}
|
||||
|
||||
+10
-3
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::Issue;
|
||||
use crate::models::base_info::{self, UserBaseInfo};
|
||||
use crate::models::issues::IssueDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
@@ -27,7 +28,7 @@ pub struct PathParams {
|
||||
operation_id = "issueGet",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Issue retrieved successfully. Returns complete issue with all metadata.", body = ApiResponse<Issue>),
|
||||
(status = 200, description = "Issue retrieved successfully. Returns complete issue with all metadata.", body = ApiResponse<IssueDetail>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
|
||||
@@ -46,5 +47,11 @@ pub async fn get(
|
||||
.issue
|
||||
.issue_get(&session, &path.workspace_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(issue)))
|
||||
let author_id = issue.author_id;
|
||||
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
|
||||
let author = users
|
||||
.get(&author_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(issue.into_detail(author))))
|
||||
}
|
||||
|
||||
+16
-3
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::Issue;
|
||||
use crate::models::base_info::{self, UserBaseInfo};
|
||||
use crate::models::issues::IssueDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::service::issues::core::IssueListFilters;
|
||||
use crate::session::Session;
|
||||
@@ -48,7 +49,7 @@ pub struct QueryParams {
|
||||
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 = 200, description = "Issues listed successfully. Returns filtered array of issue objects with metadata.", body = ApiResponse<Vec<IssueDetail>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
@@ -81,5 +82,17 @@ pub async fn list(
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(issues)))
|
||||
let user_ids: Vec<_> = issues.iter().map(|i| i.author_id).collect();
|
||||
let users = base_info::resolve_users(&service.ctx.db, &user_ids).await?;
|
||||
let details: Vec<IssueDetail> = issues
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
let author = users
|
||||
.get(&i.author_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| UserBaseInfo::placeholder(i.author_id));
|
||||
i.into_detail(author)
|
||||
})
|
||||
.collect();
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(details)))
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::issues::IssueComment;
|
||||
use crate::models::base_info::{self, UserBaseInfo};
|
||||
use crate::models::issues::IssueCommentDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
@@ -31,7 +32,7 @@ pub struct QueryParams {
|
||||
operation_id = "issueListComments",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Comments listed successfully.", body = ApiResponse<Vec<IssueComment>>),
|
||||
(status = 200, description = "Comments listed successfully.", body = ApiResponse<Vec<IssueCommentDetail>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to view this issue", body = ApiErrorResponse),
|
||||
(status = 404, description = "Workspace or issue not found", body = ApiErrorResponse),
|
||||
@@ -55,5 +56,17 @@ pub async fn list_comments(
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(comments)))
|
||||
let user_ids: Vec<_> = comments.iter().map(|c| c.author_id).collect();
|
||||
let users = base_info::resolve_users(&service.ctx.db, &user_ids).await?;
|
||||
let details: Vec<IssueCommentDetail> = comments
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
let author = users
|
||||
.get(&c.author_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| UserBaseInfo::placeholder(c.author_id));
|
||||
c.into_detail(author)
|
||||
})
|
||||
.collect();
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(details)))
|
||||
}
|
||||
|
||||
+2
-2
@@ -36,7 +36,7 @@ use actix_web::web;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/issues")
|
||||
web::scope("")
|
||||
// Core
|
||||
.route("", web::get().to(list::list))
|
||||
.route("", web::post().to(create::create))
|
||||
@@ -144,7 +144,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
|
||||
pub fn configure_repo_level(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/issues")
|
||||
web::scope("")
|
||||
.route("/labels", web::get().to(list_labels::list_labels))
|
||||
.route("/labels", web::post().to(create_label::create_label))
|
||||
.route(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
pub mod auth;
|
||||
pub mod im;
|
||||
pub mod internal;
|
||||
pub mod issue;
|
||||
pub mod notify;
|
||||
pub mod openapi;
|
||||
pub mod pr;
|
||||
pub mod repo;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Clear all notifications (dismiss all)
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/notifications",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationClearAll",
|
||||
responses(
|
||||
(status = 200, description = "All notifications cleared", body = ApiResponse<i64>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn clear_all_notifications(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service.notify.clear_all_notifications(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::notifications::NotificationBlock;
|
||||
use crate::service::AppService;
|
||||
use crate::service::notify::blocks::CreateBlockParams;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Create a notification block
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/notifications/blocks",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationCreateBlock",
|
||||
request_body(
|
||||
content = CreateBlockParams,
|
||||
description = "Block creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Block created", body = ApiResponse<NotificationBlock>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn create_block(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<CreateBlockParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.notify
|
||||
.create_block(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::notifications::NotificationSubscription;
|
||||
use crate::service::AppService;
|
||||
use crate::service::notify::subscriptions::CreateSubscriptionParams;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Create a notification subscription
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/notifications/subscriptions",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationCreateSubscription",
|
||||
request_body(
|
||||
content = CreateSubscriptionParams,
|
||||
description = "Subscription creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Subscription created", body = ApiResponse<NotificationSubscription>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn create_subscription(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<CreateSubscriptionParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.notify
|
||||
.create_subscription(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::notifications::NotificationTemplate;
|
||||
use crate::service::AppService;
|
||||
use crate::service::notify::templates::CreateTemplateParams;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Create a notification template (requires system admin)
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/notifications/templates",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationCreateTemplate",
|
||||
request_body(
|
||||
content = CreateTemplateParams,
|
||||
description = "Template creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Template created", body = ApiResponse<NotificationTemplate>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "System admin access required", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn create_template(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
params: web::Json<CreateTemplateParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.notify
|
||||
.create_template(&session, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub block_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Delete a notification block
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/notifications/blocks/{block_id}",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationDeleteBlock",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Block deleted", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Block not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn delete_block(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service.notify.delete_block(&session, path.block_id).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Block deleted".to_string())))
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub notification_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Delete a notification
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/notifications/{notification_id}",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationDelete",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Notification deleted", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Notification not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn delete_notification(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.notify
|
||||
.delete_notification(&session, path.notification_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Notification deleted".to_string())))
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub subscription_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Delete a notification subscription
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/notifications/subscriptions/{subscription_id}",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationDeleteSubscription",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Subscription deleted", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Subscription not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn delete_subscription(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.notify
|
||||
.delete_subscription(&session, path.subscription_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Subscription deleted".to_string())))
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub template_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Delete a notification template (requires system admin)
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/notifications/templates/{template_id}",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationDeleteTemplate",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Template deleted", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "System admin access required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Template not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn delete_template(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.notify
|
||||
.delete_template(&session, path.template_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Template deleted".to_string())))
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::base_info;
|
||||
use crate::models::notifications::NotificationDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub notification_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Dismiss a notification
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/notifications/{notification_id}/dismiss",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationDismiss",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Notification dismissed", body = ApiResponse<NotificationDetail>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Notification not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn dismiss_notification(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let notification = service
|
||||
.notify
|
||||
.dismiss_notification(&session, path.notification_id)
|
||||
.await?;
|
||||
|
||||
let actor = match notification.actor_id {
|
||||
Some(id) => base_info::resolve_users(&service.ctx.db, &[id])
|
||||
.await?
|
||||
.remove(&id),
|
||||
None => None,
|
||||
};
|
||||
let workspace = match notification.workspace_id {
|
||||
Some(id) => base_info::resolve_workspaces(&service.ctx.db, &[id])
|
||||
.await?
|
||||
.remove(&id),
|
||||
None => None,
|
||||
};
|
||||
let repo = match notification.repo_id {
|
||||
Some(id) => base_info::resolve_repos(&service.ctx.db, &[id])
|
||||
.await?
|
||||
.remove(&id),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(
|
||||
notification.into_detail(actor, workspace, repo),
|
||||
)))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::notifications::NotificationTemplate;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub template_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Get a notification template by ID (requires system admin)
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/notifications/templates/{template_id}",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationGetTemplate",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Template retrieved", body = ApiResponse<NotificationTemplate>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "System admin access required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Template not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_template(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.notify
|
||||
.get_template(&session, path.template_id)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Get unread notification count for the current user
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/notifications/unread-count",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationUnreadCount",
|
||||
responses(
|
||||
(status = 200, description = "Unread count returned successfully", body = ApiResponse<i64>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_unread_count(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service.notify.count_unread(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::notifications::NotificationBlock;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List notification blocks for the current user
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/notifications/blocks",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationListBlocks",
|
||||
params(QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Blocks listed successfully", body = ApiResponse<Vec<NotificationBlock>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_blocks(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.notify
|
||||
.list_blocks(
|
||||
&session,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::notifications::NotificationDelivery;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List notification deliveries for the current user
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/notifications/deliveries",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationListDeliveries",
|
||||
params(QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Deliveries listed successfully", body = ApiResponse<Vec<NotificationDelivery>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_deliveries(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.notify
|
||||
.list_deliveries(
|
||||
&session,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::notifications::NotificationDelivery;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub notification_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List deliveries for a specific notification
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/notifications/{notification_id}/deliveries",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationListDeliveriesForNotification",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Deliveries listed successfully", body = ApiResponse<Vec<NotificationDelivery>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Notification not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_deliveries_for_notification(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.notify
|
||||
.list_deliveries_for_notification(
|
||||
&session,
|
||||
path.notification_id,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::base_info;
|
||||
use crate::models::notifications::NotificationDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub unread_only: Option<bool>,
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List notifications for the current user
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/notifications",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationList",
|
||||
params(QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Notifications listed successfully", body = ApiResponse<Vec<NotificationDetail>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_notifications(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let notifications = service
|
||||
.notify
|
||||
.list_notifications(
|
||||
&session,
|
||||
query.unread_only.unwrap_or(false),
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let actor_ids: Vec<_> = notifications.iter().filter_map(|n| n.actor_id).collect();
|
||||
let workspace_ids: Vec<_> = notifications
|
||||
.iter()
|
||||
.filter_map(|n| n.workspace_id)
|
||||
.collect();
|
||||
let repo_ids: Vec<_> = notifications.iter().filter_map(|n| n.repo_id).collect();
|
||||
|
||||
let actors = base_info::resolve_users(&service.ctx.db, &actor_ids).await?;
|
||||
let workspaces = base_info::resolve_workspaces(&service.ctx.db, &workspace_ids).await?;
|
||||
let repos = base_info::resolve_repos(&service.ctx.db, &repo_ids).await?;
|
||||
|
||||
let details: Vec<NotificationDetail> = notifications
|
||||
.into_iter()
|
||||
.map(|n| {
|
||||
let actor = n.actor_id.and_then(|id| actors.get(&id).cloned());
|
||||
let workspace = n.workspace_id.and_then(|id| workspaces.get(&id).cloned());
|
||||
let repo = n.repo_id.and_then(|id| repos.get(&id).cloned());
|
||||
n.into_detail(actor, workspace, repo)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(details)))
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::notifications::NotificationSubscription;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List notification subscriptions for the current user
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/notifications/subscriptions",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationListSubscriptions",
|
||||
params(QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Subscriptions listed successfully", body = ApiResponse<Vec<NotificationSubscription>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_subscriptions(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.notify
|
||||
.list_subscriptions(
|
||||
&session,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::notifications::NotificationTemplate;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// List notification templates (requires system admin)
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/notifications/templates",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationListTemplates",
|
||||
params(QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Templates listed successfully", body = ApiResponse<Vec<NotificationTemplate>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "System admin access required", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn list_templates(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.notify
|
||||
.list_templates(
|
||||
&session,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
/// Mark all notifications as read
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/notifications/read-all",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationMarkAllAsRead",
|
||||
responses(
|
||||
(status = 200, description = "All notifications marked as read", body = ApiResponse<i64>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn mark_all_as_read(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service.notify.mark_all_as_read(&session).await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::base_info;
|
||||
use crate::models::notifications::NotificationDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub notification_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Mark a notification as read
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/notifications/{notification_id}/read",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationMarkAsRead",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Notification marked as read", body = ApiResponse<NotificationDetail>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Notification not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn mark_as_read(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let notification = service
|
||||
.notify
|
||||
.mark_as_read(&session, path.notification_id)
|
||||
.await?;
|
||||
|
||||
let actor = match notification.actor_id {
|
||||
Some(id) => base_info::resolve_users(&service.ctx.db, &[id])
|
||||
.await?
|
||||
.remove(&id),
|
||||
None => None,
|
||||
};
|
||||
let workspace = match notification.workspace_id {
|
||||
Some(id) => base_info::resolve_workspaces(&service.ctx.db, &[id])
|
||||
.await?
|
||||
.remove(&id),
|
||||
None => None,
|
||||
};
|
||||
let repo = match notification.repo_id {
|
||||
Some(id) => base_info::resolve_repos(&service.ctx.db, &[id])
|
||||
.await?
|
||||
.remove(&id),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(
|
||||
notification.into_detail(actor, workspace, repo),
|
||||
)))
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
pub mod clear_all_notifications;
|
||||
pub mod create_block;
|
||||
pub mod create_subscription;
|
||||
pub mod create_template;
|
||||
pub mod delete_block;
|
||||
pub mod delete_notification;
|
||||
pub mod delete_subscription;
|
||||
pub mod delete_template;
|
||||
pub mod dismiss_notification;
|
||||
pub mod get_template;
|
||||
pub mod get_unread_count;
|
||||
pub mod list_blocks;
|
||||
pub mod list_deliveries;
|
||||
pub mod list_deliveries_for_notification;
|
||||
pub mod list_notifications;
|
||||
pub mod list_subscriptions;
|
||||
pub mod list_templates;
|
||||
pub mod mark_all_as_read;
|
||||
pub mod mark_as_read;
|
||||
pub mod update_subscription;
|
||||
pub mod update_template;
|
||||
|
||||
use actix_web::web;
|
||||
|
||||
/// Configure notification routes under `/api/v1/notifications`
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/notifications")
|
||||
// Non-parameterized paths first
|
||||
.route("", web::get().to(list_notifications::list_notifications))
|
||||
.route(
|
||||
"",
|
||||
web::delete().to(clear_all_notifications::clear_all_notifications),
|
||||
)
|
||||
.route(
|
||||
"/unread-count",
|
||||
web::get().to(get_unread_count::get_unread_count),
|
||||
)
|
||||
.route(
|
||||
"/read-all",
|
||||
web::put().to(mark_all_as_read::mark_all_as_read),
|
||||
)
|
||||
// Parameterized notification paths
|
||||
.route(
|
||||
"/{notification_id}",
|
||||
web::delete().to(delete_notification::delete_notification),
|
||||
)
|
||||
.route(
|
||||
"/{notification_id}/read",
|
||||
web::put().to(mark_as_read::mark_as_read),
|
||||
)
|
||||
.route(
|
||||
"/{notification_id}/dismiss",
|
||||
web::post().to(dismiss_notification::dismiss_notification),
|
||||
)
|
||||
.route(
|
||||
"/{notification_id}/deliveries",
|
||||
web::get().to(list_deliveries_for_notification::list_deliveries_for_notification),
|
||||
)
|
||||
// Subscriptions
|
||||
.route(
|
||||
"/subscriptions",
|
||||
web::get().to(list_subscriptions::list_subscriptions),
|
||||
)
|
||||
.route(
|
||||
"/subscriptions",
|
||||
web::post().to(create_subscription::create_subscription),
|
||||
)
|
||||
.route(
|
||||
"/subscriptions/{subscription_id}",
|
||||
web::put().to(update_subscription::update_subscription),
|
||||
)
|
||||
.route(
|
||||
"/subscriptions/{subscription_id}",
|
||||
web::delete().to(delete_subscription::delete_subscription),
|
||||
)
|
||||
// Blocks
|
||||
.route("/blocks", web::get().to(list_blocks::list_blocks))
|
||||
.route("/blocks", web::post().to(create_block::create_block))
|
||||
.route(
|
||||
"/blocks/{block_id}",
|
||||
web::delete().to(delete_block::delete_block),
|
||||
)
|
||||
// Deliveries
|
||||
.route(
|
||||
"/deliveries",
|
||||
web::get().to(list_deliveries::list_deliveries),
|
||||
)
|
||||
// Templates
|
||||
.route("/templates", web::get().to(list_templates::list_templates))
|
||||
.route(
|
||||
"/templates",
|
||||
web::post().to(create_template::create_template),
|
||||
)
|
||||
.route(
|
||||
"/templates/{template_id}",
|
||||
web::get().to(get_template::get_template),
|
||||
)
|
||||
.route(
|
||||
"/templates/{template_id}",
|
||||
web::put().to(update_template::update_template),
|
||||
)
|
||||
.route(
|
||||
"/templates/{template_id}",
|
||||
web::delete().to(delete_template::delete_template),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::notifications::NotificationSubscription;
|
||||
use crate::service::AppService;
|
||||
use crate::service::notify::subscriptions::UpdateSubscriptionParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub subscription_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Update a notification subscription
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/notifications/subscriptions/{subscription_id}",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationUpdateSubscription",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = UpdateSubscriptionParams,
|
||||
description = "Subscription update parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Subscription updated", body = ApiResponse<NotificationSubscription>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 404, description = "Subscription not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn update_subscription(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<UpdateSubscriptionParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.notify
|
||||
.update_subscription(&session, path.subscription_id, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::notifications::NotificationTemplate;
|
||||
use crate::service::AppService;
|
||||
use crate::service::notify::templates::UpdateTemplateParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub template_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// Update a notification template (requires system admin)
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/notifications/templates/{template_id}",
|
||||
tag = "Notifications",
|
||||
operation_id = "notificationUpdateTemplate",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = UpdateTemplateParams,
|
||||
description = "Template update parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Template updated", body = ApiResponse<NotificationTemplate>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "System admin access required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Template not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn update_template(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<UpdateTemplateParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.notify
|
||||
.update_template(&session, path.template_id, params.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
+231
-22
@@ -13,34 +13,52 @@ 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::user::upload_avatar::AvatarData;
|
||||
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::base_info::{
|
||||
ChannelBaseInfo, IssueBaseInfo, PullRequestBaseInfo,
|
||||
RepoBaseInfo, UserBaseInfo, WikiPageBaseInfo, WorkspaceBaseInfo,
|
||||
};
|
||||
use crate::models::channels::channel::ChannelDetail;
|
||||
use crate::models::issues::issue::IssueDetail;
|
||||
use crate::models::issues::issue_comments::IssueCommentDetail;
|
||||
use crate::models::issues::{
|
||||
Issue, IssueAssignee, IssueComment, IssueEvent, IssueLabel, IssueLabelRelation, IssueMilestone,
|
||||
IssuePrRelation, IssueReaction, IssueRepoRelation, IssueSubscriber, IssueTemplate,
|
||||
};
|
||||
use crate::models::notifications::notification::NotificationDetail;
|
||||
use crate::models::notifications::{
|
||||
Notification, NotificationBlock, NotificationDelivery, NotificationSubscription,
|
||||
NotificationTemplate,
|
||||
};
|
||||
use crate::models::prs::pr_review::PrReviewDetail;
|
||||
use crate::models::prs::pull_request::PullRequestDetail;
|
||||
use crate::models::prs::{
|
||||
PrAssignee, PrCheckRun, PrCommit, PrEvent, PrFile, PrLabel, PrLabelRelation, PrMergeStrategy,
|
||||
PrReaction, PrReview, PrReviewComment, PrStatus, PrSubscription, PullRequest,
|
||||
};
|
||||
use crate::models::repos::repo::RepoDetail;
|
||||
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,
|
||||
User, UserAppearance, UserBlock, UserDevice, UserFollow, UserGpgKey, UserNotifySetting,
|
||||
UserPresence, UserProfile, UserSecurityLog, UserSshKey,
|
||||
};
|
||||
use crate::models::wiki::{WikiPage, WikiPageRevision};
|
||||
use crate::models::workspaces::workspace::WorkspaceDetail;
|
||||
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::change_password::ChangePasswordParams;
|
||||
use crate::service::auth::email::{EmailChangeRequest, EmailResponse, EmailVerifyRequest};
|
||||
use crate::service::auth::login::LoginParams;
|
||||
use crate::service::auth::me::ContextMe;
|
||||
@@ -60,6 +78,12 @@ use crate::service::issues::pr_relations::LinkPrParams;
|
||||
use crate::service::issues::reactions::CreateIssueReactionParams;
|
||||
use crate::service::issues::repo_relations::LinkRepoParams;
|
||||
use crate::service::issues::templates::{CreateTemplateParams, UpdateTemplateParams};
|
||||
use crate::service::notify::blocks::CreateBlockParams;
|
||||
use crate::service::notify::subscriptions::{CreateSubscriptionParams, UpdateSubscriptionParams};
|
||||
use crate::service::notify::templates::{
|
||||
CreateTemplateParams as NotifyCreateTemplateParams,
|
||||
UpdateTemplateParams as NotifyUpdateTemplateParams,
|
||||
};
|
||||
use crate::service::pr::check_runs::{CreateCheckRunParams, UpdateCheckRunParams};
|
||||
use crate::service::pr::core::{CreatePrParams, MergePrParams, PrListFilters, UpdatePrParams};
|
||||
use crate::service::pr::labels::{CreatePrLabelParams, UpdatePrLabelParams};
|
||||
@@ -80,30 +104,36 @@ 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::tags::{CreateTagParams, UpdateTagParams};
|
||||
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::account::UpdateUserAccountParams;
|
||||
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::user::security::{
|
||||
CreatePersonalAccessTokenResponse, 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::domains::{AddDomainParams, UpdateDomainParams};
|
||||
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};
|
||||
// IM Channel models
|
||||
use crate::models::channels::{Channel, ChannelCategory, ChannelMember};
|
||||
// IM Service params
|
||||
use crate::service::im::categories::{CreateCategoryParams, UpdateCategoryParams};
|
||||
use crate::service::im::channels::{ChannelListFilters, CreateChannelParams, UpdateChannelParams};
|
||||
use crate::service::im::members::{InviteMemberParams, UpdateMemberParams};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
@@ -120,6 +150,9 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
|
||||
(name = "Issues", description = "Issue tracking, comments, labels, milestones, assignees, events, reactions, subscribers, templates, and cross-references."),
|
||||
(name = "Pull Requests", description = "Pull request lifecycle including reviews, check runs, merge strategies, labels, assignees, events, reactions, and subscriptions."),
|
||||
(name = "Wiki", description = "Wiki page management including CRUD operations, revision history, version comparison, and page reversion."),
|
||||
(name = "Notifications", description = "User notification management including listing, reading, dismissing, deleting, subscriptions, blocks, deliveries, and templates."),
|
||||
(name = "Git", description = "Git-level operations including commits, branches, merges, rebase, blame, tree, blob, tags, and repository health/statistics endpoints."),
|
||||
(name = "IM", description = "Channel management, member administration, and category organization."),
|
||||
),
|
||||
paths(
|
||||
// Auth
|
||||
@@ -140,6 +173,7 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
|
||||
crate::api::auth::verify_2fa::handle,
|
||||
crate::api::auth::disable_2fa::handle,
|
||||
crate::api::auth::regenerate_2fa_backup_codes::handle,
|
||||
crate::api::auth::change_password::change_password,
|
||||
// User
|
||||
crate::api::user::get_account::get_account,
|
||||
crate::api::user::update_account::update_account,
|
||||
@@ -166,6 +200,15 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
|
||||
crate::api::user::list_security_logs::list_security_logs,
|
||||
crate::api::user::list_personal_access_tokens::list_tokens,
|
||||
crate::api::user::revoke_personal_access_token::revoke_token,
|
||||
crate::api::user::create_personal_access_token::create_token,
|
||||
crate::api::user::get_presence::get_presence,
|
||||
crate::api::user::update_presence::update_presence,
|
||||
crate::api::user::list_blocks::list_blocks,
|
||||
crate::api::user::block_user::block_user,
|
||||
crate::api::user::unblock_user::unblock_user,
|
||||
crate::api::user::list_follows::list_follows,
|
||||
crate::api::user::follow_user::follow_user,
|
||||
crate::api::user::unfollow_user::unfollow_user,
|
||||
// Issues
|
||||
crate::api::issue::list::list,
|
||||
crate::api::issue::get::get,
|
||||
@@ -226,11 +269,9 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
|
||||
// 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,
|
||||
@@ -238,11 +279,9 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
|
||||
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,
|
||||
@@ -251,23 +290,18 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
|
||||
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,
|
||||
@@ -277,7 +311,27 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
|
||||
crate::api::wiki::list_revisions::list_revisions,
|
||||
crate::api::wiki::get_revision::get_revision,
|
||||
crate::api::wiki::compare_revisions::compare_revisions,
|
||||
// Workspaces
|
||||
crate::api::notify::list_notifications::list_notifications,
|
||||
crate::api::notify::get_unread_count::get_unread_count,
|
||||
crate::api::notify::mark_as_read::mark_as_read,
|
||||
crate::api::notify::mark_all_as_read::mark_all_as_read,
|
||||
crate::api::notify::dismiss_notification::dismiss_notification,
|
||||
crate::api::notify::delete_notification::delete_notification,
|
||||
crate::api::notify::clear_all_notifications::clear_all_notifications,
|
||||
crate::api::notify::list_subscriptions::list_subscriptions,
|
||||
crate::api::notify::create_subscription::create_subscription,
|
||||
crate::api::notify::update_subscription::update_subscription,
|
||||
crate::api::notify::delete_subscription::delete_subscription,
|
||||
crate::api::notify::list_blocks::list_blocks,
|
||||
crate::api::notify::create_block::create_block,
|
||||
crate::api::notify::delete_block::delete_block,
|
||||
crate::api::notify::list_deliveries::list_deliveries,
|
||||
crate::api::notify::list_deliveries_for_notification::list_deliveries_for_notification,
|
||||
crate::api::notify::list_templates::list_templates,
|
||||
crate::api::notify::get_template::get_template,
|
||||
crate::api::notify::create_template::create_template,
|
||||
crate::api::notify::update_template::update_template,
|
||||
crate::api::notify::delete_template::delete_template,
|
||||
crate::api::workspace::list::handle,
|
||||
crate::api::workspace::get::handle,
|
||||
crate::api::workspace::create::handle,
|
||||
@@ -321,7 +375,17 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
|
||||
crate::api::workspace::request_approval::handle,
|
||||
crate::api::workspace::review_approval::handle,
|
||||
crate::api::workspace::audit_logs::handle,
|
||||
// Repos
|
||||
crate::api::workspace::restore::restore_workspace,
|
||||
crate::api::workspace::billing_history::billing_history,
|
||||
crate::api::workspace::list_webhook_deliveries::handle,
|
||||
crate::api::workspace::retry_webhook_delivery::handle,
|
||||
crate::api::workspace::update_domain::update_domain,
|
||||
crate::api::workspace::get_member::handle,
|
||||
crate::api::workspace::get_invitation::handle,
|
||||
crate::api::workspace::get_webhook::handle,
|
||||
crate::api::workspace::get_integration::handle,
|
||||
crate::api::workspace::get_domain::handle,
|
||||
crate::api::workspace::get_approval::handle,
|
||||
crate::api::repo::list::list,
|
||||
crate::api::repo::get::get,
|
||||
crate::api::repo::create::create,
|
||||
@@ -381,6 +445,61 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
|
||||
crate::api::repo::resolve_commit_comment::resolve_commit_comment,
|
||||
crate::api::repo::get_stats::get_stats,
|
||||
crate::api::repo::refresh_stats::refresh_stats,
|
||||
crate::api::repo::get_branch::get_branch,
|
||||
crate::api::repo::get_tag::get_tag,
|
||||
crate::api::repo::get_release::get_release,
|
||||
crate::api::repo::get_webhook::get_webhook,
|
||||
crate::api::repo::get_deploy_key::get_deploy_key,
|
||||
crate::api::repo::get_member::get_member,
|
||||
crate::api::repo::get_invitation::get_invitation,
|
||||
crate::api::repo::update_commit_comment::update_commit_comment,
|
||||
crate::api::repo::update_tag::update_tag,
|
||||
crate::api::repo::delete_fork::delete_fork,
|
||||
crate::api::repo::get_commit_status::get_commit_status,
|
||||
crate::api::repo::repo_webhook_deliveries::repo_webhook_deliveries,
|
||||
crate::api::repo::repo_webhook_retry::repo_webhook_retry,
|
||||
crate::api::repo::git::git_list_commits::git_list_commits,
|
||||
crate::api::repo::git::git_get_commit::git_get_commit,
|
||||
crate::api::repo::git::git_create_commit::git_create_commit,
|
||||
crate::api::repo::git::git_diff::git_diff,
|
||||
crate::api::repo::git::git_diff_stats::git_diff_stats,
|
||||
crate::api::repo::git::git_compare::git_compare,
|
||||
crate::api::repo::git::git_list_branches::git_list_branches,
|
||||
crate::api::repo::git::git_get_branch::git_get_branch,
|
||||
crate::api::repo::git::git_create_branch::git_create_branch,
|
||||
crate::api::repo::git::git_delete_branch::git_delete_branch,
|
||||
crate::api::repo::git::git_merge_check::git_merge_check,
|
||||
crate::api::repo::git::git_merge::git_merge,
|
||||
crate::api::repo::git::git_rebase::git_rebase,
|
||||
crate::api::repo::git::git_cherry_pick::git_cherry_pick,
|
||||
crate::api::repo::git::git_revert::git_revert,
|
||||
crate::api::repo::git::git_conflicts::git_conflicts,
|
||||
crate::api::repo::git::git_tree::git_tree,
|
||||
crate::api::repo::git::git_blob::git_blob,
|
||||
crate::api::repo::git::git_blame::git_blame,
|
||||
crate::api::repo::git::git_tags::git_tags,
|
||||
crate::api::repo::git::git_create_tag::git_create_tag,
|
||||
crate::api::repo::git::git_delete_tag::git_delete_tag,
|
||||
crate::api::repo::git::git_info::git_info,
|
||||
crate::api::repo::git::git_exists::git_exists,
|
||||
crate::api::repo::git::git_stats::git_stats,
|
||||
crate::api::repo::git::git_health::git_health,
|
||||
crate::api::repo::git::git_gc::git_gc,
|
||||
crate::api::im::channel_list::channel_list,
|
||||
crate::api::im::channel_create::channel_create,
|
||||
crate::api::im::channel_get::channel_get,
|
||||
crate::api::im::channel_update::channel_update,
|
||||
crate::api::im::channel_delete::channel_delete,
|
||||
crate::api::im::member_list::member_list,
|
||||
crate::api::im::member_invite::member_invite,
|
||||
crate::api::im::member_update::member_update,
|
||||
crate::api::im::member_kick::member_kick,
|
||||
crate::api::im::member_join::member_join,
|
||||
crate::api::im::member_leave::member_leave,
|
||||
crate::api::im::category_list::category_list,
|
||||
crate::api::im::category_create::category_create,
|
||||
crate::api::im::category_update::category_update,
|
||||
crate::api::im::category_delete::category_delete,
|
||||
),
|
||||
components(schemas(
|
||||
ApiEmptyResponse,
|
||||
@@ -416,12 +535,11 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
|
||||
Regenerate2FABackupCodesResponse,
|
||||
// User
|
||||
ApiResponse<User>,
|
||||
ApiResponse<UserAvatarResponse>,
|
||||
ApiResponse<AvatarData>,
|
||||
ApiResponse<String>,
|
||||
User,
|
||||
UpdateUserAccountParams,
|
||||
UploadUserAvatarParams,
|
||||
UserAvatarResponse,
|
||||
AvatarData,
|
||||
ApiResponse<UserAppearance>,
|
||||
UserAppearance,
|
||||
UpdateUserAppearanceParams,
|
||||
@@ -697,6 +815,97 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
|
||||
RepoCommitComment,
|
||||
CreateCommitCommentParams,
|
||||
RepoStats,
|
||||
// Notifications
|
||||
ApiResponse<Notification>,
|
||||
ApiResponse<Vec<Notification>>,
|
||||
Notification,
|
||||
ApiResponse<i64>,
|
||||
ApiResponse<NotificationSubscription>,
|
||||
ApiResponse<Vec<NotificationSubscription>>,
|
||||
NotificationSubscription,
|
||||
CreateSubscriptionParams,
|
||||
UpdateSubscriptionParams,
|
||||
ApiResponse<NotificationBlock>,
|
||||
ApiResponse<Vec<NotificationBlock>>,
|
||||
NotificationBlock,
|
||||
CreateBlockParams,
|
||||
ApiResponse<NotificationDelivery>,
|
||||
ApiResponse<Vec<NotificationDelivery>>,
|
||||
NotificationDelivery,
|
||||
ApiResponse<NotificationTemplate>,
|
||||
ApiResponse<Vec<NotificationTemplate>>,
|
||||
NotificationTemplate,
|
||||
NotifyCreateTemplateParams,
|
||||
NotifyUpdateTemplateParams,
|
||||
// Auth additions
|
||||
ChangePasswordParams,
|
||||
// User additions - Presence/Block/Follow
|
||||
ApiResponse<UserPresence>,
|
||||
UserPresence,
|
||||
ApiResponse<UserBlock>,
|
||||
ApiResponse<Vec<UserBlock>>,
|
||||
UserBlock,
|
||||
ApiResponse<UserFollow>,
|
||||
ApiResponse<Vec<UserFollow>>,
|
||||
UserFollow,
|
||||
ApiResponse<CreatePersonalAccessTokenResponse>,
|
||||
CreatePersonalAccessTokenResponse,
|
||||
// Workspace additions
|
||||
UpdateDomainParams,
|
||||
// Repo additions
|
||||
UpdateTagParams,
|
||||
// IM - Channels
|
||||
ApiResponse<Channel>,
|
||||
ApiResponse<Vec<Channel>>,
|
||||
Channel,
|
||||
CreateChannelParams,
|
||||
UpdateChannelParams,
|
||||
ChannelListFilters,
|
||||
// IM - Members
|
||||
ApiResponse<ChannelMember>,
|
||||
ApiResponse<Vec<ChannelMember>>,
|
||||
ChannelMember,
|
||||
InviteMemberParams,
|
||||
UpdateMemberParams,
|
||||
// IM - Categories
|
||||
ApiResponse<ChannelCategory>,
|
||||
ApiResponse<Vec<ChannelCategory>>,
|
||||
ChannelCategory,
|
||||
CreateCategoryParams,
|
||||
UpdateCategoryParams,
|
||||
// BaseInfo types
|
||||
UserBaseInfo,
|
||||
WorkspaceBaseInfo,
|
||||
RepoBaseInfo,
|
||||
ChannelBaseInfo,
|
||||
IssueBaseInfo,
|
||||
PullRequestBaseInfo,
|
||||
WikiPageBaseInfo,
|
||||
// Detail types
|
||||
ApiResponse<RepoDetail>,
|
||||
ApiResponse<Vec<RepoDetail>>,
|
||||
RepoDetail,
|
||||
ApiResponse<WorkspaceDetail>,
|
||||
ApiResponse<Vec<WorkspaceDetail>>,
|
||||
WorkspaceDetail,
|
||||
ApiResponse<ChannelDetail>,
|
||||
ApiResponse<Vec<ChannelDetail>>,
|
||||
ChannelDetail,
|
||||
ApiResponse<IssueDetail>,
|
||||
ApiResponse<Vec<IssueDetail>>,
|
||||
IssueDetail,
|
||||
ApiResponse<IssueCommentDetail>,
|
||||
ApiResponse<Vec<IssueCommentDetail>>,
|
||||
IssueCommentDetail,
|
||||
ApiResponse<PullRequestDetail>,
|
||||
ApiResponse<Vec<PullRequestDetail>>,
|
||||
PullRequestDetail,
|
||||
ApiResponse<PrReviewDetail>,
|
||||
ApiResponse<Vec<PrReviewDetail>>,
|
||||
PrReviewDetail,
|
||||
ApiResponse<NotificationDetail>,
|
||||
ApiResponse<Vec<NotificationDetail>>,
|
||||
NotificationDetail,
|
||||
))
|
||||
)]
|
||||
pub struct OpenApiDoc;
|
||||
|
||||
+10
-3
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PullRequest;
|
||||
use crate::models::base_info::{self, UserBaseInfo};
|
||||
use crate::models::prs::PullRequestDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::service::pr::core::CreatePrParams;
|
||||
use crate::session::Session;
|
||||
@@ -51,7 +52,7 @@ pub struct PathParams {
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "PR created successfully. Returns the newly created PR with full metadata.", body = ApiResponse<PullRequest>),
|
||||
(status = 201, description = "PR created successfully. Returns the newly created PR with full metadata.", body = ApiResponse<PullRequestDetail>),
|
||||
(status = 400, description = "Invalid parameters: empty title, non-existent branch/commit, or invalid fork relationship", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (requires Member role or higher)", body = ApiErrorResponse),
|
||||
@@ -77,5 +78,11 @@ pub async fn create(
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(pr)))
|
||||
let author_id = pr.author_id;
|
||||
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
|
||||
let author = users
|
||||
.get(&author_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(pr.into_detail(author))))
|
||||
}
|
||||
|
||||
+10
-3
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PullRequest;
|
||||
use crate::models::base_info::{self, UserBaseInfo};
|
||||
use crate::models::prs::PullRequestDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
@@ -29,7 +30,7 @@ pub struct PathParams {
|
||||
operation_id = "prGet",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "PR retrieved successfully. Returns complete PR with all metadata.", body = ApiResponse<PullRequest>),
|
||||
(status = 200, description = "PR retrieved successfully. Returns complete PR with all metadata.", body = ApiResponse<PullRequestDetail>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to access this repository", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository, workspace, or PR not found", body = ApiErrorResponse),
|
||||
@@ -48,5 +49,11 @@ pub async fn get(
|
||||
.pr
|
||||
.pr_get(&session, &path.workspace_name, &path.repo_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(pr)))
|
||||
let author_id = pr.author_id;
|
||||
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
|
||||
let author = users
|
||||
.get(&author_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(pr.into_detail(author))))
|
||||
}
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@ pub struct QP {
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
// ── Repo-level labels ──
|
||||
// Section: Repo-level labels
|
||||
|
||||
/// List PR labels in a repository
|
||||
#[utoipa::path(
|
||||
@@ -189,7 +189,7 @@ pub async fn delete_label(
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Label deleted".to_string())))
|
||||
}
|
||||
|
||||
// ── PR-level label relations ──
|
||||
// Section: PR-level label relations
|
||||
|
||||
/// List labels assigned to a PR
|
||||
#[utoipa::path(
|
||||
|
||||
+16
-3
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PullRequest;
|
||||
use crate::models::base_info::{self, UserBaseInfo};
|
||||
use crate::models::prs::PullRequestDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::service::pr::core::PrListFilters;
|
||||
use crate::session::Session;
|
||||
@@ -43,7 +44,7 @@ pub struct QueryParams {
|
||||
operation_id = "prList",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "PRs listed successfully. Returns filtered array of PR objects.", body = ApiResponse<Vec<PullRequest>>),
|
||||
(status = 200, description = "PRs listed successfully. Returns filtered array of PR objects.", body = ApiResponse<Vec<PullRequestDetail>>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to access this repository", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||
@@ -75,5 +76,17 @@ pub async fn list(
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(prs)))
|
||||
let user_ids: Vec<_> = prs.iter().map(|p| p.author_id).collect();
|
||||
let users = base_info::resolve_users(&service.ctx.db, &user_ids).await?;
|
||||
let details: Vec<PullRequestDetail> = prs
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
let author = users
|
||||
.get(&p.author_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| UserBaseInfo::placeholder(p.author_id));
|
||||
p.into_detail(author)
|
||||
})
|
||||
.collect();
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(details)))
|
||||
}
|
||||
|
||||
+28
-2
@@ -16,8 +16,10 @@ pub mod merge;
|
||||
pub mod merge_strategy;
|
||||
pub mod reactions;
|
||||
pub mod reopen;
|
||||
pub mod review_requests;
|
||||
pub mod reviews;
|
||||
pub mod subscriptions;
|
||||
pub mod templates;
|
||||
pub mod update;
|
||||
|
||||
use actix_web::web;
|
||||
@@ -25,12 +27,23 @@ 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")
|
||||
web::scope("")
|
||||
// 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))
|
||||
// Templates
|
||||
.route("/templates", web::get().to(templates::list_templates))
|
||||
.route("/templates", web::post().to(templates::create_template))
|
||||
.route(
|
||||
"/templates/{template_id}",
|
||||
web::put().to(templates::update_template),
|
||||
)
|
||||
.route(
|
||||
"/templates/{template_id}",
|
||||
web::delete().to(templates::delete_template),
|
||||
)
|
||||
// Core
|
||||
.route("", web::get().to(list::list))
|
||||
.route("", web::post().to(create::create))
|
||||
@@ -156,6 +169,19 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
"/{number}/subscribe",
|
||||
web::delete().to(subscriptions::unsubscribe),
|
||||
)
|
||||
.route("/{number}/mute", web::put().to(subscriptions::mute)),
|
||||
.route("/{number}/mute", web::put().to(subscriptions::mute))
|
||||
// Review Requests
|
||||
.route(
|
||||
"/{number}/requested_reviewers",
|
||||
web::get().to(review_requests::list_requested_reviewers),
|
||||
)
|
||||
.route(
|
||||
"/{number}/requested_reviewers",
|
||||
web::post().to(review_requests::request_reviewers),
|
||||
)
|
||||
.route(
|
||||
"/{number}/requested_reviewers/{user_id}",
|
||||
web::delete().to(review_requests::remove_requested_reviewer),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PrReviewRequest;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub number: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct ReviewerPathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub number: i64,
|
||||
pub user_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct RequestReviewersBody {
|
||||
pub reviewer_ids: Vec<Uuid>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/requested_reviewers",
|
||||
tag = "PullRequests",
|
||||
operation_id = "prListRequestedReviewers",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "List of requested reviewers", body = ApiResponse<Vec<PrReviewRequest>>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_requested_reviewers(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.pr
|
||||
.pr_requested_reviewers(&session, &path.workspace_name, &path.repo_name, path.number)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/requested_reviewers",
|
||||
tag = "PullRequests",
|
||||
operation_id = "prRequestReviewers",
|
||||
params(PathParams),
|
||||
request_body(content = RequestReviewersBody),
|
||||
responses(
|
||||
(status = 201, description = "Reviewers requested", body = ApiResponse<Vec<PrReviewRequest>>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn request_reviewers(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
body: web::Json<RequestReviewersBody>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.pr
|
||||
.pr_request_reviewers(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
body.reviewer_ids.clone(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/requested_reviewers/{user_id}",
|
||||
tag = "PullRequests",
|
||||
operation_id = "prRemoveRequestedReviewer",
|
||||
params(ReviewerPathParams),
|
||||
responses(
|
||||
(status = 200, description = "Reviewer removed", body = ApiResponse<String>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn remove_requested_reviewer(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<ReviewerPathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.pr
|
||||
.pr_remove_requested_reviewer(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.number,
|
||||
path.user_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("reviewer removed".to_string())))
|
||||
}
|
||||
+41
-9
@@ -4,7 +4,9 @@ use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::{PrReview, PrReviewComment};
|
||||
use crate::models::base_info;
|
||||
use crate::models::base_info::UserBaseInfo;
|
||||
use crate::models::prs::{PrReviewComment, PrReviewDetail};
|
||||
use crate::service::AppService;
|
||||
use crate::service::pr::reviews::{
|
||||
AddReplyParams, CreateReviewParams, DismissReviewParams, SubmitReviewParams,
|
||||
@@ -59,7 +61,7 @@ pub struct QP {
|
||||
operation_id = "prListReviews",
|
||||
params(PrPath, QP),
|
||||
responses(
|
||||
(status = 200, description = "Reviews listed.", body = ApiResponse<Vec<PrReview>>),
|
||||
(status = 200, description = "Reviews listed.", body = ApiResponse<Vec<PrReviewDetail>>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "PR not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
@@ -83,7 +85,19 @@ pub async fn list_reviews(
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(reviews)))
|
||||
let user_ids: Vec<_> = reviews.iter().map(|r| r.author_id).collect();
|
||||
let users = base_info::resolve_users(&service.ctx.db, &user_ids).await?;
|
||||
let details: Vec<PrReviewDetail> = reviews
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
let author = users
|
||||
.get(&r.author_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| UserBaseInfo::placeholder(r.author_id));
|
||||
r.into_detail(author)
|
||||
})
|
||||
.collect();
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(details)))
|
||||
}
|
||||
|
||||
/// Create a review. States: pending, approved, changes_requested, commented. Authors cannot approve their own PRs.
|
||||
@@ -95,7 +109,7 @@ pub async fn list_reviews(
|
||||
params(PrPath),
|
||||
request_body(content = CreateReviewParams, description = "Review parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 201, description = "Review created.", body = ApiResponse<PrReview>),
|
||||
(status = 201, description = "Review created.", body = ApiResponse<PrReviewDetail>),
|
||||
(status = 400, description = "Invalid state or self-approval", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
@@ -120,7 +134,13 @@ pub async fn create_review(
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(review)))
|
||||
let author_id = review.author_id;
|
||||
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
|
||||
let author = users
|
||||
.get(&author_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(review.into_detail(author))))
|
||||
}
|
||||
|
||||
/// Submit a pending review. Changes its state to approved, changes_requested, or commented.
|
||||
@@ -132,7 +152,7 @@ pub async fn create_review(
|
||||
params(ReviewPath),
|
||||
request_body(content = SubmitReviewParams, description = "Submit parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 200, description = "Review submitted.", body = ApiResponse<PrReview>),
|
||||
(status = 200, description = "Review submitted.", body = ApiResponse<PrReviewDetail>),
|
||||
(status = 400, description = "Invalid state or self-approval", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions", body = ApiErrorResponse),
|
||||
@@ -158,7 +178,13 @@ pub async fn submit_review(
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(review)))
|
||||
let author_id = review.author_id;
|
||||
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
|
||||
let author = users
|
||||
.get(&author_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(review.into_detail(author))))
|
||||
}
|
||||
|
||||
/// Dismiss a submitted review. Requires Admin role.
|
||||
@@ -170,7 +196,7 @@ pub async fn submit_review(
|
||||
params(ReviewPath),
|
||||
request_body(content = DismissReviewParams, description = "Dismiss parameters", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 200, description = "Review dismissed.", body = ApiResponse<PrReview>),
|
||||
(status = 200, description = "Review dismissed.", body = ApiResponse<PrReviewDetail>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions (Admin required)", body = ApiErrorResponse),
|
||||
(status = 404, description = "Review not found or not submitted", body = ApiErrorResponse),
|
||||
@@ -195,7 +221,13 @@ pub async fn dismiss_review(
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(review)))
|
||||
let author_id = review.author_id;
|
||||
let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?;
|
||||
let author = users
|
||||
.get(&author_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| UserBaseInfo::placeholder(author_id));
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(review.into_detail(author))))
|
||||
}
|
||||
|
||||
/// List comments for a specific review
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::prs::PrTemplate;
|
||||
use crate::service::AppService;
|
||||
use crate::service::pr::templates::{CreatePrTemplateParams, UpdatePrTemplateParams};
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct TemplatePathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub template_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/templates",
|
||||
tag = "PullRequests",
|
||||
operation_id = "prListTemplates",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "List of PR templates", body = ApiResponse<Vec<PrTemplate>>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_templates(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.pr
|
||||
.pr_templates(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/templates",
|
||||
tag = "PullRequests",
|
||||
operation_id = "prCreateTemplate",
|
||||
params(PathParams),
|
||||
request_body(content = CreatePrTemplateParams),
|
||||
responses(
|
||||
(status = 201, description = "Template created", body = ApiResponse<PrTemplate>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn create_template(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
body: web::Json<CreatePrTemplateParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.pr
|
||||
.pr_create_template(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
body.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/templates/{template_id}",
|
||||
tag = "PullRequests",
|
||||
operation_id = "prUpdateTemplate",
|
||||
params(TemplatePathParams),
|
||||
request_body(content = UpdatePrTemplateParams),
|
||||
responses(
|
||||
(status = 200, description = "Template updated", body = ApiResponse<PrTemplate>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn update_template(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<TemplatePathParams>,
|
||||
body: web::Json<UpdatePrTemplateParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.pr
|
||||
.pr_update_template(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.template_id,
|
||||
body.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/templates/{template_id}",
|
||||
tag = "PullRequests",
|
||||
operation_id = "prDeleteTemplate",
|
||||
params(TemplatePathParams),
|
||||
responses(
|
||||
(status = 200, description = "Template deleted", body = ApiResponse<String>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn delete_template(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<TemplatePathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.pr
|
||||
.pr_delete_template(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.template_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("template deleted".to_string())))
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::repo::contributors::Contributor;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/contributors",
|
||||
tag = "Repos",
|
||||
operation_id = "repoListContributors",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "List of contributors", body = ApiResponse<Vec<Contributor>>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repo not found", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn list_contributors(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.repo_contributors(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
query.limit.unwrap_or(50),
|
||||
query.offset.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
+14
-3
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::Repo;
|
||||
use crate::models::base_info::{resolve_users, resolve_workspaces};
|
||||
use crate::models::repos::RepoDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::service::repo::core::CreateRepoParams;
|
||||
use crate::session::Session;
|
||||
@@ -42,7 +43,7 @@ pub struct PathParams {
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Repository created successfully. Returns the newly created repository with full metadata.", body = ApiResponse<Repo>),
|
||||
(status = 201, description = "Repository created successfully. Returns the newly created repository with full metadata.", body = ApiResponse<RepoDetail>),
|
||||
(status = 400, description = "Invalid parameters: name too long, invalid characters, or invalid visibility", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to create repositories in this workspace", body = ApiErrorResponse),
|
||||
@@ -65,5 +66,15 @@ pub async fn create(
|
||||
.repo_create(&session, &path.workspace_name, params.into_inner())
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(repo)))
|
||||
let db = &service.ctx.db;
|
||||
let users = resolve_users(db, &[repo.owner_id]).await?;
|
||||
let workspaces = resolve_workspaces(db, &[repo.workspace_id]).await?;
|
||||
let owner = users.get(&repo.owner_id).cloned().unwrap_or_default();
|
||||
let workspace = workspaces
|
||||
.get(&repo.workspace_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let detail = repo.into_detail(owner, workspace);
|
||||
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(detail)))
|
||||
}
|
||||
|
||||
+10
-29
@@ -9,42 +9,24 @@ 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,
|
||||
/// Branch ID (UUID)
|
||||
pub branch_id: uuid::Uuid,
|
||||
pub branch_name: String,
|
||||
}
|
||||
|
||||
/// Delete a branch
|
||||
///
|
||||
/// Permanently deletes a branch from the repository. The default branch cannot be deleted.
|
||||
/// Requires Write role or higher in the repository.
|
||||
///
|
||||
/// Effects:
|
||||
/// - Branch is permanently removed from the repository
|
||||
/// - All commits exclusive to this branch remain accessible via their SHA
|
||||
/// - Open pull requests targeting this branch will be closed
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches/{branch_id}",
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches/{branch_name}",
|
||||
tag = "Repos",
|
||||
operation_id = "repoDeleteBranch",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Branch deleted successfully.", body = ApiResponse<String>),
|
||||
(status = 400, description = "Cannot delete the default branch", 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 branch not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error or Git operation failed", body = ApiErrorResponse),
|
||||
(status = 200, description = "Branch deleted", body = ApiResponse<String>),
|
||||
(status = 400, description = "Cannot delete default branch", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Branch not found", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn delete_branch(
|
||||
service: web::Data<AppService>,
|
||||
@@ -53,13 +35,12 @@ pub async fn delete_branch(
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.repo
|
||||
.repo_delete_branch(
|
||||
.git_delete_branch(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.branch_id,
|
||||
&path.branch_name,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Branch deleted successfully".to_string())))
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Branch deleted".to_string())))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/fork",
|
||||
tag = "Repos",
|
||||
operation_id = "repoDeleteFork",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Fork deleted successfully", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Fork not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn delete_fork(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.repo
|
||||
.repo_delete_fork(&session, &path.workspace_name, &path.repo_name)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Fork deleted successfully".to_string())))
|
||||
}
|
||||
+13
-27
@@ -9,41 +9,23 @@ 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,
|
||||
/// Tag ID (UUID)
|
||||
pub tag_id: uuid::Uuid,
|
||||
pub tag_name: String,
|
||||
}
|
||||
|
||||
/// Delete a tag
|
||||
///
|
||||
/// Permanently deletes a tag from the repository. The tagged commit remains accessible via its SHA.
|
||||
/// Requires Write role or higher in the repository.
|
||||
///
|
||||
/// Effects:
|
||||
/// - Tag is permanently removed from the repository
|
||||
/// - The tagged commit remains in the repository history
|
||||
/// - Releases associated with this tag are not automatically deleted
|
||||
///
|
||||
/// Returns success message on completion.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/tags/{tag_id}",
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/tags/{tag_name}",
|
||||
tag = "Repos",
|
||||
operation_id = "repoDeleteTag",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Tag deleted successfully.", body = ApiResponse<String>),
|
||||
(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 tag not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error or Git operation failed", body = ApiErrorResponse),
|
||||
(status = 200, description = "Tag deleted", body = ApiResponse<String>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Tag not found", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("session_cookie" = [])
|
||||
)
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn delete_tag(
|
||||
service: web::Data<AppService>,
|
||||
@@ -52,8 +34,12 @@ pub async fn delete_tag(
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.repo
|
||||
.repo_delete_tag(&session, &path.workspace_name, &path.repo_name, path.tag_id)
|
||||
.git_delete_tag(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&path.tag_name,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Tag deleted successfully".to_string())))
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Tag deleted".to_string())))
|
||||
}
|
||||
|
||||
+14
-3
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::Repo;
|
||||
use crate::models::base_info::{resolve_users, resolve_workspaces};
|
||||
use crate::models::repos::RepoDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
@@ -42,7 +43,7 @@ use crate::service::repo::fork::ForkRepoParams;
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Repository forked successfully. Returns the newly created fork with full metadata.", body = ApiResponse<Repo>),
|
||||
(status = 201, description = "Repository forked successfully. Returns the newly created fork with full metadata.", body = ApiResponse<RepoDetail>),
|
||||
(status = 400, description = "Invalid parameters: target name conflicts or invalid characters", body = ApiErrorResponse),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to fork or create in target workspace", body = ApiErrorResponse),
|
||||
@@ -70,5 +71,15 @@ pub async fn fork_repo(
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(repo)))
|
||||
let db = &service.ctx.db;
|
||||
let users = resolve_users(db, &[repo.owner_id]).await?;
|
||||
let workspaces = resolve_workspaces(db, &[repo.workspace_id]).await?;
|
||||
let owner = users.get(&repo.owner_id).cloned().unwrap_or_default();
|
||||
let workspace = workspaces
|
||||
.get(&repo.workspace_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let detail = repo.into_detail(owner, workspace);
|
||||
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(detail)))
|
||||
}
|
||||
|
||||
+14
-3
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::Repo;
|
||||
use crate::models::base_info::{resolve_users, resolve_workspaces};
|
||||
use crate::models::repos::RepoDetail;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
@@ -32,7 +33,7 @@ pub struct PathParams {
|
||||
operation_id = "repoGet",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Repository retrieved successfully. Returns complete repository metadata including visibility, default branch, creation date, and statistics.", body = ApiResponse<Repo>),
|
||||
(status = 200, description = "Repository retrieved successfully. Returns complete repository metadata including visibility, default branch, creation date, and statistics.", body = ApiResponse<RepoDetail>),
|
||||
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||
(status = 403, description = "Insufficient permissions to access this repository", body = ApiErrorResponse),
|
||||
(status = 404, description = "Repository not found or access denied", body = ApiErrorResponse),
|
||||
@@ -52,5 +53,15 @@ pub async fn get(
|
||||
.repo_get(&session, &path.workspace_name, &path.repo_name)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(repo)))
|
||||
let db = &service.ctx.db;
|
||||
let users = resolve_users(db, &[repo.owner_id]).await?;
|
||||
let workspaces = resolve_workspaces(db, &[repo.workspace_id]).await?;
|
||||
let owner = users.get(&repo.owner_id).cloned().unwrap_or_default();
|
||||
let workspace = workspaces
|
||||
.get(&repo.workspace_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let detail = repo.into_detail(owner, workspace);
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(detail)))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub branch_name: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches/{branch_name}",
|
||||
tag = "Repos",
|
||||
operation_id = "repoGetBranch",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Branch retrieved", body = ApiResponse<crate::pb::repo::Branch>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Branch not found", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn get_branch(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_get_branch(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&path.branch_name,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::RepoCommitStatus;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub push_commit_id: uuid::Uuid,
|
||||
pub status_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/commits/{push_commit_id}/statuses/{status_id}",
|
||||
tag = "Repos",
|
||||
operation_id = "repoGetCommitStatus",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Commit status retrieved successfully", body = ApiResponse<RepoCommitStatus>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Commit status not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn get_commit_status(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let statuses = service
|
||||
.repo
|
||||
.repo_commit_statuses(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
path.push_commit_id,
|
||||
1000,
|
||||
0,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let status = statuses
|
||||
.into_iter()
|
||||
.find(|s| s.id == path.status_id)
|
||||
.ok_or(AppError::NotFound("commit status not found".into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(status)))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::RepoDeployKey;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub key_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/deploy-keys/{key_id}",
|
||||
tag = "Repos",
|
||||
operation_id = "repoGetDeployKey",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Deploy key retrieved successfully", body = ApiResponse<RepoDeployKey>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Deploy key not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn get_deploy_key(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let keys = service
|
||||
.repo
|
||||
.repo_deploy_keys(&session, &path.workspace_name, &path.repo_name, 1000, 0)
|
||||
.await?;
|
||||
|
||||
let key = keys
|
||||
.into_iter()
|
||||
.find(|k| k.id == path.key_id)
|
||||
.ok_or(AppError::NotFound("deploy key not found".into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(key)))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::RepoInvitation;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub invitation_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/invitations/{invitation_id}",
|
||||
tag = "Repos",
|
||||
operation_id = "repoGetInvitation",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Invitation retrieved successfully", body = ApiResponse<RepoInvitation>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Invitation not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn get_invitation(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let invitations = service
|
||||
.repo
|
||||
.repo_invitations(&session, &path.workspace_name, &path.repo_name, 1000, 0)
|
||||
.await?;
|
||||
|
||||
let invitation = invitations
|
||||
.into_iter()
|
||||
.find(|i| i.id == path.invitation_id)
|
||||
.ok_or(AppError::NotFound("invitation not found".into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(invitation)))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::RepoMember;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub member_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/members/{member_id}",
|
||||
tag = "Repos",
|
||||
operation_id = "repoGetMember",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Member retrieved successfully", body = ApiResponse<RepoMember>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Member not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn get_member(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let members = service
|
||||
.repo
|
||||
.repo_members(&session, &path.workspace_name, &path.repo_name, 1000, 0)
|
||||
.await?;
|
||||
|
||||
let member = members
|
||||
.into_iter()
|
||||
.find(|m| m.id == path.member_id)
|
||||
.ok_or(AppError::NotFound("member not found".into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(member)))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::RepoRelease;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub release_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/releases/{release_id}",
|
||||
tag = "Repos",
|
||||
operation_id = "repoGetRelease",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Release retrieved successfully", body = ApiResponse<RepoRelease>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Release not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn get_release(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let releases = service
|
||||
.repo
|
||||
.repo_releases(&session, &path.workspace_name, &path.repo_name, 1000, 0)
|
||||
.await?;
|
||||
|
||||
let release = releases
|
||||
.into_iter()
|
||||
.find(|r| r.id == path.release_id)
|
||||
.ok_or(AppError::NotFound("release not found".into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(release)))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub tag_name: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/tags/{tag_name}",
|
||||
tag = "Repos",
|
||||
operation_id = "repoGetTag",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Tag retrieved", body = ApiResponse<crate::pb::repo::Tag>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Tag not found", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn get_tag(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_get_tag(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&path.tag_name,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::RepoWebhook;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub webhook_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/webhooks/{webhook_id}",
|
||||
tag = "Repos",
|
||||
operation_id = "repoGetWebhook",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Webhook retrieved successfully", body = ApiResponse<RepoWebhook>),
|
||||
(status = 401, description = "Authentication required", body = ApiErrorResponse),
|
||||
(status = 404, description = "Webhook not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn get_webhook(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let webhooks = service
|
||||
.repo
|
||||
.repo_webhooks(&session, &path.workspace_name, &path.repo_name, 1000, 0)
|
||||
.await?;
|
||||
|
||||
let webhook = webhooks
|
||||
.into_iter()
|
||||
.find(|w| w.id == path.webhook_id)
|
||||
.ok_or(AppError::NotFound("webhook not found".into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(webhook)))
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub revision: String,
|
||||
pub path: String,
|
||||
pub page_size: Option<u32>,
|
||||
}
|
||||
|
||||
/// Blame a file
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/blame",
|
||||
tag = "Git",
|
||||
operation_id = "gitBlame",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Blame retrieved successfully", body = ApiResponse<crate::pb::repo::BlameResponse>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_blame(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_blame(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&query.revision,
|
||||
&query.path,
|
||||
query.page_size.unwrap_or(30),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub revision: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Get blob content
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/blobs",
|
||||
tag = "Git",
|
||||
operation_id = "gitGetBlob",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Blob retrieved successfully", body = ApiResponse<crate::pb::repo::Blob>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 404, description = "Blob not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_blob(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_get_blob(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&query.revision,
|
||||
&query.path,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::repo::git::merge::CherryPickParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
/// Cherry-pick a commit
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/cherry-pick",
|
||||
tag = "Git",
|
||||
operation_id = "gitCherryPick",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CherryPickParams,
|
||||
description = "Cherry-pick parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Cherry-pick completed successfully", body = ApiResponse<crate::pb::repo::CreateCommitResponse>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 409, description = "Cherry-pick conflict", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_cherry_pick(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CherryPickParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_cherry_pick(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
use crate::api::response::ApiResponse;
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct RevisionPathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub revision: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct FindCommitQuery {
|
||||
pub revision: String,
|
||||
}
|
||||
#[utoipa::path(get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/find-commit", tag = "Git", operation_id = "gitFindCommit", params(PathParams, FindCommitQuery), responses((status=200,body=ApiResponse<crate::pb::repo::Commit>)), security(("session_cookie"=[])))]
|
||||
pub async fn git_find_commit(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
q: web::Query<FindCommitQuery>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let r = service
|
||||
.repo
|
||||
.git_find_commit(&session, &path.workspace_name, &path.repo_name, &q.revision)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(r)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ListCommitsByOidBody {
|
||||
pub oids: Vec<String>,
|
||||
}
|
||||
#[utoipa::path(post, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits/by-oid", tag = "Git", operation_id = "gitCommitsByOid", params(PathParams), request_body(content=ListCommitsByOidBody), responses((status=200,body=ApiResponse<crate::pb::repo::ListCommitsByOidResponse>)), security(("session_cookie"=[])))]
|
||||
pub async fn git_commits_by_oid(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
body: web::Json<ListCommitsByOidBody>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let oids: Vec<Vec<u8>> = body
|
||||
.oids
|
||||
.iter()
|
||||
.map(|s| hex::decode(s).unwrap_or_default())
|
||||
.collect();
|
||||
let r = service
|
||||
.repo
|
||||
.git_list_commits_by_oid(&session, &path.workspace_name, &path.repo_name, oids)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(r)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct AncestorQuery {
|
||||
pub ancestor: String,
|
||||
pub descendant: String,
|
||||
}
|
||||
#[utoipa::path(get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commit-is-ancestor", tag = "Git", operation_id = "gitCommitIsAncestor", params(PathParams, AncestorQuery), responses((status=200,body=ApiResponse<bool>)), security(("session_cookie"=[])))]
|
||||
pub async fn git_commit_is_ancestor(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
q: web::Query<AncestorQuery>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let r = service
|
||||
.repo
|
||||
.git_commit_is_ancestor(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&q.ancestor,
|
||||
&q.descendant,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(r)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct LastCommitQuery {
|
||||
pub path: String,
|
||||
pub revision: Option<String>,
|
||||
}
|
||||
#[utoipa::path(get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/last-commit", tag = "Git", operation_id = "gitLastCommitForPath", params(PathParams, LastCommitQuery), responses((status=200,body=ApiResponse<crate::pb::repo::LastCommitForPathResponse>)), security(("session_cookie"=[])))]
|
||||
pub async fn git_last_commit(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
q: web::Query<LastCommitQuery>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let r = service
|
||||
.repo
|
||||
.git_last_commit_for_path(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&q.path,
|
||||
q.revision.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(r)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct CommitsByMsgQuery {
|
||||
pub q: String,
|
||||
pub revision: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
#[utoipa::path(get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits/search", tag = "Git", operation_id = "gitCommitsByMessage", params(PathParams, CommitsByMsgQuery), responses((status=200,body=ApiResponse<crate::pb::repo::CommitsByMessageResponse>)), security(("session_cookie"=[])))]
|
||||
pub async fn git_commits_by_message(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
q: web::Query<CommitsByMsgQuery>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let r = service
|
||||
.repo
|
||||
.git_commits_by_message(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&q.q,
|
||||
q.revision.as_deref(),
|
||||
q.limit.unwrap_or(20),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(r)))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub revision: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits/{revision}/stats",
|
||||
tag = "Git",
|
||||
operation_id = "gitCommitStats",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Commit stats", body = ApiResponse<crate::pb::repo::CommitStats>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 404, description = "Not found", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_commit_stats(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_commit_stats(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&path.revision,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::repo::git::merge::CompareParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub base: String,
|
||||
pub head: String,
|
||||
pub page_size: Option<u32>,
|
||||
}
|
||||
|
||||
/// Compare two commits
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/compare",
|
||||
tag = "Git",
|
||||
operation_id = "gitCompare",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Comparison completed successfully", body = ApiResponse<crate::pb::repo::CompareCommitsResponse>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_compare(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_compare_commits(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
CompareParams {
|
||||
base: query.base.clone(),
|
||||
head: query.head.clone(),
|
||||
page_size: query.page_size,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub target: String,
|
||||
pub source: String,
|
||||
pub page_size: Option<u32>,
|
||||
}
|
||||
|
||||
/// List merge conflicts
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/conflicts",
|
||||
tag = "Git",
|
||||
operation_id = "gitListConflicts",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Conflicts listed successfully", body = ApiResponse<crate::pb::repo::ListMergeConflictsResponse>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_conflicts(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_list_conflicts(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&query.target,
|
||||
&query.source,
|
||||
query.page_size.unwrap_or(30),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub revision: Option<String>,
|
||||
pub path: Option<String>,
|
||||
pub since: Option<String>,
|
||||
pub until: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits/count",
|
||||
tag = "Git",
|
||||
operation_id = "gitCountCommits",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Commit count", body = ApiResponse<crate::pb::repo::CountCommitsResponse>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 404, description = "Not found", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_count_commits(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_count_commits(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
query.revision.as_deref(),
|
||||
query.path.as_deref(),
|
||||
query.since.as_deref(),
|
||||
query.until.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub left: String,
|
||||
pub right: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/compare/diverging",
|
||||
tag = "Git",
|
||||
operation_id = "gitCountDivergingCommits",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Diverging commit counts", body = ApiResponse<crate::pb::repo::CountDivergingCommitsResponse>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 404, description = "Not found", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_count_diverging(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_count_diverging_commits(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&query.left,
|
||||
&query.right,
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams, ToSchema)]
|
||||
pub struct CreateBranchBody {
|
||||
pub branch_name: String,
|
||||
pub start_point: String,
|
||||
}
|
||||
|
||||
/// Create a branch
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/branches",
|
||||
tag = "Git",
|
||||
operation_id = "gitCreateBranch",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CreateBranchBody,
|
||||
description = "Branch creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Branch created successfully", body = ApiResponse<crate::pb::repo::Branch>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_create_branch(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
body: web::Json<CreateBranchBody>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_create_branch(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&body.branch_name,
|
||||
&body.start_point,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::service::repo::git::merge::CreateCommitParams;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
/// Create a commit
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits",
|
||||
tag = "Git",
|
||||
operation_id = "gitCreateCommit",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CreateCommitParams,
|
||||
description = "Commit creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Commit created successfully", body = ApiResponse<crate::pb::repo::CreateCommitResponse>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_create_commit(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
params: web::Json<CreateCommitParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_create_commit(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
params.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams, ToSchema)]
|
||||
pub struct CreateTagBody {
|
||||
pub tag_name: String,
|
||||
pub target: String,
|
||||
pub message: Option<String>,
|
||||
pub annotated: Option<bool>,
|
||||
}
|
||||
|
||||
/// Create a tag
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/tags",
|
||||
tag = "Git",
|
||||
operation_id = "gitCreateTag",
|
||||
params(PathParams),
|
||||
request_body(
|
||||
content = CreateTagBody,
|
||||
description = "Tag creation parameters",
|
||||
content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Tag created successfully", body = ApiResponse<crate::pb::repo::Tag>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_create_tag(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
body: web::Json<CreateTagBody>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_create_tag(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&body.tag_name,
|
||||
&body.target,
|
||||
body.message.clone(),
|
||||
body.annotated.unwrap_or(false),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub branch_name: String,
|
||||
}
|
||||
|
||||
/// Delete a branch
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/branches/{branch_name}",
|
||||
tag = "Git",
|
||||
operation_id = "gitDeleteBranch",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Branch deleted successfully", body = ApiResponse<String>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_delete_branch(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.repo
|
||||
.git_delete_branch(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&path.branch_name,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Branch deleted successfully".to_string())))
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub tag_name: String,
|
||||
}
|
||||
|
||||
/// Delete a tag
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/tags/{tag_name}",
|
||||
tag = "Git",
|
||||
operation_id = "gitDeleteTag",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Tag deleted successfully", body = ApiResponse<String>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_delete_tag(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
service
|
||||
.repo
|
||||
.git_delete_tag(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&path.tag_name,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new("Tag deleted successfully".to_string())))
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub base: String,
|
||||
pub head: String,
|
||||
pub page_size: Option<u32>,
|
||||
}
|
||||
|
||||
/// Get diff between two revisions
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/diff",
|
||||
tag = "Git",
|
||||
operation_id = "gitDiff",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Diff retrieved successfully", body = ApiResponse<crate::pb::repo::GetDiffResponse>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_diff(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_diff(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&query.base,
|
||||
&query.head,
|
||||
query.page_size.unwrap_or(30),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct QueryParams {
|
||||
pub base: String,
|
||||
pub head: String,
|
||||
}
|
||||
|
||||
/// Get diff statistics between two revisions
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/diff-stats",
|
||||
tag = "Git",
|
||||
operation_id = "gitDiffStats",
|
||||
params(PathParams, QueryParams),
|
||||
responses(
|
||||
(status = 200, description = "Diff stats retrieved successfully", body = ApiResponse<crate::pb::repo::DiffStats>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_diff_stats(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
query: web::Query<QueryParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_diff_stats(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&query.base,
|
||||
&query.head,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
/// Check if repository exists
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/exists",
|
||||
tag = "Git",
|
||||
operation_id = "gitRepoExists",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Repository existence check completed", body = ApiResponse<bool>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_exists(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_repo_exists(&session, &path.workspace_name, &path.repo_name)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
}
|
||||
|
||||
/// Run garbage collection
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/garbage-collect",
|
||||
tag = "Git",
|
||||
operation_id = "gitGarbageCollect",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Garbage collection completed", body = ApiResponse<crate::pb::repo::RepositoryMaintenanceResponse>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_gc(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_garbage_collect(&session, &path.workspace_name, &path.repo_name)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub branch_name: String,
|
||||
}
|
||||
|
||||
/// Get a branch
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/branches/{branch_name}",
|
||||
tag = "Git",
|
||||
operation_id = "gitGetBranch",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Branch retrieved successfully", body = ApiResponse<crate::pb::repo::Branch>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 404, description = "Branch not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_get_branch(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_get_branch(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&path.branch_name,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::api::response::{ApiErrorResponse, ApiResponse};
|
||||
use crate::error::AppError;
|
||||
use crate::service::AppService;
|
||||
use crate::session::Session;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PathParams {
|
||||
pub workspace_name: String,
|
||||
pub repo_name: String,
|
||||
pub revision: String,
|
||||
}
|
||||
|
||||
/// Get a single commit
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits/{revision}",
|
||||
tag = "Git",
|
||||
operation_id = "gitGetCommit",
|
||||
params(PathParams),
|
||||
responses(
|
||||
(status = 200, description = "Commit retrieved successfully", body = ApiResponse<crate::pb::repo::Commit>),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 404, description = "Commit not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(("session_cookie" = []))
|
||||
)]
|
||||
pub async fn git_get_commit(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<PathParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let result = service
|
||||
.repo
|
||||
.git_get_commit(
|
||||
&session,
|
||||
&path.workspace_name,
|
||||
&path.repo_name,
|
||||
&path.revision,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user