docker compose
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m51s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m51s
This commit is contained in:
parent
3a51cf863e
commit
e93a65ae9c
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@ -0,0 +1,12 @@
|
||||
.git/
|
||||
.gitea/
|
||||
bruno/
|
||||
target/
|
||||
.dockerignore
|
||||
.env
|
||||
.gitignore
|
||||
BACKEND_BLUEPRINT.md
|
||||
Dockerfile
|
||||
LICENSE
|
||||
compose.prod.yaml
|
||||
compose.yaml
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
.env
|
||||
/target
|
||||
/logs
|
||||
postgres-data
|
||||
|
||||
@ -1,51 +1,137 @@
|
||||
# Go Backend Blueprint (Iteration Draft)
|
||||
# Rhythm Backend Blueprint (Rust/Axum)
|
||||
|
||||
## IDEAS
|
||||
- instead of elastic search use ts vector and ts query to allow easy iseeu searching for smooth experience (pg vector?)
|
||||
- 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
|
||||
|
||||
- **Framework:** Gin
|
||||
- **Dependency Injection:** Uber fx (runtime DI, Spring-like autowiring)
|
||||
- **Migrations:** Goose (SQL-only)
|
||||
- **DB Access:** SQLC (no ORM), package per bounded context
|
||||
- **PostgreSQL Driver/Pool:** pgx/v5 + pgxpool
|
||||
- **Logging:** Uber Zap (structured logs)
|
||||
- **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
|
||||
- **API Docs:** OpenAPI (for frontend TypeScript type generation)
|
||||
- **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
|
||||
|
||||
### Layering
|
||||
### Project Structure
|
||||
|
||||
- `cmd/api` - application entrypoint and fx DI wiring
|
||||
- `internal/http` - Gin Server provider, `router.go` for fx mapping
|
||||
- `internal/http/api/...` - Domain handlers structured by route hierarchy (e.g., `api/health`, `api/protected/users`)
|
||||
- `internal/service` - business logic + transaction boundaries
|
||||
- `internal/db/<context>` - SQLC-generated code by bounded context
|
||||
- `internal/store` - shared DB/Tx helpers
|
||||
- `internal/auth` - JWT validation + role guards
|
||||
- `internal/config` - env configuration loading
|
||||
- `migrations/` - Goose SQL migration files
|
||||
- `api/openapi/` - OpenAPI spec + generated artifacts
|
||||
```
|
||||
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
|
||||
|
||||
- Handlers stay thin.
|
||||
- Service layer owns DB transactions.
|
||||
- SQLC queries are called with either pool or tx using `DBTX` interfaces.
|
||||
- No transaction logic in handlers.
|
||||
|
||||
### Dependency Injection (fx)
|
||||
|
||||
- Use `fx.Provide` to register constructors (config, logger, db pool, server, handlers).
|
||||
- Use `fx.Invoke` in `main.go` to orchestrate route mapping and application startup.
|
||||
- `internal/http/router.go` acts as the central route mapping orchestrator injected via `fx.Invoke`.
|
||||
- Each domain handler (`internal/http/api/*/handler.go`) receives its dependencies via constructor injection.
|
||||
- Lifecycle hooks (`fx.Lifecycle`) are used for graceful startup/shutdown of the HTTP server and DB pool.
|
||||
- Service layer owns DB transactions
|
||||
- Repositories accept either `&PgPool` or `&mut Transaction<Postgres>`
|
||||
- sqlx compile-time query checking ensures SQL correctness at build time
|
||||
|
||||
---
|
||||
|
||||
@ -55,131 +141,588 @@
|
||||
|
||||
- REST JSON API
|
||||
- Possible future WebSocket support for interactive features
|
||||
- Suggested versioning: `/api/v1`
|
||||
- Versioning: `/api/v1`
|
||||
|
||||
### Health Endpoints
|
||||
|
||||
- `GET /api/health/live` - process is alive
|
||||
- `GET /api/health/ready` - DB ping succeeds (and optionally migration version check)
|
||||
- `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
|
||||
|
||||
- Zap JSON logs
|
||||
- Uber Zap logger will be initialized in `cmd/api/main.go` and injected into services/middleware (no global loggers).
|
||||
- Logs will be written directly to a file from the application, with log rotation implemented via `lumberjack`.
|
||||
- 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 and Migrations
|
||||
## Database Schema
|
||||
|
||||
### Goose
|
||||
```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');
|
||||
|
||||
- SQL-only migrations
|
||||
- Keep up/down migration scripts
|
||||
- Run on startup in non-prod optional, required in CI/CD/deploy step
|
||||
- Migrations are securely bundled into the binary using Go's `embed.FS` from a dedicated `migrations` package to isolate them from `internal` db logic.
|
||||
-- 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()
|
||||
);
|
||||
|
||||
### SQLC
|
||||
-- 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()
|
||||
);
|
||||
|
||||
- Generate one package per bounded context (similar to Spring repository modules)
|
||||
- Keep SQL in context directories (`query.sql`, `models.sql` style)
|
||||
- Service layer composes multiple repositories when needed
|
||||
-- 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)
|
||||
);
|
||||
|
||||
### Work in Progress Snapshot
|
||||
-- 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)
|
||||
);
|
||||
|
||||
- `sqlc.yaml` is now configured for PostgreSQL with schema from `migrations/*.sql`
|
||||
- First bounded context added: `internal/db/users`
|
||||
- Current users SQLC artifacts generated:
|
||||
- `db.go`, `models.go`, `queries.sql.go`, `querier.go`
|
||||
- Current SQLC generation options in use:
|
||||
- `sql_package: pgx/v5`
|
||||
- `emit_interface: true` (generated `Querier` interface)
|
||||
- `emit_json_tags: false` (can be revisited if API structs are returned directly)
|
||||
- Initial queries implemented for users: `GetUser`, `CreateUser`, `DeleteUser`
|
||||
- **Goose startup migrations** wired into `db.ProvidePool`, utilizing the `embed.FS` strategy.
|
||||
- **fx DI fully implemented.** All dependencies are provided via `fx.Provide` constructors:
|
||||
- `config.Provide` → `*Config`
|
||||
- `logger.NewFromConfig` → `*zap.SugaredLogger`
|
||||
- `db.ProvidePool` → `*pgxpool.Pool` (lifecycle hooks for graceful shutdown)
|
||||
- `http.NewServer` → `*gin.Engine` (lifecycle hooks for startup/shutdown)
|
||||
- `health.NewHandler` → `*health.Handler`
|
||||
- **HTTP route hierarchy** via `internal/http/router.go` (`GlueRoutes`):
|
||||
- `GET /api/health/live` - process is alive
|
||||
- `GET /api/health/ready` - DB ping succeeds
|
||||
- **Next Planned:** Auth handler (`/api/auth/login`, `/api/auth/register`), JWT middleware, service layer.
|
||||
-- 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.
|
||||
|
||||
---
|
||||
|
||||
## Testing Approach (Beginner-Friendly)
|
||||
## 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)
|
||||
- Integration tests for SQLC repositories with real Postgres via Docker
|
||||
- Unit tests for pure service logic (no DB, mock repositories)
|
||||
- Integration tests for repositories with real Postgres via Docker
|
||||
|
||||
### DB Interface Testing (via SQLC `Querier`)
|
||||
### Repository Testing
|
||||
|
||||
- Treat generated SQLC interfaces (e.g. `usersdb.Querier`) as service dependencies
|
||||
- For service unit tests, provide a fake/mock implementation of `Querier`
|
||||
- Focus unit tests on business rules, branching, and error mapping (not SQL behavior)
|
||||
- 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 `httptest`
|
||||
- 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
|
||||
|
||||
- Keep spec in repo at `api/openapi/openapi.yaml`
|
||||
- Generate frontend TypeScript types from OpenAPI (e.g. `openapi-typescript`)
|
||||
- Optionally serve Swagger UI from backend
|
||||
- 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 should target readiness endpoint
|
||||
- Healthcheck in compose targets readiness endpoint
|
||||
- Env-based configuration (`.env`, `.env.example`)
|
||||
|
||||
---
|
||||
|
||||
## Pending Decisions
|
||||
|
||||
1. JWT signing:
|
||||
- HS256 shared secret (simple)
|
||||
- RS256 keypair (better long-term)
|
||||
|
||||
2. Token model:
|
||||
- Access token only
|
||||
- Access + refresh token
|
||||
|
||||
3. Initial roles:
|
||||
- USER / ADMIN
|
||||
- USER / MODERATOR / ADMIN
|
||||
|
||||
4. OpenAPI workflow:
|
||||
- Contract-first (spec first)
|
||||
- Code-first annotations
|
||||
|
||||
5. CORS policy:
|
||||
- Allowed frontend origins in dev/prod
|
||||
|
||||
6. Schema strategy:
|
||||
- Single schema (`public`) confirmation
|
||||
|
||||
7. Initial bounded contexts:
|
||||
- e.g. `auth`, `users`, `rooms` (or your domain names)
|
||||
- Multi-stage Dockerfile (rust:alpine builder → alpine runtime)
|
||||
|
||||
---
|
||||
|
||||
@ -187,3 +730,4 @@
|
||||
|
||||
- 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.
|
||||
|
||||
22
Dockerfile
22
Dockerfile
@ -1,19 +1,15 @@
|
||||
FROM golang:1.26-bookworm AS builder
|
||||
WORKDIR /src
|
||||
FROM rust:1.95.0-alpine3.22 AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Cache deps first
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
ARG DB_URL
|
||||
ARG APP_ENV
|
||||
|
||||
# Copy source and build
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
go build -trimpath -ldflags="-s -w" -o /out/api ./cmd/api
|
||||
RUN cargo build --release
|
||||
|
||||
# Small runtime image
|
||||
FROM gcr.io/distroless/static-debian12
|
||||
FROM alpine:3.22.4
|
||||
WORKDIR /app
|
||||
COPY --from=builder /out/api /app/api
|
||||
EXPOSE 8080
|
||||
USER nonroot:nonroot
|
||||
ENTRYPOINT ["/app/api"]
|
||||
COPY --from=builder /app/target/release/rhythm-backend /app/executable
|
||||
|
||||
ENTRYPOINT ["/app/executable"]
|
||||
|
||||
9
bruno/.gitignore
vendored
9
bruno/.gitignore
vendored
@ -1,9 +0,0 @@
|
||||
# Secrets
|
||||
.env*
|
||||
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@ -1,27 +0,0 @@
|
||||
info:
|
||||
name: login user
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: "{{host}}/api/v1/auth/login"
|
||||
body:
|
||||
type: json
|
||||
data: |-
|
||||
{
|
||||
"email": "dmo@dmo.dmo",
|
||||
"password": "password12345"
|
||||
}
|
||||
auth: inherit
|
||||
|
||||
runtime:
|
||||
variables:
|
||||
- name: host
|
||||
value: http://localhost:8080
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
@ -1,10 +0,0 @@
|
||||
opencollection: 1.0.0
|
||||
|
||||
info:
|
||||
name: rhythm
|
||||
bundled: false
|
||||
extensions:
|
||||
bruno:
|
||||
ignore:
|
||||
- node_modules
|
||||
- .git
|
||||
@ -1,27 +0,0 @@
|
||||
info:
|
||||
name: register user
|
||||
type: http
|
||||
seq: 2
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: "{{host}}/api/v1/auth/register"
|
||||
body:
|
||||
type: json
|
||||
data: |-
|
||||
{
|
||||
"email": "dmo@dmo.dmo",
|
||||
"password": "password12345"
|
||||
}
|
||||
auth: inherit
|
||||
|
||||
runtime:
|
||||
variables:
|
||||
- name: host
|
||||
value: http://localhost:8080
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
@ -1,36 +0,0 @@
|
||||
services:
|
||||
api-prod:
|
||||
image: git.kanopo.dev/rhythm/rhythm-backend:latest
|
||||
restart: unless-stopped
|
||||
container_name: rhythm-api-prod
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
DB_HOST: db-prod
|
||||
env_file:
|
||||
- ".env"
|
||||
depends_on:
|
||||
db-prod:
|
||||
condition: service_healthy
|
||||
profiles:
|
||||
- prod
|
||||
|
||||
db-prod:
|
||||
image: postgres:18.0-alpine
|
||||
restart: unless-stopped
|
||||
container_name: rhythm-db-prod
|
||||
ports:
|
||||
- "${DB_PORT_PROD:-5433}:5432"
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_DB: ${DB_NAME}
|
||||
volumes:
|
||||
- ./.data/postgres-prod:/var/lib/postgresql/data
|
||||
profiles:
|
||||
- prod
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_NAME}"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
67
compose.yaml
67
compose.yaml
@ -1,40 +1,4 @@
|
||||
services:
|
||||
api-prod:
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
container_name: rhythm-api-prod
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
DB_HOST: db-prod
|
||||
env_file:
|
||||
- ".env"
|
||||
depends_on:
|
||||
db-prod:
|
||||
condition: service_healthy
|
||||
profiles:
|
||||
- prod
|
||||
|
||||
db-prod:
|
||||
image: postgres:18.0-alpine
|
||||
restart: unless-stopped
|
||||
container_name: rhythm-db-prod
|
||||
ports:
|
||||
- "${DB_PORT_PROD:-5433}:5432"
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_DB: ${DB_NAME}
|
||||
volumes:
|
||||
- ./.data/postgres-prod:/var/lib/postgresql/data
|
||||
profiles:
|
||||
- prod
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_NAME}"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
db-dev:
|
||||
image: postgres:18.0-alpine
|
||||
restart: unless-stopped
|
||||
@ -53,5 +17,34 @@ services:
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
api-prod:
|
||||
image: git.kanopo.dev/rhythm/rhythm-backend:latest
|
||||
container_name: rhythm-api-prod
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DB_HOST: db-prod
|
||||
env_file:
|
||||
- ".env"
|
||||
depends_on:
|
||||
db-prod:
|
||||
condition: service_healthy
|
||||
profiles:
|
||||
- prod
|
||||
|
||||
db-prod:
|
||||
image: postgres:18.0-alpine
|
||||
restart: unless-stopped
|
||||
container_name: rhythm-db-prod
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_DB: rhythm-prod
|
||||
volumes:
|
||||
postgres_data:
|
||||
- ./postgres-data:/var/lib/postgresql/data
|
||||
profiles:
|
||||
- prod
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d rhythm-prod"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user