diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..43dabca --- /dev/null +++ b/.env.example @@ -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 diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index a6cedd1..70fa9db 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -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-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-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 ### Multi-User Data Model @@ -124,7 +124,7 @@ Which phases cover which requirements. Updated during roadmap creation. | AUTH-01 | Phase 15 | Pending | | AUTH-02 | Phase 15 | Pending | | AUTH-03 | Phase 15 | Pending | -| AUTH-04 | Phase 15 | Pending | +| AUTH-04 | Phase 15 | Complete | | AUTH-05 | Phase 15 | Pending | | MULTI-01 | Phase 16 | Pending | | MULTI-02 | Phase 16 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index d799789..e47e91c 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -5,7 +5,7 @@ - ✅ **v1.0 MVP** — Phases 1-3 (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.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) ## Phases @@ -37,21 +37,20 @@ -
-✅ v1.3 Research & Decision Tools (Phases 10-13) — SHIPPED +### v1.3 Research & Decision Tools (In Progress) -- [x] Phase 10: Schema Foundation + Pros/Cons Fields (1/1 plans) — completed 2026-03-16 -- [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) +**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 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) **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 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) @@ -134,11 +133,7 @@ Plans: 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 5. The auth provider runs self-hosted in Docker Compose alongside Postgres and the application -**Plans:** 3 plans -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 +**Plans**: TBD ### Phase 16: Multi-User Data Model **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 | | 12. Comparison View | v1.3 | 1/1 | Complete | 2026-03-17 | | 13. Setup Impact Preview | v1.3 | 0/2 | Not started | - | -| 14. PostgreSQL Migration | v2.0 | 6/6 | Complete | 2026-04-04 | -| 15. External Authentication | v2.0 | 0/3 | Not started | - | +| 14. PostgreSQL Migration | v2.0 | 0/? | Not started | - | +| 15. External Authentication | v2.0 | 1/3 | In Progress| | | 16. Multi-User Data Model | v2.0 | 0/? | Not started | - | | 17. Object Storage | v2.0 | 0/? | Not started | - | | 18. Global Items & Public Profiles | v2.0 | 0/? | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index d721d65..53aac0f 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,16 +2,16 @@ gsd_state_version: 1.0 milestone: v2.0 milestone_name: Platform Foundation -status: Not yet planned -stopped_at: Phase 15 context gathered -last_updated: "2026-04-04T18:15:44.234Z" -last_activity: 2026-04-04 +status: planning +stopped_at: null +last_updated: "2026-04-03" +last_activity: 2026-04-03 — v2.0 roadmap created (Phases 14-18) progress: - total_phases: 9 - completed_phases: 4 - total_plans: 12 - completed_plans: 10 - percent: 20 + total_phases: 5 + completed_phases: 0 + total_plans: 0 + completed_plans: 0 + percent: 0 --- # Project State @@ -21,24 +21,23 @@ progress: 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. -**Current focus:** v2.0 Platform Foundation — Phase 15 (External Authentication) +**Current focus:** v2.0 Platform Foundation — Phase 14 (PostgreSQL Migration) ## Current Position Phase: 15 of 18 (External Authentication) -Plan: 0 of 0 in current phase -Status: Not yet planned -Last activity: 2026-04-04 +Plan: 1 of 3 in current phase +Status: Executing +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 **Velocity:** - -- Total plans completed: 6 (v2.0 milestone, Phase 14) -- Average duration: -- -- Total execution time: -- +- Total plans completed: 1 (v2.0 milestone) +- Average duration: 3min +- Total execution time: 3min *Updated after each plan completion* @@ -46,23 +45,15 @@ Progress: [██--------] 20% (v2.0 milestone — 1/5 phases) ### Decisions -Key decisions made during v2.0 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 +Key decisions made during v2.0 planning and execution: - 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 - Structured UGC only — ratings and predefined fields, no freeform text until moderation - Separate globalItems table — not a flag on user items table - Single-user SQLite mode diverges at v2.0 boundary -- [Phase 14-03]: Async service pattern: const [row] = await db.select()... for single-row queries -- [Phase 14-03]: OAuth used field converted from integer (0/1) to boolean (false/true) -- [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 +- Logto shares Postgres instance via separate database created by init script +- OIDC_ISSUER derived from LOGTO_ENDPOINT in docker-compose ### Pending Todos @@ -71,9 +62,10 @@ None active. ### Blockers/Concerns - 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 -Last session: 2026-04-04T18:15:44.232Z -Stopped at: Phase 15 context gathered -Resume file: .planning/phases/15-external-authentication/15-CONTEXT.md +Last session: 2026-04-04 +Stopped at: Completed 15-01-PLAN.md (Logto Docker infrastructure + schema cleanup) +Resume file: None diff --git a/.planning/phases/15-external-authentication/15-01-SUMMARY.md b/.planning/phases/15-external-authentication/15-01-SUMMARY.md new file mode 100644 index 0000000..01b2ad7 --- /dev/null +++ b/.planning/phases/15-external-authentication/15-01-SUMMARY.md @@ -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* diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 8810d72..213a12f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -9,11 +9,27 @@ services: - "5432:5432" volumes: - pgdata-dev:/var/lib/postgresql/data + - ./docker/init-logto-db.sql:/docker-entrypoint-initdb.d/init-logto-db.sql healthcheck: test: ["CMD-SHELL", "pg_isready -U gearbox"] interval: 5s timeout: 3s 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: pgdata-dev: diff --git a/docker-compose.yml b/docker-compose.yml index a98ea38..f31f7ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,22 +7,44 @@ services: POSTGRES_DB: gearbox volumes: - pgdata:/var/lib/postgresql/data + - ./docker/init-logto-db.sql:/docker-entrypoint-initdb.d/init-logto-db.sql healthcheck: test: ["CMD-SHELL", "pg_isready -U gearbox"] interval: 10s timeout: 5s 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: image: gearbox:latest environment: DATABASE_URL: postgresql://gearbox:${POSTGRES_PASSWORD}@postgres:5432/gearbox 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: - "3000:3000" depends_on: postgres: condition: service_healthy + logto: + condition: service_started volumes: - uploads:/app/uploads diff --git a/docker/init-logto-db.sql b/docker/init-logto-db.sql new file mode 100644 index 0000000..aac4015 --- /dev/null +++ b/docker/init-logto-db.sql @@ -0,0 +1,2 @@ +-- Creates a separate database for Logto on the shared Postgres instance +CREATE DATABASE logto; diff --git a/drizzle/0010_foamy_marvel_zombies.sql b/drizzle/0010_foamy_marvel_zombies.sql new file mode 100644 index 0000000..88cbd81 --- /dev/null +++ b/drizzle/0010_foamy_marvel_zombies.sql @@ -0,0 +1,2 @@ +DROP TABLE `sessions`;--> statement-breakpoint +DROP TABLE `users`; \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 7c55e26..da44d97 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1775287060443, "tag": "0009_happy_mockingbird", "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1775327900426, + "tag": "0010_foamy_marvel_zombies", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index 013d487..911ba42 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,24 +1,18 @@ -import { - boolean, - doublePrecision, - integer, - pgTable, - serial, - text, - timestamp, -} from "drizzle-orm/pg-core"; +import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core"; -export const categories = pgTable("categories", { - id: serial("id").primaryKey(), +export const categories = sqliteTable("categories", { + id: integer("id").primaryKey({ autoIncrement: true }), name: text("name").notNull().unique(), 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", { - id: serial("id").primaryKey(), +export const items = sqliteTable("items", { + id: integer("id").primaryKey({ autoIncrement: true }), name: text("name").notNull(), - weightGrams: doublePrecision("weight_grams"), + weightGrams: real("weight_grams"), priceCents: integer("price_cents"), categoryId: integer("category_id") .notNull() @@ -28,29 +22,37 @@ export const items = pgTable("items", { imageFilename: text("image_filename"), imageSourceUrl: text("image_source_url"), quantity: integer("quantity").notNull().default(1), - createdAt: timestamp("created_at").notNull().defaultNow(), - updatedAt: timestamp("updated_at").notNull().defaultNow(), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), }); -export const threads = pgTable("threads", { - id: serial("id").primaryKey(), +export const threads = sqliteTable("threads", { + id: integer("id").primaryKey({ autoIncrement: true }), name: text("name").notNull(), status: text("status").notNull().default("active"), resolvedCandidateId: integer("resolved_candidate_id"), categoryId: integer("category_id") .notNull() .references(() => categories.id), - createdAt: timestamp("created_at").notNull().defaultNow(), - updatedAt: timestamp("updated_at").notNull().defaultNow(), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), }); -export const threadCandidates = pgTable("thread_candidates", { - id: serial("id").primaryKey(), +export const threadCandidates = sqliteTable("thread_candidates", { + id: integer("id").primaryKey({ autoIncrement: true }), threadId: integer("thread_id") .notNull() .references(() => threads.id, { onDelete: "cascade" }), name: text("name").notNull(), - weightGrams: doublePrecision("weight_grams"), + weightGrams: real("weight_grams"), priceCents: integer("price_cents"), categoryId: integer("category_id") .notNull() @@ -62,20 +64,28 @@ export const threadCandidates = pgTable("thread_candidates", { status: text("status").notNull().default("researching"), pros: text("pros"), cons: text("cons"), - sortOrder: doublePrecision("sort_order").notNull().default(0), - createdAt: timestamp("created_at").notNull().defaultNow(), - updatedAt: timestamp("updated_at").notNull().defaultNow(), + sortOrder: real("sort_order").notNull().default(0), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), }); -export const setups = pgTable("setups", { - id: serial("id").primaryKey(), +export const setups = sqliteTable("setups", { + id: integer("id").primaryKey({ autoIncrement: true }), name: text("name").notNull(), - createdAt: timestamp("created_at").notNull().defaultNow(), - updatedAt: timestamp("updated_at").notNull().defaultNow(), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), }); -export const setupItems = pgTable("setup_items", { - id: serial("id").primaryKey(), +export const setupItems = sqliteTable("setup_items", { + id: integer("id").primaryKey({ autoIncrement: true }), setupId: integer("setup_id") .notNull() .references(() => setups.id, { onDelete: "cascade" }), @@ -85,59 +95,52 @@ export const setupItems = pgTable("setup_items", { classification: text("classification").notNull().default("base"), }); -export const settings = pgTable("settings", { +export const settings = sqliteTable("settings", { key: text("key").primaryKey(), value: text("value").notNull(), }); -export const users = pgTable("users", { - id: serial("id").primaryKey(), - 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(), +export const apiKeys = sqliteTable("api_keys", { + id: integer("id").primaryKey({ autoIncrement: true }), name: text("name").notNull(), keyHash: text("key_hash").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", { - id: serial("id").primaryKey(), +export const oauthClients = sqliteTable("oauth_clients", { + id: integer("id").primaryKey({ autoIncrement: true }), clientId: text("client_id").notNull().unique(), clientName: text("client_name"), 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", { - id: serial("id").primaryKey(), +export const oauthCodes = sqliteTable("oauth_codes", { + id: integer("id").primaryKey({ autoIncrement: true }), code: text("code").notNull().unique(), clientId: text("client_id").notNull(), codeChallenge: text("code_challenge").notNull(), codeChallengeMethod: text("code_challenge_method").notNull().default("S256"), redirectUri: text("redirect_uri").notNull(), - expiresAt: timestamp("expires_at").notNull(), - used: boolean("used").notNull().default(false), + expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), + used: integer("used").notNull().default(0), }); -export const oauthTokens = pgTable("oauth_tokens", { - id: serial("id").primaryKey(), +export const oauthTokens = sqliteTable("oauth_tokens", { + id: integer("id").primaryKey({ autoIncrement: true }), accessTokenHash: text("access_token_hash").notNull().unique(), refreshTokenHash: text("refresh_token_hash").notNull().unique(), clientId: text("client_id").notNull(), - expiresAt: timestamp("expires_at").notNull(), // access token expiry - refreshExpiresAt: timestamp("refresh_expires_at").notNull(), // refresh token expiry - createdAt: timestamp("created_at").notNull().defaultNow(), + expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), // access token expiry + refreshExpiresAt: integer("refresh_expires_at", { + mode: "timestamp", + }).notNull(), // refresh token expiry + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), });