All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m51s
734 lines
28 KiB
Markdown
734 lines
28 KiB
Markdown
# Rhythm Backend Blueprint (Rust/Axum)
|
|
|
|
## IDEAS
|
|
- instead of elastic search use ts vector and ts query to allow easy issue searching for smooth experience
|
|
- YouTrack competitor: issue tracker with organizations, projects, issues, and project-level RBAC
|
|
|
|
---
|
|
|
|
## Chosen Stack
|
|
|
|
- **Language:** Rust (edition 2024)
|
|
- **Framework:** Axum
|
|
- **Runtime:** Tokio
|
|
- **Migrations:** sqlx built-in migrations
|
|
- **DB Access:** sqlx (compile-time checked queries, hand-written repositories)
|
|
- **PostgreSQL Driver/Pool:** sqlx::PgPool
|
|
- **Logging:** tracing + tracing-subscriber + tracing-tree (structured, hierarchical)
|
|
- **Auth:** jsonwebtoken (HS256) + argon2 (password hashing)
|
|
- **API Docs:** utoipa (code-first OpenAPI generation)
|
|
- **Health Probes:** liveness/readiness endpoints
|
|
- **Deployment:** Docker Compose on self-hosted hardware
|
|
|
|
---
|
|
|
|
## Decisions Made
|
|
|
|
1. **JWT signing:** HS256 (shared secret from env, can migrate to RS256 later)
|
|
2. **Token model:** Access + Refresh (short-lived access ~15min, long-lived refresh ~7d, refresh tokens stored hashed in DB)
|
|
3. **Roles:** Project-level roles (`admin`, `developer`, `reporter`) + Org-level roles (`owner`, `admin`, `member`)
|
|
4. **OpenAPI workflow:** Code-first with utoipa (auto-generate spec from Rust handlers/models)
|
|
5. **DB access:** sqlx with hand-written repositories (compile-time checked, no ORM)
|
|
6. **First entities:** Users → Organizations → Projects → Issues
|
|
7. **Sprint stages:** Custom per project (project admin defines board columns like Todo → In Review → Done)
|
|
8. **Assignees:** Multiple assignees per issue (join table)
|
|
9. **Issue content features:** Tags, comments, issue relations, time tracking
|
|
10. **Plugin system:** Lua scripting with cron + event-driven triggers (Phase 7)
|
|
|
|
---
|
|
|
|
## Architecture Direction
|
|
|
|
### Project Structure
|
|
|
|
```
|
|
src/
|
|
├── main.rs # Entrypoint, server, graceful shutdown
|
|
├── config.rs # Config (exists, extend)
|
|
├── logging.rs # Logging (exists)
|
|
├── errors.rs # Unified error types → Axum responses
|
|
├── state.rs # AppState (PgPool, config, etc.)
|
|
├── routes.rs # Router composition (/api/v1/...)
|
|
├── auth/
|
|
│ ├── jwt.rs # HS256 token creation/validation
|
|
│ ├── hash.rs # argon2 password hashing
|
|
│ ├── handlers.rs # register, login, refresh
|
|
│ ├── models.rs # auth DTOs
|
|
│ └── service.rs # auth business logic
|
|
├── middleware/
|
|
│ ├── auth.rs # JWT extraction layer
|
|
│ └── rbac.rs # Project-level role guard
|
|
├── models/
|
|
│ ├── user.rs
|
|
│ ├── org.rs
|
|
│ ├── project.rs
|
|
│ ├── issue.rs
|
|
│ ├── comment.rs
|
|
│ ├── tag.rs
|
|
│ ├── sprint.rs
|
|
│ ├── stage.rs
|
|
│ ├── time_entry.rs
|
|
│ └── role.rs # OrgRole, ProjectRole enums
|
|
├── handlers/
|
|
│ ├── health.rs
|
|
│ ├── orgs.rs
|
|
│ ├── projects.rs
|
|
│ ├── issues.rs
|
|
│ ├── comments.rs
|
|
│ ├── tags.rs
|
|
│ ├── sprints.rs
|
|
│ ├── stages.rs
|
|
│ └── time_entries.rs
|
|
├── services/
|
|
│ ├── org.rs
|
|
│ ├── project.rs
|
|
│ ├── issue.rs
|
|
│ ├── comment.rs
|
|
│ ├── tag.rs
|
|
│ ├── sprint.rs
|
|
│ ├── stage.rs
|
|
│ └── time_entry.rs
|
|
└── db/
|
|
├── mod.rs # Pool setup, migration runner
|
|
└── repos/
|
|
├── users.rs
|
|
├── orgs.rs
|
|
├── projects.rs
|
|
├── issues.rs
|
|
├── comments.rs
|
|
├── tags.rs
|
|
├── sprints.rs
|
|
├── stages.rs
|
|
├── time_entries.rs
|
|
├── memberships.rs
|
|
└── refresh_tokens.rs
|
|
migrations/
|
|
├── 001_create_users.sql
|
|
├── 002_create_organizations.sql
|
|
├── 003_create_memberships.sql
|
|
├── 004_create_projects.sql
|
|
├── 005_create_stages.sql
|
|
├── 006_create_issues.sql
|
|
├── 007_create_issue_assignees.sql
|
|
├── 008_create_tags.sql
|
|
├── 009_create_comments.sql
|
|
├── 010_create_issue_relations.sql
|
|
├── 011_create_time_entries.sql
|
|
├── 012_create_sprints.sql
|
|
├── 013_create_refresh_tokens.sql
|
|
└── 014_add_tsvector_search.sql
|
|
```
|
|
|
|
### Layering Principles
|
|
|
|
- Handlers stay thin — they extract, validate, call service, respond
|
|
- Service layer owns business logic + DB transactions
|
|
- Repositories own SQL queries (sqlx compile-time checked)
|
|
- No transaction logic in handlers
|
|
- AppState shared via Axum's State extractor
|
|
|
|
### Transaction Strategy
|
|
|
|
- Service layer owns DB transactions
|
|
- Repositories accept either `&PgPool` or `&mut Transaction<Postgres>`
|
|
- sqlx compile-time query checking ensures SQL correctness at build time
|
|
|
|
---
|
|
|
|
## API and Runtime
|
|
|
|
### API Shape
|
|
|
|
- REST JSON API
|
|
- Possible future WebSocket support for interactive features
|
|
- Versioning: `/api/v1`
|
|
|
|
### Health Endpoints
|
|
|
|
- `GET /api/v1/health/live` - process is alive
|
|
- `GET /api/v1/health/ready` - DB ping succeeds (and optionally migration version check)
|
|
|
|
### Auth Endpoints
|
|
|
|
- `POST /api/v1/auth/register` - create account (email + password)
|
|
- `POST /api/v1/auth/login` - get access + refresh tokens
|
|
- `POST /api/v1/auth/refresh` - rotate refresh token
|
|
|
|
### Organization Endpoints
|
|
|
|
- `POST /api/v1/orgs` - create org (creator becomes owner)
|
|
- `GET /api/v1/orgs` - list user's orgs
|
|
- `GET /api/v1/orgs/{org_slug}` - get org
|
|
- `PATCH /api/v1/orgs/{org_slug}` - update org (owner/admin)
|
|
- `DELETE /api/v1/orgs/{org_slug}` - delete org (owner)
|
|
|
|
### Project Endpoints
|
|
|
|
- `POST /api/v1/orgs/{org_slug}/projects` - create project (org owner/admin)
|
|
- `GET /api/v1/orgs/{org_slug}/projects` - list projects
|
|
- `GET /api/v1/orgs/{org_slug}/projects/{project_slug}` - get project
|
|
- `PATCH /api/v1/orgs/{org_slug}/projects/{project_slug}` - update project (project admin)
|
|
- `DELETE /api/v1/orgs/{org_slug}/projects/{project_slug}` - delete project (project admin)
|
|
|
|
### Issue Endpoints
|
|
|
|
- `POST /api/v1/orgs/{org_slug}/projects/{project_slug}/issues` - create issue (reporter+)
|
|
- `GET /api/v1/orgs/{org_slug}/projects/{project_slug}/issues` - list issues (with `?q=` search, `?tag=`, `?assignee=`, `?status=`)
|
|
- `GET /api/v1/orgs/{org_slug}/projects/{project_slug}/issues/{number}` - get issue
|
|
- `PATCH /api/v1/orgs/{org_slug}/projects/{project_slug}/issues/{number}` - update issue (developer+)
|
|
- `DELETE /api/v1/orgs/{org_slug}/projects/{project_slug}/issues/{number}` - delete issue (admin)
|
|
|
|
### Issue Sub-resource Endpoints
|
|
|
|
**Tags:**
|
|
- `GET /api/v1/orgs/{org}/projects/{proj}/tags` - list project tags
|
|
- `POST /api/v1/orgs/{org}/projects/{proj}/tags` - create tag (developer+)
|
|
- `DELETE /api/v1/orgs/{org}/projects/{proj}/tags/{tag_id}` - delete tag (admin)
|
|
- `PUT /api/v1/orgs/{org}/projects/{proj}/issues/{num}/tags` - set issue tags (developer+)
|
|
|
|
**Comments:**
|
|
- `GET /api/v1/orgs/{org}/projects/{proj}/issues/{num}/comments` - list comments
|
|
- `POST /api/v1/orgs/{org}/projects/{proj}/issues/{num}/comments` - add comment (reporter+)
|
|
- `PATCH /api/v1/orgs/{org}/projects/{proj}/issues/{num}/comments/{id}` - edit comment (author only)
|
|
- `DELETE /api/v1/orgs/{org}/projects/{proj}/issues/{num}/comments/{id}` - delete comment (author or admin)
|
|
|
|
**Assignees:**
|
|
- `GET /api/v1/orgs/{org}/projects/{proj}/issues/{num}/assignees` - list assignees
|
|
- `PUT /api/v1/orgs/{org}/projects/{proj}/issues/{num}/assignees` - set assignees (developer+)
|
|
|
|
**Relations:**
|
|
- `GET /api/v1/orgs/{org}/projects/{proj}/issues/{num}/relations` - list relations
|
|
- `POST /api/v1/orgs/{org}/projects/{proj}/issues/{num}/relations` - add relation (developer+)
|
|
- `DELETE /api/v1/orgs/{org}/projects/{proj}/issues/{num}/relations/{id}` - remove relation (developer+)
|
|
|
|
**Time Tracking:**
|
|
- `GET /api/v1/orgs/{org}/projects/{proj}/issues/{num}/time-entries` - list time entries
|
|
- `POST /api/v1/orgs/{org}/projects/{proj}/issues/{num}/time-entries` - log time (developer+)
|
|
- `DELETE /api/v1/orgs/{org}/projects/{proj}/issues/{num}/time-entries/{id}` - delete entry (author or admin)
|
|
|
|
### Sprint Endpoints
|
|
|
|
- `POST /api/v1/orgs/{org}/projects/{proj}/sprints` - create sprint (developer+)
|
|
- `GET /api/v1/orgs/{org}/projects/{proj}/sprints` - list sprints
|
|
- `GET /api/v1/orgs/{org}/projects/{proj}/sprints/{id}` - get sprint
|
|
- `PATCH /api/v1/orgs/{org}/projects/{proj}/sprints/{id}` - update sprint (developer+)
|
|
- `PATCH /api/v1/orgs/{org}/projects/{proj}/sprints/{id}/start` - start sprint (admin)
|
|
- `PATCH /api/v1/orgs/{org}/projects/{proj}/sprints/{id}/complete` - complete sprint (admin)
|
|
- `GET /api/v1/orgs/{org}/projects/{proj}/sprints/{id}/board` - sprint board (issues grouped by stage)
|
|
|
|
### Stage Endpoints (project board columns)
|
|
|
|
- `POST /api/v1/orgs/{org}/projects/{proj}/stages` - create stage (admin)
|
|
- `PATCH /api/v1/orgs/{org}/projects/{proj}/stages` - reorder stages (admin)
|
|
- `DELETE /api/v1/orgs/{org}/projects/{proj}/stages/{id}` - delete stage (admin)
|
|
|
|
### Logging
|
|
|
|
- tracing with hierarchical layer (dev: pretty console, prod: JSON + file with rotation)
|
|
- Correlation/request ID in middleware
|
|
- Structured error logging from middleware and service boundaries
|
|
|
|
---
|
|
|
|
## Database Schema
|
|
|
|
```sql
|
|
-- Enums
|
|
CREATE TYPE org_role AS ENUM ('owner', 'admin', 'member');
|
|
CREATE TYPE project_role AS ENUM ('admin', 'developer', 'reporter');
|
|
CREATE TYPE issue_status AS ENUM ('open', 'in_progress', 'resolved', 'closed');
|
|
CREATE TYPE issue_priority AS ENUM ('critical', 'major', 'normal', 'minor');
|
|
CREATE TYPE issue_type AS ENUM ('bug', 'feature', 'task', 'improvement');
|
|
CREATE TYPE issue_relation_type AS ENUM ('blocks', 'is_blocked_by', 'duplicates', 'is_duplicated_by', 'relates_to', 'clones', 'is_cloned_by');
|
|
CREATE TYPE sprint_status AS ENUM ('planned', 'active', 'completed');
|
|
CREATE TYPE trigger_type AS ENUM ('event', 'schedule', 'manual');
|
|
|
|
-- Users
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
email TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
display_name TEXT,
|
|
avatar_url TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Organizations
|
|
CREATE TABLE organizations (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
description TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Org memberships (owner/admin/member)
|
|
CREATE TABLE org_memberships (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
role org_role NOT NULL DEFAULT 'member',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE(user_id, org_id)
|
|
);
|
|
|
|
-- Projects
|
|
CREATE TABLE projects (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
slug TEXT NOT NULL,
|
|
description TEXT,
|
|
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
created_by UUID NOT NULL REFERENCES users(id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE(org_id, slug)
|
|
);
|
|
|
|
-- Project memberships (admin/developer/reporter)
|
|
CREATE TABLE project_memberships (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
role project_role NOT NULL DEFAULT 'reporter',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE(user_id, project_id)
|
|
);
|
|
|
|
-- Stages (custom board columns per project)
|
|
CREATE TABLE stages (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
position INT NOT NULL,
|
|
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
UNIQUE(project_id, position)
|
|
);
|
|
|
|
-- Issues
|
|
CREATE TABLE issues (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
number BIGINT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
description TEXT,
|
|
status issue_status NOT NULL DEFAULT 'open',
|
|
priority issue_priority NOT NULL DEFAULT 'normal',
|
|
issue_type issue_type NOT NULL DEFAULT 'task',
|
|
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
reporter_id UUID NOT NULL REFERENCES users(id),
|
|
stage_id UUID REFERENCES stages(id) ON DELETE SET NULL,
|
|
sprint_id UUID REFERENCES sprints(id) ON DELETE SET NULL,
|
|
search_vector TSVECTOR GENERATED ALWAYS AS (
|
|
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
|
|
setweight(to_tsvector('english', coalesce(description, '')), 'B')
|
|
) STORED,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE(project_id, number)
|
|
);
|
|
|
|
-- Multiple assignees per issue
|
|
CREATE TABLE issue_assignees (
|
|
issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (issue_id, user_id)
|
|
);
|
|
|
|
-- Tags (per project)
|
|
CREATE TABLE tags (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
color TEXT,
|
|
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
UNIQUE(project_id, name)
|
|
);
|
|
|
|
CREATE TABLE issue_tags (
|
|
issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
|
|
tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (issue_id, tag_id)
|
|
);
|
|
|
|
-- Comments
|
|
CREATE TABLE comments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
|
|
author_id UUID NOT NULL REFERENCES users(id),
|
|
body TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Related issues
|
|
CREATE TABLE issue_relations (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
from_issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
|
|
to_issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
|
|
relation_type issue_relation_type NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE(from_issue_id, to_issue_id, relation_type)
|
|
);
|
|
|
|
-- Time tracking
|
|
CREATE TABLE time_entries (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id),
|
|
seconds_spent INTEGER NOT NULL CHECK (seconds_spent > 0),
|
|
description TEXT,
|
|
logged_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Sprints
|
|
CREATE TABLE sprints (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
starts_at TIMESTAMPTZ NOT NULL,
|
|
ends_at TIMESTAMPTZ NOT NULL,
|
|
status sprint_status NOT NULL DEFAULT 'planned',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Refresh tokens
|
|
CREATE TABLE refresh_tokens (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Scripts (for Lua plugin system)
|
|
CREATE TABLE scripts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
source TEXT NOT NULL,
|
|
trigger_type trigger_type NOT NULL,
|
|
trigger_config JSONB,
|
|
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
|
|
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
created_by UUID NOT NULL REFERENCES users(id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE script_revisions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
script_id UUID NOT NULL REFERENCES scripts(id) ON DELETE CASCADE,
|
|
source TEXT NOT NULL,
|
|
created_by UUID NOT NULL REFERENCES users(id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE script_execution_logs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
script_id UUID NOT NULL REFERENCES scripts(id) ON DELETE CASCADE,
|
|
status TEXT NOT NULL,
|
|
output TEXT,
|
|
error TEXT,
|
|
duration_ms INTEGER,
|
|
triggered_by TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Indexes
|
|
CREATE INDEX idx_issues_search ON issues USING GIN(search_vector);
|
|
CREATE INDEX idx_issues_project_sprint ON issues(project_id, sprint_id);
|
|
CREATE INDEX idx_issues_project_stage ON issues(project_id, stage_id);
|
|
CREATE INDEX idx_time_entries_issue ON time_entries(issue_id);
|
|
CREATE INDEX idx_comments_issue ON comments(issue_id);
|
|
```
|
|
|
|
**Note:** The `issues` table references `sprints(id)` via `sprint_id`. In practice, the `sprints` table migration must run before `issues` (or `sprint_id` is added in a later migration). The migration files are ordered to handle this.
|
|
|
|
---
|
|
|
|
## RBAC Model
|
|
|
|
### Org-Level Roles
|
|
|
|
| Role | Capabilities |
|
|
|---------|-------------|
|
|
| owner | Delete org, transfer ownership, manage members, create/delete projects |
|
|
| admin | Manage members, create/delete projects, update org settings |
|
|
| member | View org and its projects, join projects |
|
|
|
|
### Project-Level Roles
|
|
|
|
| Role | Capabilities |
|
|
|-----------|-------------|
|
|
| admin | Update/delete project, manage project members, delete issues, manage stages, start/complete sprints |
|
|
| developer | Create issues, assign issues, update issue status/priority/stage, log time, add tags, add relations |
|
|
| reporter | Create issues, comment, view issues |
|
|
|
|
### RBAC Implementation
|
|
|
|
- `RequireRole<ProjectRole>` and `RequireRole<OrgRole>` Axum extractors
|
|
- Extract `CurrentUser` from JWT middleware
|
|
- Query membership for the org/project (from path params)
|
|
- Reject if not a member or insufficient role (401/403)
|
|
- Org members automatically get `reporter` role in org's projects unless explicitly assigned
|
|
|
|
---
|
|
|
|
## Implementation Phases
|
|
|
|
### Phase 1: Foundation (Axum + DB + Health)
|
|
|
|
**Dependencies:** `axum`, `tokio`, `sqlx` (postgres, runtime-tokio, tls-rustls, migrate, uuid, chrono), `serde`/`serde_json`, `uuid`, `chrono`
|
|
|
|
**Tasks:**
|
|
1. Extend `Config` with `jwt_secret`, `db_url` parsing, `server_port`
|
|
2. Create `AppState` struct holding `PgPool` and `Config`
|
|
3. Set up `sqlx::PgPool` with `sqlx::migrate!().run()` on startup
|
|
4. Create health endpoints: `GET /api/v1/health/live` and `GET /api/v1/health/ready`
|
|
5. Wire up Axum router with `routes()`, shared state, and graceful shutdown via `tokio::signal`
|
|
6. Create first migration: `users` table
|
|
|
|
### Phase 2: Auth (JWT + Register/Login/Refresh)
|
|
|
|
**Additional dependencies:** `jsonwebtoken`, `argon2`, `validator`
|
|
|
|
**Tasks:**
|
|
1. Create `users` migration (if not done in Phase 1)
|
|
2. Create `refresh_tokens` migration
|
|
3. Implement `auth::hash` — argon2 password hashing/verification
|
|
4. Implement `auth::jwt` — HS256 access + refresh token creation/validation
|
|
5. Implement `db::repos::users` — create, get by email, get by id
|
|
6. Implement `db::repos::refresh_tokens` — store, find, delete
|
|
7. Implement `auth::service` — register, login, refresh logic
|
|
8. Implement `auth::handlers` — `POST /api/v1/auth/register`, `/login`, `/refresh`
|
|
9. Implement `auth::models` — request/response DTOs with `validator` checks
|
|
10. Create `errors.rs` — unified `AppError` enum → `IntoResponse`
|
|
11. Create `middleware::auth` — JWT extraction, inject `CurrentUser`
|
|
12. Protect routes with auth layer
|
|
|
|
### Phase 3: RBAC Layer
|
|
|
|
**Tasks:**
|
|
1. Create `organization_memberships` and `project_memberships` migrations
|
|
2. Define `OrgRole` and `ProjectRole` as SQL enums + Rust enums
|
|
3. Implement `db::repos::memberships` — add/remove/check, get role
|
|
4. Create `RequireRole<ProjectRole>` and `RequireRole<OrgRole>` Axum extractors
|
|
5. Wire role guards into project/issue routes
|
|
|
|
### Phase 4: Core Domain (Organizations → Projects → Issues)
|
|
|
|
**Tasks:**
|
|
1. **Organizations** — CRUD at `POST/GET/PATCH/DELETE /api/v1/orgs`
|
|
- Only org owner/admin can update/delete
|
|
- Org creator becomes owner automatically
|
|
- Slug auto-generated from name
|
|
2. **Projects** — CRUD at `/api/v1/orgs/{org_slug}/projects`
|
|
- Project membership inherits from org membership
|
|
- Org owner/admin can create projects
|
|
- Project admin can update project settings
|
|
3. **Issues** — CRUD at `/api/v1/orgs/{org_slug}/projects/{project_slug}/issues`
|
|
- reporter+ can create issues
|
|
- developer+ can assign/update status
|
|
- admin can delete issues
|
|
- `tsvector` column for full-text search on title + description
|
|
- `GET .../issues?q=search+terms` uses `tsquery`
|
|
4. **Pagination** — cursor-based pagination on all list endpoints
|
|
5. **utoipa** — add `#[derive(OpenApi)]` annotations, expose `GET /api/v1/docs/openapi.json`
|
|
|
|
### Phase 5: Issue Rich Content (Tags, Comments, Relations, Time Tracking, Assignees)
|
|
|
|
**Tasks:**
|
|
1. **Tags** — `tags` + `issue_tags` tables
|
|
- Create/list/delete tags per project (developer+ to create, admin to delete)
|
|
- Set issue tags (developer+)
|
|
- Filter issues by tag: `GET .../issues?tag=bug`
|
|
2. **Comments** — `comments` table
|
|
- List/create comments on issues (reporter+)
|
|
- Edit own comment, delete own comment or admin
|
|
3. **Issue Relations** — `issue_relations` table with relation types
|
|
- Add/remove relations (developer+)
|
|
- Types: blocks, is_blocked_by, duplicates, is_duplicated_by, relates_to, clones, is_cloned_by
|
|
4. **Time Tracking** — `time_entries` table
|
|
- Log time on issues (developer+)
|
|
- Delete own entry or admin
|
|
- Aggregate total time spent on issue (computed from entries)
|
|
5. **Multiple Assignees** — `issue_assignees` join table
|
|
- Replace single `assignee_id` with join table
|
|
- Set/replace assignees on issue (developer+)
|
|
- Filter issues by assignee: `GET .../issues?assignee={user_id}`
|
|
|
|
### Phase 6: Sprints & Stages
|
|
|
|
**Tasks:**
|
|
1. **Stages** — `stages` table (custom board columns per project)
|
|
- Create/reorder/delete stages (project admin)
|
|
- Default stages created with new project (e.g. Todo, In Progress, Done)
|
|
- Issue `stage_id` tracks which column it's in
|
|
2. **Sprints** — `sprints` table
|
|
- Sprint CRUD endpoints
|
|
- Sprint state transitions: `planned` → `active` → `completed`
|
|
- Issue-sprint assignment via `sprint_id` on issues (developer+)
|
|
3. **Sprint Board** — `GET .../sprints/{id}/board`
|
|
- Returns issues grouped by stage within the sprint
|
|
- Moving an issue between stages updates `stage_id`
|
|
|
|
### Phase 7: Polish & Hardening
|
|
|
|
**Tasks:**
|
|
1. CORS middleware (configurable origins from env)
|
|
2. Request ID middleware (propagate through logging)
|
|
3. Rate limiting (e.g., `tower-governor`)
|
|
4. Comprehensive error responses (validation → 422, auth → 401/403)
|
|
5. Integration tests with test DB containers (`testcontainers`)
|
|
6. Update Dockerfile and compose.yaml for production-ready build
|
|
|
|
### Phase 8: Plugin System (Lua Scripting)
|
|
|
|
**Additional dependencies:** `mlua` (lua54, vendored, async, serialize), `cron` (cron expression parsing)
|
|
|
|
**Concept:** Users write Lua scripts triggered by events or schedules. Scripts get a sandboxed API to automate workflows (e.g., auto-migrate tasks when a sprint ends, auto-assign issues, send notifications).
|
|
|
|
**Example user scripts:**
|
|
```lua
|
|
-- Auto-resolve in-progress tasks when sprint ends
|
|
on_event("sprint.ended", function(ctx)
|
|
local issues = ctx.project:issues({ status = "in_progress", sprint = ctx.sprint.id })
|
|
for _, issue in ipairs(issues) do
|
|
issue:update({ status = "resolved", comment = "Auto-resolved: sprint ended" })
|
|
end
|
|
end)
|
|
```
|
|
|
|
```lua
|
|
-- Every Friday at 6pm, notify about stale issues
|
|
on_schedule("0 18 * * 5", function(ctx)
|
|
local stale = ctx.project:issues({ status = "open", updated_before = "7d" })
|
|
for _, issue in ipairs(stale) do
|
|
issue:add_comment("This issue has been inactive for 7 days.")
|
|
end
|
|
end)
|
|
```
|
|
|
|
**Trigger models (cron + event-driven only):**
|
|
|
|
| Trigger | Example | Implementation |
|
|
|---------|---------|----------------|
|
|
| Event-driven | `on_event("issue.created", ...)` | Hook into service layer, emit events after mutations |
|
|
| Scheduled (cron) | `on_schedule("0 18 * * 5", ...)` | Cron expressions, background tokio task |
|
|
|
|
**Available events:**
|
|
- `issue.created`, `issue.updated`, `issue.deleted`
|
|
- `issue.status_changed`, `issue.assigned`, `issue.unassigned`
|
|
- `comment.created`, `comment.deleted`
|
|
- `sprint.started`, `sprint.ended`, `sprint.completed`
|
|
- `tag.created`, `tag.removed`
|
|
|
|
**Sandboxed Lua API surface:**
|
|
- `ctx.project:issues(filter)` — query issues
|
|
- `ctx.project:sprints()` — list sprints
|
|
- `issue:update(fields)` — mutate issue
|
|
- `issue:add_comment(body)` — add comment
|
|
- `ctx.user` — current user info
|
|
|
|
**Safety model:**
|
|
- No filesystem, no network access
|
|
- Execution timeout (e.g., 5s max per script)
|
|
- Memory limit via `mlua` Lua state options
|
|
- Scripts run in separate tokio tasks, never block the API
|
|
|
|
**Script storage:**
|
|
- `scripts` table in Postgres (org_id, project_id, name, source, trigger_type, trigger_config, enabled)
|
|
- `script_revisions` table for revision history / audit
|
|
- `script_execution_logs` table for past runs, errors, output
|
|
|
|
**Codebase location:**
|
|
```
|
|
src/
|
|
├── scripting/
|
|
│ ├── mod.rs # Script engine, mlua setup, sandbox config
|
|
│ ├── api.rs # Lua API bindings (ctx.project, ctx.issue, etc.)
|
|
│ ├── scheduler.rs # Cron-based trigger runner
|
|
│ ├── hooks.rs # Event emission from service layer
|
|
│ └── models.rs # Script DB model + DTOs
|
|
```
|
|
|
|
**Tasks:**
|
|
1. Add `scripts`, `script_revisions`, and `script_execution_logs` tables to DB
|
|
2. Implement `scripting::mod` — mlua sandbox setup, script compilation/validation
|
|
3. Implement `scripting::api` — expose safe Lua API (ctx.project, ctx.issue, ctx.sprint)
|
|
4. Implement `scripting::hooks` — event emission from service layer
|
|
5. Implement `scripting::scheduler` — cron expression parsing + background tokio task runner
|
|
6. Script CRUD endpoints at `/api/v1/orgs/{org_slug}/projects/{project_slug}/scripts`
|
|
7. Script execution endpoint: `POST .../scripts/{id}/run` (for manual trigger / testing)
|
|
8. Script execution log endpoint: `GET .../scripts/{id}/logs`
|
|
9. Admin controls: enable/disable scripts per org/project, execution timeout config
|
|
|
|
---
|
|
|
|
## Implementation Order (within each feature)
|
|
|
|
1. **Migration** → write and run the SQL migration
|
|
2. **Repository** → write the sqlx queries + Rust repo struct
|
|
3. **Model** → define the Rust domain model + DTOs
|
|
4. **Service** → business logic (validation, authorization rules)
|
|
5. **Handler** → thin Axum handler calling service
|
|
6. **Route** → wire into the router with appropriate middleware
|
|
7. **Test** → write a test for the endpoint
|
|
|
|
---
|
|
|
|
## Testing Approach
|
|
|
|
### Phase 1 (Recommended Start)
|
|
|
|
- Unit tests for pure service logic (no DB, mock repositories)
|
|
- Integration tests for repositories with real Postgres via Docker
|
|
|
|
### Repository Testing
|
|
|
|
- Define trait interfaces for repositories
|
|
- For service unit tests, provide mock implementations
|
|
- Focus unit tests on business rules, branching, and error mapping
|
|
- Keep SQL correctness in integration tests against real Postgres
|
|
- This split gives fast unit tests plus high-confidence DB integration coverage
|
|
|
|
### Phase 2
|
|
|
|
- HTTP handler tests with Axum test helpers
|
|
- Auth middleware tests
|
|
- RBAC extractor tests
|
|
|
|
### Phase 3
|
|
|
|
- Minimal end-to-end happy path tests
|
|
- Load/stress testing
|
|
|
|
---
|
|
|
|
## OpenAPI + Frontend Type Generation
|
|
|
|
- utoipa annotations on handlers and models
|
|
- Auto-generate OpenAPI spec at build time
|
|
- Expose at `GET /api/v1/docs/openapi.json`
|
|
- Generate frontend TypeScript types from OpenAPI (e.g., `openapi-typescript`)
|
|
- Optionally serve Swagger UI from backend via `utoipa-swagger-ui`
|
|
|
|
---
|
|
|
|
## Deployment
|
|
|
|
- Docker Compose for app + postgres
|
|
- Healthcheck in compose targets readiness endpoint
|
|
- Env-based configuration (`.env`, `.env.example`)
|
|
- Multi-stage Dockerfile (rust:alpine builder → alpine runtime)
|
|
|
|
---
|
|
|
|
## Iteration Notes
|
|
|
|
- This file is intentionally a working draft.
|
|
- We will refine decisions and turn this into a concrete implementation checklist.
|
|
- Originally started as Go/Gin, pivoted to Rust/Axum for the Rust learning journey.
|