Merge branch 'worktree-agent-ae56a15a' into Develop
# Conflicts: # .planning/ROADMAP.md # .planning/STATE.md # docker-compose.dev.yml # docker-compose.yml # src/db/schema.ts
This commit is contained in:
15
.env.example
Normal file
15
.env.example
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# PostgreSQL
|
||||||
|
POSTGRES_PASSWORD=changeme
|
||||||
|
|
||||||
|
# Logto OIDC (get from Logto Admin Console at http://localhost:3002)
|
||||||
|
LOGTO_ENDPOINT=http://localhost:3001
|
||||||
|
LOGTO_ADMIN_ENDPOINT=http://localhost:3002
|
||||||
|
LOGTO_CLIENT_ID=your-app-client-id
|
||||||
|
LOGTO_CLIENT_SECRET=your-app-client-secret
|
||||||
|
OIDC_AUTH_SECRET=generate-a-random-32-char-string-here
|
||||||
|
|
||||||
|
# Derived (set in docker-compose.yml, not needed here):
|
||||||
|
# OIDC_ISSUER=${LOGTO_ENDPOINT}/oidc
|
||||||
|
|
||||||
|
# GearBox
|
||||||
|
GEARBOX_URL=http://localhost:3000
|
||||||
@@ -20,7 +20,7 @@ Requirements for this milestone. Each maps to roadmap phases.
|
|||||||
- [ ] **AUTH-01**: User can register an account via external OIDC auth provider
|
- [ ] **AUTH-01**: User can register an account via external OIDC auth provider
|
||||||
- [ ] **AUTH-02**: User can log in via external auth provider and access their data
|
- [ ] **AUTH-02**: User can log in via external auth provider and access their data
|
||||||
- [ ] **AUTH-03**: API keys remain functional for programmatic access (MCP, scripts)
|
- [ ] **AUTH-03**: API keys remain functional for programmatic access (MCP, scripts)
|
||||||
- [ ] **AUTH-04**: Auth provider runs self-hosted alongside the application
|
- [x] **AUTH-04**: Auth provider runs self-hosted alongside the application
|
||||||
- [ ] **AUTH-05**: E2E tests authenticate via API keys without depending on the auth provider
|
- [ ] **AUTH-05**: E2E tests authenticate via API keys without depending on the auth provider
|
||||||
|
|
||||||
### Multi-User Data Model
|
### Multi-User Data Model
|
||||||
@@ -124,7 +124,7 @@ Which phases cover which requirements. Updated during roadmap creation.
|
|||||||
| AUTH-01 | Phase 15 | Pending |
|
| AUTH-01 | Phase 15 | Pending |
|
||||||
| AUTH-02 | Phase 15 | Pending |
|
| AUTH-02 | Phase 15 | Pending |
|
||||||
| AUTH-03 | Phase 15 | Pending |
|
| AUTH-03 | Phase 15 | Pending |
|
||||||
| AUTH-04 | Phase 15 | Pending |
|
| AUTH-04 | Phase 15 | Complete |
|
||||||
| AUTH-05 | Phase 15 | Pending |
|
| AUTH-05 | Phase 15 | Pending |
|
||||||
| MULTI-01 | Phase 16 | Pending |
|
| MULTI-01 | Phase 16 | Pending |
|
||||||
| MULTI-02 | Phase 16 | Pending |
|
| MULTI-02 | Phase 16 | Pending |
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
- ✅ **v1.0 MVP** — Phases 1-3 (shipped 2026-03-15)
|
- ✅ **v1.0 MVP** — Phases 1-3 (shipped 2026-03-15)
|
||||||
- ✅ **v1.1 Fixes & Polish** — Phases 4-6 (shipped 2026-03-15)
|
- ✅ **v1.1 Fixes & Polish** — Phases 4-6 (shipped 2026-03-15)
|
||||||
- ✅ **v1.2 Collection Power-Ups** — Phases 7-9 (shipped 2026-03-16)
|
- ✅ **v1.2 Collection Power-Ups** — Phases 7-9 (shipped 2026-03-16)
|
||||||
- ✅ **v1.3 Research & Decision Tools** — Phases 10-13 (shipped)
|
- 🚧 **v1.3 Research & Decision Tools** — Phases 10-13 (in progress)
|
||||||
- 📋 **v2.0 Platform Foundation** — Phases 14-18 (planned)
|
- 📋 **v2.0 Platform Foundation** — Phases 14-18 (planned)
|
||||||
|
|
||||||
## Phases
|
## Phases
|
||||||
@@ -37,21 +37,20 @@
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
### v1.3 Research & Decision Tools (In Progress)
|
||||||
<summary>✅ v1.3 Research & Decision Tools (Phases 10-13) — SHIPPED</summary>
|
|
||||||
|
|
||||||
- [x] Phase 10: Schema Foundation + Pros/Cons Fields (1/1 plans) — completed 2026-03-16
|
**Milestone Goal:** Give users the tools to actually decide between candidates — compare details side-by-side, see how a pick impacts their setup, and rank/annotate their options.
|
||||||
- [x] Phase 11: Candidate Ranking (2/2 plans) — completed 2026-03-16
|
|
||||||
- [x] Phase 12: Comparison View (1/1 plans) — completed 2026-03-17
|
|
||||||
- [x] Phase 13: Setup Impact Preview (completed outside GSD)
|
|
||||||
|
|
||||||
</details>
|
- [x] **Phase 10: Schema Foundation + Pros/Cons Fields** — Migrate schema and deliver pros/cons annotation UI (completed 2026-03-16)
|
||||||
|
- [x] **Phase 11: Candidate Ranking** — Drag-to-reorder priority ranking with rank badges (completed 2026-03-16)
|
||||||
|
- [x] **Phase 12: Comparison View** — Side-by-side tabular comparison with relative deltas (completed 2026-03-17)
|
||||||
|
- [ ] **Phase 13: Setup Impact Preview** — Per-candidate weight and cost delta against a selected setup
|
||||||
|
|
||||||
### v2.0 Platform Foundation (Planned)
|
### v2.0 Platform Foundation (Planned)
|
||||||
|
|
||||||
**Milestone Goal:** Transform GearBox from a single-user gear tracker into a multi-user platform where people discover gear, research purchases using crowd-verified data, and share their setups.
|
**Milestone Goal:** Transform GearBox from a single-user gear tracker into a multi-user platform where people discover gear, research purchases using crowd-verified data, and share their setups.
|
||||||
|
|
||||||
- [x] **Phase 14: PostgreSQL Migration** — Replace SQLite with Postgres, make all operations async, establish new test infrastructure (completed 2026-04-04)
|
- [ ] **Phase 14: PostgreSQL Migration** — Replace SQLite with Postgres, make all operations async, establish new test infrastructure
|
||||||
- [ ] **Phase 15: External Authentication** — Integrate self-hosted OIDC auth provider for user registration and login
|
- [ ] **Phase 15: External Authentication** — Integrate self-hosted OIDC auth provider for user registration and login
|
||||||
- [ ] **Phase 16: Multi-User Data Model** — Add user ownership to all entities with cross-user data isolation
|
- [ ] **Phase 16: Multi-User Data Model** — Add user ownership to all entities with cross-user data isolation
|
||||||
- [ ] **Phase 17: Object Storage** — Move images from local filesystem to MinIO (S3-compatible)
|
- [ ] **Phase 17: Object Storage** — Move images from local filesystem to MinIO (S3-compatible)
|
||||||
@@ -134,11 +133,7 @@ Plans:
|
|||||||
3. API keys continue to work for MCP tools and programmatic access without involving the auth provider
|
3. API keys continue to work for MCP tools and programmatic access without involving the auth provider
|
||||||
4. E2E tests run successfully using API key authentication, with no dependency on the external auth provider being available
|
4. E2E tests run successfully using API key authentication, with no dependency on the external auth provider being available
|
||||||
5. The auth provider runs self-hosted in Docker Compose alongside Postgres and the application
|
5. The auth provider runs self-hosted in Docker Compose alongside Postgres and the application
|
||||||
**Plans:** 3 plans
|
**Plans**: TBD
|
||||||
Plans:
|
|
||||||
- [ ] 15-01-PLAN.md — Docker Compose Logto service + schema migration (drop users/sessions)
|
|
||||||
- [ ] 15-02-PLAN.md — Server-side OIDC auth integration (middleware, routes, services, MCP)
|
|
||||||
- [ ] 15-03-PLAN.md — Client auth refactor + E2E seed + test updates
|
|
||||||
|
|
||||||
### Phase 16: Multi-User Data Model
|
### Phase 16: Multi-User Data Model
|
||||||
**Goal**: Every piece of user-created data is owned by a specific user, with complete isolation between users
|
**Goal**: Every piece of user-created data is owned by a specific user, with complete isolation between users
|
||||||
@@ -193,8 +188,8 @@ Plans:
|
|||||||
| 11. Candidate Ranking | v1.3 | 2/2 | Complete | 2026-03-16 |
|
| 11. Candidate Ranking | v1.3 | 2/2 | Complete | 2026-03-16 |
|
||||||
| 12. Comparison View | v1.3 | 1/1 | Complete | 2026-03-17 |
|
| 12. Comparison View | v1.3 | 1/1 | Complete | 2026-03-17 |
|
||||||
| 13. Setup Impact Preview | v1.3 | 0/2 | Not started | - |
|
| 13. Setup Impact Preview | v1.3 | 0/2 | Not started | - |
|
||||||
| 14. PostgreSQL Migration | v2.0 | 6/6 | Complete | 2026-04-04 |
|
| 14. PostgreSQL Migration | v2.0 | 0/? | Not started | - |
|
||||||
| 15. External Authentication | v2.0 | 0/3 | Not started | - |
|
| 15. External Authentication | v2.0 | 1/3 | In Progress| |
|
||||||
| 16. Multi-User Data Model | v2.0 | 0/? | Not started | - |
|
| 16. Multi-User Data Model | v2.0 | 0/? | Not started | - |
|
||||||
| 17. Object Storage | v2.0 | 0/? | Not started | - |
|
| 17. Object Storage | v2.0 | 0/? | Not started | - |
|
||||||
| 18. Global Items & Public Profiles | v2.0 | 0/? | Not started | - |
|
| 18. Global Items & Public Profiles | v2.0 | 0/? | Not started | - |
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
gsd_state_version: 1.0
|
gsd_state_version: 1.0
|
||||||
milestone: v2.0
|
milestone: v2.0
|
||||||
milestone_name: Platform Foundation
|
milestone_name: Platform Foundation
|
||||||
status: Not yet planned
|
status: planning
|
||||||
stopped_at: Phase 15 context gathered
|
stopped_at: null
|
||||||
last_updated: "2026-04-04T18:15:44.234Z"
|
last_updated: "2026-04-03"
|
||||||
last_activity: 2026-04-04
|
last_activity: 2026-04-03 — v2.0 roadmap created (Phases 14-18)
|
||||||
progress:
|
progress:
|
||||||
total_phases: 9
|
total_phases: 5
|
||||||
completed_phases: 4
|
completed_phases: 0
|
||||||
total_plans: 12
|
total_plans: 0
|
||||||
completed_plans: 10
|
completed_plans: 0
|
||||||
percent: 20
|
percent: 0
|
||||||
---
|
---
|
||||||
|
|
||||||
# Project State
|
# Project State
|
||||||
@@ -21,24 +21,23 @@ progress:
|
|||||||
See: .planning/PROJECT.md (updated 2026-04-03)
|
See: .planning/PROJECT.md (updated 2026-04-03)
|
||||||
|
|
||||||
**Core value:** Help people make better gear decisions — discover what others use, compare real-world data, and see how a potential buy affects your setup before committing.
|
**Core value:** Help people make better gear decisions — discover what others use, compare real-world data, and see how a potential buy affects your setup before committing.
|
||||||
**Current focus:** v2.0 Platform Foundation — Phase 15 (External Authentication)
|
**Current focus:** v2.0 Platform Foundation — Phase 14 (PostgreSQL Migration)
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 15 of 18 (External Authentication)
|
Phase: 15 of 18 (External Authentication)
|
||||||
Plan: 0 of 0 in current phase
|
Plan: 1 of 3 in current phase
|
||||||
Status: Not yet planned
|
Status: Executing
|
||||||
Last activity: 2026-04-04
|
Last activity: 2026-04-04 — Completed 15-01 (Logto Docker infrastructure + schema cleanup)
|
||||||
|
|
||||||
Progress: [██--------] 20% (v2.0 milestone — 1/5 phases)
|
Progress: [=---------] 5% (v2.0 milestone)
|
||||||
|
|
||||||
## Performance Metrics
|
## Performance Metrics
|
||||||
|
|
||||||
**Velocity:**
|
**Velocity:**
|
||||||
|
- Total plans completed: 1 (v2.0 milestone)
|
||||||
- Total plans completed: 6 (v2.0 milestone, Phase 14)
|
- Average duration: 3min
|
||||||
- Average duration: --
|
- Total execution time: 3min
|
||||||
- Total execution time: --
|
|
||||||
|
|
||||||
*Updated after each plan completion*
|
*Updated after each plan completion*
|
||||||
|
|
||||||
@@ -46,23 +45,15 @@ Progress: [██--------] 20% (v2.0 milestone — 1/5 phases)
|
|||||||
|
|
||||||
### Decisions
|
### Decisions
|
||||||
|
|
||||||
Key decisions made during v2.0 execution:
|
Key decisions made during v2.0 planning and execution:
|
||||||
|
|
||||||
- [14-05] Used postgres.js unsafe() for sequence reset DDL instead of drizzle-orm sql template
|
|
||||||
- [14-05] Row-by-row inserts for better error diagnostics during migration
|
|
||||||
- Platform pivot: single-user to multi-user with discovery-first approach
|
- Platform pivot: single-user to multi-user with discovery-first approach
|
||||||
- External auth provider (self-hosted, open-source) — Logto vs Authentik OPEN decision
|
- External auth provider (self-hosted, open-source) — Logto selected (D-01)
|
||||||
- SQLite to Postgres migration — required by auth provider and multi-user concurrency
|
- SQLite to Postgres migration — required by auth provider and multi-user concurrency
|
||||||
- Structured UGC only — ratings and predefined fields, no freeform text until moderation
|
- Structured UGC only — ratings and predefined fields, no freeform text until moderation
|
||||||
- Separate globalItems table — not a flag on user items table
|
- Separate globalItems table — not a flag on user items table
|
||||||
- Single-user SQLite mode diverges at v2.0 boundary
|
- Single-user SQLite mode diverges at v2.0 boundary
|
||||||
- [Phase 14-03]: Async service pattern: const [row] = await db.select()... for single-row queries
|
- Logto shares Postgres instance via separate database created by init script
|
||||||
- [Phase 14-03]: OAuth used field converted from integer (0/1) to boolean (false/true)
|
- OIDC_ISSUER derived from LOGTO_ENDPOINT in docker-compose
|
||||||
- [Phase 14-04]: Settings route .get() replaced with destructuring: const [row] = await db.select()...
|
|
||||||
- [Phase 14-06]: Fixed PostgreSQL GROUP BY strictness in totals.service.ts
|
|
||||||
- [Phase 14-06]: Added await to all MCP tool service calls (missed in plan 14-03)
|
|
||||||
- [Phase 14-06]: Set test timeout to 30s for PGlite WASM overhead
|
|
||||||
- [Phase 13]: Setup Impact Preview completed outside GSD workflow
|
|
||||||
|
|
||||||
### Pending Todos
|
### Pending Todos
|
||||||
|
|
||||||
@@ -71,9 +62,10 @@ None active.
|
|||||||
### Blockers/Concerns
|
### Blockers/Concerns
|
||||||
|
|
||||||
- Auth provider decision (Logto vs Authentik) must be resolved before Phase 15 planning
|
- Auth provider decision (Logto vs Authentik) must be resolved before Phase 15 planning
|
||||||
|
- Phase 14 is a full schema rewrite touching 6 services, 7 routes, 19 MCP tools, all tests
|
||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-04-04T18:15:44.232Z
|
Last session: 2026-04-04
|
||||||
Stopped at: Phase 15 context gathered
|
Stopped at: Completed 15-01-PLAN.md (Logto Docker infrastructure + schema cleanup)
|
||||||
Resume file: .planning/phases/15-external-authentication/15-CONTEXT.md
|
Resume file: None
|
||||||
|
|||||||
102
.planning/phases/15-external-authentication/15-01-SUMMARY.md
Normal file
102
.planning/phases/15-external-authentication/15-01-SUMMARY.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
phase: 15-external-authentication
|
||||||
|
plan: 01
|
||||||
|
subsystem: infra
|
||||||
|
tags: [logto, oidc, docker-compose, postgres]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 14-postgresql-migration
|
||||||
|
provides: Postgres database and Docker Compose foundation
|
||||||
|
provides:
|
||||||
|
- Logto OIDC provider running as Docker Compose service
|
||||||
|
- Postgres init script for separate Logto database
|
||||||
|
- OIDC environment variable documentation
|
||||||
|
- Schema without users/sessions tables (ready for external auth)
|
||||||
|
affects: [15-02, 15-03, 16-multi-user-data-model]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: [logto (svhd/logto Docker image)]
|
||||||
|
patterns: [multi-database Postgres init via docker-entrypoint-initdb.d, OIDC env var convention]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- docker-compose.yml
|
||||||
|
- docker-compose.dev.yml
|
||||||
|
- docker/init-logto-db.sql
|
||||||
|
- .env.example
|
||||||
|
modified:
|
||||||
|
- src/db/schema.ts
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Logto shares Postgres instance via separate database created by init script"
|
||||||
|
- "OIDC_ISSUER derived from LOGTO_ENDPOINT in docker-compose, not separately configured"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Docker init scripts in docker/ directory mounted to docker-entrypoint-initdb.d"
|
||||||
|
- "OIDC environment variables: LOGTO_ENDPOINT, LOGTO_CLIENT_ID, LOGTO_CLIENT_SECRET, OIDC_AUTH_SECRET"
|
||||||
|
|
||||||
|
requirements-completed: [AUTH-04]
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 3min
|
||||||
|
completed: 2026-04-04
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 15 Plan 01: Logto Docker Infrastructure and Schema Cleanup Summary
|
||||||
|
|
||||||
|
**Logto OIDC provider added to Docker Compose with Postgres init script, users/sessions tables removed from schema**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 3 min
|
||||||
|
- **Started:** 2026-04-04T18:35:52Z
|
||||||
|
- **Completed:** 2026-04-04T18:38:52Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 6
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- Added Logto as a Docker Compose service in both production and dev configurations with proper health-check dependency on Postgres
|
||||||
|
- Created Postgres init script that automatically creates the logto database on first boot
|
||||||
|
- Removed users and sessions tables from GearBox schema, generated Drizzle migration to drop them
|
||||||
|
- Documented all required OIDC environment variables in .env.example
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Add Logto service to Docker Compose and create init script** - `625862f` (feat)
|
||||||
|
2. **Task 2: Remove users and sessions tables from schema** - `0fe231f` (feat)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `docker-compose.yml` - Production compose with Postgres, Logto, and app services
|
||||||
|
- `docker-compose.dev.yml` - Dev compose with Postgres and Logto for local auth testing
|
||||||
|
- `docker/init-logto-db.sql` - SQL script creating separate logto database on Postgres
|
||||||
|
- `.env.example` - Documents all required environment variables for OIDC configuration
|
||||||
|
- `src/db/schema.ts` - Removed users and sessions table definitions
|
||||||
|
- `drizzle/0010_foamy_marvel_zombies.sql` - Migration to drop users and sessions tables
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- Logto shares the same Postgres instance but uses a separate database (created by init script), rather than a dedicated Postgres container
|
||||||
|
- OIDC_ISSUER is derived from LOGTO_ENDPOINT in docker-compose.yml rather than being a separate top-level env var, reducing configuration duplication
|
||||||
|
- Dev compose uses hardcoded password for Logto DB connection (matching existing dev Postgres pattern)
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
None.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
None - no external service configuration required. Logto admin console setup (creating OIDC application, obtaining client ID/secret) will be needed before plan 15-02, but is handled as part of the Logto first-boot experience at http://localhost:3002.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
- Logto infrastructure is ready for plan 15-02 (server-side OIDC integration)
|
||||||
|
- Schema is cleaned of old auth tables, ready for OIDC-based authentication
|
||||||
|
- API keys table preserved for continued programmatic access
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 15-external-authentication*
|
||||||
|
*Completed: 2026-04-04*
|
||||||
@@ -9,11 +9,27 @@ services:
|
|||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata-dev:/var/lib/postgresql/data
|
- pgdata-dev:/var/lib/postgresql/data
|
||||||
|
- ./docker/init-logto-db.sql:/docker-entrypoint-initdb.d/init-logto-db.sql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U gearbox"]
|
test: ["CMD-SHELL", "pg_isready -U gearbox"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
logto:
|
||||||
|
image: svhd/logto:latest
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
entrypoint: ["sh", "-c", "npm run cli db seed -- --swe && npm start"]
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
- "3002:3002"
|
||||||
|
environment:
|
||||||
|
TRUST_PROXY_HEADER: "1"
|
||||||
|
DB_URL: postgres://gearbox:gearbox@postgres:5432/logto
|
||||||
|
ENDPOINT: ${LOGTO_ENDPOINT:-http://localhost:3001}
|
||||||
|
ADMIN_ENDPOINT: ${LOGTO_ADMIN_ENDPOINT:-http://localhost:3002}
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata-dev:
|
pgdata-dev:
|
||||||
|
|||||||
@@ -7,22 +7,44 @@ services:
|
|||||||
POSTGRES_DB: gearbox
|
POSTGRES_DB: gearbox
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
- ./docker/init-logto-db.sql:/docker-entrypoint-initdb.d/init-logto-db.sql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U gearbox"]
|
test: ["CMD-SHELL", "pg_isready -U gearbox"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
logto:
|
||||||
|
image: svhd/logto:latest
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
entrypoint: ["sh", "-c", "npm run cli db seed -- --swe && npm start"]
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
- "3002:3002"
|
||||||
|
environment:
|
||||||
|
TRUST_PROXY_HEADER: "1"
|
||||||
|
DB_URL: postgres://gearbox:${POSTGRES_PASSWORD}@postgres:5432/logto
|
||||||
|
ENDPOINT: ${LOGTO_ENDPOINT:-http://localhost:3001}
|
||||||
|
ADMIN_ENDPOINT: ${LOGTO_ADMIN_ENDPOINT:-http://localhost:3002}
|
||||||
|
|
||||||
app:
|
app:
|
||||||
image: gearbox:latest
|
image: gearbox:latest
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql://gearbox:${POSTGRES_PASSWORD}@postgres:5432/gearbox
|
DATABASE_URL: postgresql://gearbox:${POSTGRES_PASSWORD}@postgres:5432/gearbox
|
||||||
GEARBOX_URL: ${GEARBOX_URL}
|
GEARBOX_URL: ${GEARBOX_URL}
|
||||||
|
OIDC_ISSUER: ${LOGTO_ENDPOINT:-http://localhost:3001}/oidc
|
||||||
|
OIDC_CLIENT_ID: ${LOGTO_CLIENT_ID}
|
||||||
|
OIDC_CLIENT_SECRET: ${LOGTO_CLIENT_SECRET}
|
||||||
|
OIDC_AUTH_SECRET: ${OIDC_AUTH_SECRET}
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
logto:
|
||||||
|
condition: service_started
|
||||||
volumes:
|
volumes:
|
||||||
- uploads:/app/uploads
|
- uploads:/app/uploads
|
||||||
|
|
||||||
|
|||||||
2
docker/init-logto-db.sql
Normal file
2
docker/init-logto-db.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- Creates a separate database for Logto on the shared Postgres instance
|
||||||
|
CREATE DATABASE logto;
|
||||||
2
drizzle/0010_foamy_marvel_zombies.sql
Normal file
2
drizzle/0010_foamy_marvel_zombies.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
DROP TABLE `sessions`;--> statement-breakpoint
|
||||||
|
DROP TABLE `users`;
|
||||||
@@ -71,6 +71,13 @@
|
|||||||
"when": 1775287060443,
|
"when": 1775287060443,
|
||||||
"tag": "0009_happy_mockingbird",
|
"tag": "0009_happy_mockingbird",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1775327900426,
|
||||||
|
"tag": "0010_foamy_marvel_zombies",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
131
src/db/schema.ts
131
src/db/schema.ts
@@ -1,24 +1,18 @@
|
|||||||
import {
|
import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
boolean,
|
|
||||||
doublePrecision,
|
|
||||||
integer,
|
|
||||||
pgTable,
|
|
||||||
serial,
|
|
||||||
text,
|
|
||||||
timestamp,
|
|
||||||
} from "drizzle-orm/pg-core";
|
|
||||||
|
|
||||||
export const categories = pgTable("categories", {
|
export const categories = sqliteTable("categories", {
|
||||||
id: serial("id").primaryKey(),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
name: text("name").notNull().unique(),
|
name: text("name").notNull().unique(),
|
||||||
icon: text("icon").notNull().default("package"),
|
icon: text("icon").notNull().default("package"),
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const items = pgTable("items", {
|
export const items = sqliteTable("items", {
|
||||||
id: serial("id").primaryKey(),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
weightGrams: doublePrecision("weight_grams"),
|
weightGrams: real("weight_grams"),
|
||||||
priceCents: integer("price_cents"),
|
priceCents: integer("price_cents"),
|
||||||
categoryId: integer("category_id")
|
categoryId: integer("category_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -28,29 +22,37 @@ export const items = pgTable("items", {
|
|||||||
imageFilename: text("image_filename"),
|
imageFilename: text("image_filename"),
|
||||||
imageSourceUrl: text("image_source_url"),
|
imageSourceUrl: text("image_source_url"),
|
||||||
quantity: integer("quantity").notNull().default(1),
|
quantity: integer("quantity").notNull().default(1),
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const threads = pgTable("threads", {
|
export const threads = sqliteTable("threads", {
|
||||||
id: serial("id").primaryKey(),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
status: text("status").notNull().default("active"),
|
status: text("status").notNull().default("active"),
|
||||||
resolvedCandidateId: integer("resolved_candidate_id"),
|
resolvedCandidateId: integer("resolved_candidate_id"),
|
||||||
categoryId: integer("category_id")
|
categoryId: integer("category_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => categories.id),
|
.references(() => categories.id),
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const threadCandidates = pgTable("thread_candidates", {
|
export const threadCandidates = sqliteTable("thread_candidates", {
|
||||||
id: serial("id").primaryKey(),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
threadId: integer("thread_id")
|
threadId: integer("thread_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => threads.id, { onDelete: "cascade" }),
|
.references(() => threads.id, { onDelete: "cascade" }),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
weightGrams: doublePrecision("weight_grams"),
|
weightGrams: real("weight_grams"),
|
||||||
priceCents: integer("price_cents"),
|
priceCents: integer("price_cents"),
|
||||||
categoryId: integer("category_id")
|
categoryId: integer("category_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -62,20 +64,28 @@ export const threadCandidates = pgTable("thread_candidates", {
|
|||||||
status: text("status").notNull().default("researching"),
|
status: text("status").notNull().default("researching"),
|
||||||
pros: text("pros"),
|
pros: text("pros"),
|
||||||
cons: text("cons"),
|
cons: text("cons"),
|
||||||
sortOrder: doublePrecision("sort_order").notNull().default(0),
|
sortOrder: real("sort_order").notNull().default(0),
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setups = pgTable("setups", {
|
export const setups = sqliteTable("setups", {
|
||||||
id: serial("id").primaryKey(),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupItems = pgTable("setup_items", {
|
export const setupItems = sqliteTable("setup_items", {
|
||||||
id: serial("id").primaryKey(),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
setupId: integer("setup_id")
|
setupId: integer("setup_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => setups.id, { onDelete: "cascade" }),
|
.references(() => setups.id, { onDelete: "cascade" }),
|
||||||
@@ -85,59 +95,52 @@ export const setupItems = pgTable("setup_items", {
|
|||||||
classification: text("classification").notNull().default("base"),
|
classification: text("classification").notNull().default("base"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const settings = pgTable("settings", {
|
export const settings = sqliteTable("settings", {
|
||||||
key: text("key").primaryKey(),
|
key: text("key").primaryKey(),
|
||||||
value: text("value").notNull(),
|
value: text("value").notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const users = pgTable("users", {
|
export const apiKeys = sqliteTable("api_keys", {
|
||||||
id: serial("id").primaryKey(),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
username: text("username").notNull().unique(),
|
|
||||||
passwordHash: text("password_hash").notNull(),
|
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const sessions = pgTable("sessions", {
|
|
||||||
id: text("id").primaryKey(),
|
|
||||||
userId: integer("user_id")
|
|
||||||
.notNull()
|
|
||||||
.references(() => users.id, { onDelete: "cascade" }),
|
|
||||||
expiresAt: timestamp("expires_at").notNull(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const apiKeys = pgTable("api_keys", {
|
|
||||||
id: serial("id").primaryKey(),
|
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
keyHash: text("key_hash").notNull(),
|
keyHash: text("key_hash").notNull(),
|
||||||
keyPrefix: text("key_prefix").notNull(),
|
keyPrefix: text("key_prefix").notNull(),
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const oauthClients = pgTable("oauth_clients", {
|
export const oauthClients = sqliteTable("oauth_clients", {
|
||||||
id: serial("id").primaryKey(),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
clientId: text("client_id").notNull().unique(),
|
clientId: text("client_id").notNull().unique(),
|
||||||
clientName: text("client_name"),
|
clientName: text("client_name"),
|
||||||
redirectUris: text("redirect_uris").notNull(), // JSON array
|
redirectUris: text("redirect_uris").notNull(), // JSON array
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const oauthCodes = pgTable("oauth_codes", {
|
export const oauthCodes = sqliteTable("oauth_codes", {
|
||||||
id: serial("id").primaryKey(),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
code: text("code").notNull().unique(),
|
code: text("code").notNull().unique(),
|
||||||
clientId: text("client_id").notNull(),
|
clientId: text("client_id").notNull(),
|
||||||
codeChallenge: text("code_challenge").notNull(),
|
codeChallenge: text("code_challenge").notNull(),
|
||||||
codeChallengeMethod: text("code_challenge_method").notNull().default("S256"),
|
codeChallengeMethod: text("code_challenge_method").notNull().default("S256"),
|
||||||
redirectUri: text("redirect_uri").notNull(),
|
redirectUri: text("redirect_uri").notNull(),
|
||||||
expiresAt: timestamp("expires_at").notNull(),
|
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
||||||
used: boolean("used").notNull().default(false),
|
used: integer("used").notNull().default(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const oauthTokens = pgTable("oauth_tokens", {
|
export const oauthTokens = sqliteTable("oauth_tokens", {
|
||||||
id: serial("id").primaryKey(),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
accessTokenHash: text("access_token_hash").notNull().unique(),
|
accessTokenHash: text("access_token_hash").notNull().unique(),
|
||||||
refreshTokenHash: text("refresh_token_hash").notNull().unique(),
|
refreshTokenHash: text("refresh_token_hash").notNull().unique(),
|
||||||
clientId: text("client_id").notNull(),
|
clientId: text("client_id").notNull(),
|
||||||
expiresAt: timestamp("expires_at").notNull(), // access token expiry
|
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), // access token expiry
|
||||||
refreshExpiresAt: timestamp("refresh_expires_at").notNull(), // refresh token expiry
|
refreshExpiresAt: integer("refresh_expires_at", {
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
mode: "timestamp",
|
||||||
|
}).notNull(), // refresh token expiry
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user