From c2b1073fef1cc9a0845d10a2a62f4242b4d60276 Mon Sep 17 00:00:00 2001 From: Ryan Moon Date: Wed, 1 Apr 2026 19:50:37 -0500 Subject: [PATCH] feat: add CI/CD pipeline, production Dockerfile, and deployment architecture - Add production Dockerfile with bun build --compile, multi-stage Alpine build - Add .dockerignore - Swap bcrypt -> bcryptjs (pure JS, no native addons) - Add programmatic migrations on startup via drizzle migrator - Add /v1/version endpoint with APP_VERSION baked in at build time - Add .gitea/workflows/ci.yml (lint + test with postgres/valkey services) - Add .gitea/workflows/build.yml (version bump, build, push to registry) - Update CLAUDE.md and docs/architecture.md to remove multi-tenancy - Add docs/deployment.md covering DOKS + ArgoCD architecture Co-Authored-By: Claude Sonnet 4.6 --- .dockerignore | 11 + .gitea/workflows/build.yml | 77 +++++++ .gitea/workflows/ci.yml | 61 ++++++ CLAUDE.md | 3 +- Dockerfile | 32 +++ bun.lock | 12 +- docs/architecture.md | 13 +- docs/deployment.md | 193 ++++++++++++++++++ packages/backend/package.json | 8 +- packages/backend/src/db/seeds/dev-seed.ts | 4 +- packages/backend/src/main.ts | 18 ++ packages/backend/src/plugins/webdav-auth.ts | 2 +- packages/backend/src/routes/v1/auth.ts | 2 +- packages/backend/src/routes/v1/version.ts | 7 + .../backend/src/services/vault.service.ts | 2 +- 15 files changed, 419 insertions(+), 26 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitea/workflows/ci.yml create mode 100644 Dockerfile create mode 100644 docs/deployment.md create mode 100644 packages/backend/src/routes/v1/version.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2ded820 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules +.git +.gitea +docs +planning +deploy +infra +packages/admin +Dockerfile* +docker-compose* +*.md diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..dced202 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,77 @@ +name: Build & Release + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.BOT_TOKEN }} + + - name: Determine version bump + id: bump + run: | + COMMIT_MSG=$(git log -1 --pretty=%s) + if echo "$COMMIT_MSG" | grep -qiE "^breaking(\(.+\))?:|^.+!:"; then + echo "type=major" >> $GITHUB_OUTPUT + elif echo "$COMMIT_MSG" | grep -qiE "^feat(\(.+\))?:"; then + echo "type=minor" >> $GITHUB_OUTPUT + else + echo "type=patch" >> $GITHUB_OUTPUT + fi + + - name: Bump version in package.json + id: version + run: | + cd packages/backend + npm version ${{ steps.bump.outputs.type }} --no-git-tag-version + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Commit version bump + run: | + git config user.name "lunarfront-bot" + git config user.email "bot@lunarfront.tech" + git remote set-url origin https://lunarfront-bot:${{ secrets.BOT_TOKEN }}@git.lunarfront.tech/ryan/lunarfront-app.git + git add packages/backend/package.json + git commit -m "chore: bump version to v${{ steps.version.outputs.version }}" + git push origin main + + - name: Install Docker CLI + run: | + apt-get update -qq + apt-get install -y ca-certificates curl + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" > /etc/apt/sources.list.d/docker.list + apt-get update -qq + apt-get install -y docker-ce-cli + + - name: Login to registry + run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login registry.lunarfront.tech -u ryan --password-stdin + + - name: Build and push + run: | + VERSION=${{ steps.version.outputs.version }} + SHA=$(git rev-parse --short HEAD) + docker build \ + --build-arg APP_VERSION=$VERSION \ + -t registry.lunarfront.tech/ryan/lunarfront-app:$VERSION \ + -t registry.lunarfront.tech/ryan/lunarfront-app:$SHA \ + -t registry.lunarfront.tech/ryan/lunarfront-app:latest \ + . + docker push registry.lunarfront.tech/ryan/lunarfront-app:$VERSION + docker push registry.lunarfront.tech/ryan/lunarfront-app:$SHA + docker push registry.lunarfront.tech/ryan/lunarfront-app:latest + + - name: Logout + if: always() + run: docker logout registry.lunarfront.tech diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..161c27b --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + ci: + runs-on: ubuntu-latest + container: + image: registry.lunarfront.tech/ryan/ci-runner:latest + credentials: + username: ryan + password: ${{ secrets.REGISTRY_TOKEN }} + + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: lunarfront + POSTGRES_PASSWORD: lunarfront + POSTGRES_DB: lunarfront_test + options: >- + --health-cmd pg_isready + --health-interval 5s + --health-timeout 3s + --health-retries 5 + + valkey: + image: valkey/valkey:8 + options: >- + --health-cmd "valkey-cli ping" + --health-interval 5s + --health-timeout 3s + --health-retries 5 + + env: + DATABASE_URL: postgresql://lunarfront:lunarfront@postgres:5432/lunarfront_test + REDIS_URL: redis://valkey:6379 + JWT_SECRET: ci-secret + NODE_ENV: test + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Lint + run: bun run lint + + - name: Run migrations + working-directory: packages/backend + run: bunx drizzle-kit migrate + + - name: Test + run: bun run test diff --git a/CLAUDE.md b/CLAUDE.md index b9b26c1..e4d1d80 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,8 +24,7 @@ ## Database - Dev: `lunarfront` on localhost:5432 - Test: `lunarfront_test` on localhost:5432 -- Multi-tenant: `company_id` (uuid FK) on all domain tables for tenant isolation -- `location_id` (uuid FK) on tables that need per-location scoping (inventory, transactions, drawer) +- Each deployed instance has its own isolated database — no multi-tenancy, no `company_id` - Migrations via Drizzle Kit (`bunx drizzle-kit generate`, `bunx drizzle-kit migrate`) ## Key Entity Names diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a59a215 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM oven/bun:1.3.11-alpine AS deps +WORKDIR /app +COPY package.json bun.lock ./ +COPY packages/shared/package.json packages/shared/ +COPY packages/backend/package.json packages/backend/ +RUN bun install --frozen-lockfile + +FROM oven/bun:1.3.11-alpine AS build +ARG APP_VERSION=dev +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules +COPY --from=deps /app/packages/backend/node_modules ./packages/backend/node_modules +COPY packages/shared ./packages/shared +COPY packages/backend ./packages/backend +COPY package.json ./ +COPY tsconfig.base.json ./ +WORKDIR /app/packages/backend +RUN bun build src/main.ts --compile --outfile /app/server \ + --define "process.env.APP_VERSION='${APP_VERSION}'" + +FROM alpine:3.21 +RUN addgroup -S app && adduser -S app -G app +WORKDIR /app +COPY --from=build /app/server ./server +COPY --from=build /app/packages/backend/src/db/migrations ./migrations +ENV MIGRATIONS_DIR=/app/migrations +EXPOSE 8000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD wget -qO- http://localhost:8000/v1/health || exit 1 +USER app +CMD ["./server"] diff --git a/bun.lock b/bun.lock index a584e4a..196fc64 100644 --- a/bun.lock +++ b/bun.lock @@ -66,7 +66,8 @@ "@fastify/multipart": "^9.4.0", "@fastify/rate-limit": "^10.3.0", "@lunarfront/shared": "workspace:*", - "bcrypt": "^6", + "@types/bcryptjs": "^3.0.0", + "bcryptjs": "^3.0.3", "drizzle-orm": "^0.38", "fastify": "^5", "fastify-plugin": "^5", @@ -75,7 +76,6 @@ "zod": "^4", }, "devDependencies": { - "@types/bcrypt": "^5", "@types/node": "^22", "drizzle-kit": "^0.30", "pino-pretty": "^13", @@ -530,7 +530,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/bcrypt": ["@types/bcrypt@5.0.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ=="], + "@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="], "@types/date-arithmetic": ["@types/date-arithmetic@4.1.4", "", {}, "sha512-p9eZ2X9B80iKiTW4ukVj8B4K6q9/+xFtQ5MGYA5HWToY9nL4EkhV9+6ftT2VHpVMEZb5Tv00Iel516bVdO+yRw=="], @@ -614,7 +614,7 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.12", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ=="], - "bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="], + "bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], @@ -928,10 +928,6 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="], - - "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], - "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], diff --git a/docs/architecture.md b/docs/architecture.md index 430c9f3..9cd45cd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -56,11 +56,10 @@ src/ ### Request Flow 1. Fastify receives request -2. `onRequest` hook sets `companyId` from header -3. `authenticate` preHandler verifies JWT, loads permissions, checks `is_active` -4. `requirePermission` preHandler checks user has required permission slug -5. Route handler validates input with Zod, calls service, returns response -6. Error handler catches typed errors and maps to HTTP status codes +2. `authenticate` preHandler verifies JWT, loads permissions, checks `is_active` +3. `requirePermission` preHandler checks user has required permission slug +4. Route handler validates input with Zod, calls service, returns response +5. Error handler catches typed errors and maps to HTTP status codes ### Permission Inheritance @@ -109,9 +108,9 @@ src/ | Theme | Zustand store, persisted to localStorage | | Component state | React `useState` | -## Multi-Tenancy +## Deployment Model -Every domain table has a `company_id` column. All queries filter by the authenticated user's company. Location-scoped tables (inventory, transactions) additionally filter by `location_id`. +Each customer runs as a fully isolated deployment — their own Kubernetes namespace on DOKS, their own database on the shared managed Postgres instance. There is no multi-tenancy in the application layer. No `company_id`, no row-level isolation. One instance = one customer. ## Database diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..6e3da86 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,193 @@ +# Infrastructure & Deployment Architecture + +## Overview + +LunarFront runs on DigitalOcean. Each customer is a fully isolated deployment — their own Kubernetes namespace, their own database. There is no multi-tenancy in the application layer. + +**Stack:** DOKS (Kubernetes) · ArgoCD · Helm · Gitea · Terraform · Ansible + +The guiding principle is simplicity — Docker Compose on Droplets for Gitea, Kubernetes for customer app instances. No surprise bills. + +--- + +## Monthly Cost + +| Resource | Spec | Cost | +|---|---|---| +| DOKS cluster | Control plane | $12/mo | +| DOKS nodes (2x s-2vcpu-4gb) | Runs all customer apps | ~$48/mo | +| Managed Postgres (shared) | All customer databases | $15-25/mo | +| Managed Redis (shared) | All customer queues/cache | $15/mo | +| Gitea Droplet | 1 vCPU, 1GB RAM | $6/mo | +| Gitea Postgres | Dedicated managed Postgres | $15/mo | +| Spaces | Registry, backups, files | ~$21/mo | +| **Fixed total** | | **~$132-142/mo** | +| **Per customer** | New database in shared Postgres | **~$0 marginal** | + +--- + +## Environments + +| Environment | Hostname | Purpose | +|---|---|---| +| Production (per customer) | `{customer}.app.lunarfront.tech` | Live customer instance on DOKS | +| Gitea | `git.lunarfront.tech` | Source control, CI/CD, container registry | +| Dev | Local / feature namespace on DOKS | Testing and staging | + +--- + +## DNS + +Managed through Cloudflare. Records defined in Terraform. + +| Record | Type | Points To | +|---|---|---| +| `git.lunarfront.tech` | A | Gitea Droplet | +| `git-ssh.lunarfront.tech` | A | Gitea Droplet (SSH port 2222) | +| `registry.lunarfront.tech` | A | Gitea Droplet (container registry) | +| `{customer}.app.lunarfront.tech` | CNAME | DOKS load balancer | + +--- + +## Infrastructure (Terraform) + +Terraform manages all DigitalOcean and Cloudflare resources. State stored in Spaces. + +``` +/terraform + main.tf providers, backend (Spaces state) + variables.tf + cluster.tf DOKS cluster + node pools + databases.tf shared Postgres, shared Redis + gitea.tf Gitea Droplet + spaces.tf files, backups, tf-state buckets + dns.tf Cloudflare DNS records + outputs.tf cluster endpoint, DB URLs + terraform.tfvars secrets (gitignored) +``` + +### State Backend + +```hcl +terraform { + backend "s3" { + endpoint = "https://nyc3.digitaloceanspaces.com" + bucket = "lunarfront-terraform-state" + key = "terraform.tfstate" + region = "us-east-1" + skip_credentials_validation = true + skip_metadata_api_check = true + skip_region_validation = true + force_path_style = true + } +} +``` + +--- + +## Gitea (Source Control + CI/CD) + +Gitea runs on its own Droplet with a dedicated managed Postgres. It hosts: +- Source code repositories +- Gitea Actions CI/CD pipelines +- Docker container registry (`registry.lunarfront.tech`) + +Managed by Ansible. See `lunarfront-infra/ansible/roles/gitea/`. + +### CI Pipeline (Gitea Actions) + +On push to `main`: + +1. Run lint + unit tests using the shared `ci-runner` image +2. Build Docker image, push to `registry.lunarfront.tech/ryan/lunarfront-app:{sha}` +3. Update the Helm chart `values.yaml` with the new image tag +4. ArgoCD detects the change and syncs all customer deployments + +--- + +## Kubernetes (DOKS + ArgoCD) + +All customer app instances run on a single DOKS cluster managed by ArgoCD (GitOps). + +### Repository Structure + +``` +/gitops + apps/ + customer-acme/ + values.yaml + customer-foo/ + values.yaml + chart/ + Chart.yaml + templates/ + deployment.yaml + service.yaml + ingress.yaml + job-migrate.yaml # runs drizzle-kit migrate on deploy + secret.yaml +``` + +### Per-Customer values.yaml + +```yaml +customer: acme +subdomain: acme + +image: + repository: registry.lunarfront.tech/ryan/lunarfront-app + tag: "abc123" + +database: + url: "postgresql://lunarfront:pass@db-host:25060/customer_acme?sslmode=require" + +redis: + url: "rediss://..." + +env: + JWT_SECRET: "..." + NODE_ENV: production +``` + +### Adding a New Customer + +1. `CREATE DATABASE customer_x;` on the shared managed Postgres +2. Add `gitops/apps/customer-x/values.yaml` +3. Push — ArgoCD syncs, migration Job runs, instance is live +4. Add DNS CNAME in Terraform + +--- + +## Database Strategy + +One managed Postgres cluster shared across all customers. Each customer gets their own isolated database (`CREATE DATABASE customer_x`). No cross-customer queries are possible at the database level. + +- New customer = new database, no new infrastructure cost +- Managed DO Postgres handles backups, failover, and SSL +- Resize the cluster as total load grows + +--- + +## Day-to-Day Workflow + +```bash +# Provision infrastructure from scratch +terraform init && terraform apply + +# Configure Gitea server +ansible-playbook ansible/gitea.yml -i ansible/inventory.ini + +# Normal deploy flow — push to main +git push origin main +# → Gitea Actions: lint + test + build image +# → Image pushed to registry +# → Helm chart values updated +# → ArgoCD syncs all customer deployments automatically + +# Add a new customer +# 1. Create database +psql $DATABASE_URL -c "CREATE DATABASE customer_x;" +# 2. Add values file, push +git add gitops/apps/customer-x/values.yaml && git push +# Done — ArgoCD handles the rest +``` diff --git a/packages/backend/package.json b/packages/backend/package.json index 3b4b350..86b96e1 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -23,7 +23,8 @@ "@fastify/multipart": "^9.4.0", "@fastify/rate-limit": "^10.3.0", "@lunarfront/shared": "workspace:*", - "bcrypt": "^6", + "@types/bcryptjs": "^3.0.0", + "bcryptjs": "^3.0.3", "drizzle-orm": "^0.38", "fastify": "^5", "fastify-plugin": "^5", @@ -32,10 +33,9 @@ "zod": "^4" }, "devDependencies": { - "typescript": "^5", + "@types/node": "^22", "drizzle-kit": "^0.30", "pino-pretty": "^13", - "@types/node": "^22", - "@types/bcrypt": "^5" + "typescript": "^5" } } diff --git a/packages/backend/src/db/seeds/dev-seed.ts b/packages/backend/src/db/seeds/dev-seed.ts index 106227e..1fb15ef 100644 --- a/packages/backend/src/db/seeds/dev-seed.ts +++ b/packages/backend/src/db/seeds/dev-seed.ts @@ -42,8 +42,8 @@ async function seed() { const adminPassword = process.env.ADMIN_PASSWORD ?? 'admin1234' const [adminUser] = await sql`SELECT id FROM "user" WHERE email = 'admin@lunarfront.dev'` if (!adminUser) { - const bcrypt = await import('bcrypt') - const hashedPw = await (bcrypt.default || bcrypt).hash(adminPassword, 10) + const bcrypt = await import('bcryptjs') + const hashedPw = await bcrypt.hash(adminPassword, 10) const [user] = await sql`INSERT INTO "user" (email, password_hash, first_name, last_name, role) VALUES ('admin@lunarfront.dev', ${hashedPw}, 'Admin', 'User', 'admin') RETURNING id` const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1` if (adminRole) { diff --git a/packages/backend/src/main.ts b/packages/backend/src/main.ts index 81c7b69..302a2aa 100644 --- a/packages/backend/src/main.ts +++ b/packages/backend/src/main.ts @@ -1,4 +1,7 @@ import Fastify from 'fastify' +import { drizzle } from 'drizzle-orm/postgres-js' +import { migrate } from 'drizzle-orm/postgres-js/migrator' +import postgres from 'postgres' import rateLimit from '@fastify/rate-limit' import { databasePlugin } from './plugins/database.js' import { redisPlugin } from './plugins/redis.js' @@ -8,6 +11,7 @@ import { authPlugin } from './plugins/auth.js' import { devAuthPlugin } from './plugins/dev-auth.js' import { storagePlugin } from './plugins/storage.js' import { healthRoutes } from './routes/v1/health.js' +import { versionRoutes } from './routes/v1/version.js' import { authRoutes } from './routes/v1/auth.js' import { accountRoutes } from './routes/v1/accounts.js' import { inventoryRoutes } from './routes/v1/inventory.js' @@ -92,6 +96,7 @@ export async function buildApp() { // Core routes — always available await app.register(healthRoutes, { prefix: '/v1' }) + await app.register(versionRoutes, { prefix: '/v1' }) await app.register(authRoutes, { prefix: '/v1' }) await app.register(accountRoutes, { prefix: '/v1' }) await app.register(rbacRoutes, { prefix: '/v1' }) @@ -138,7 +143,20 @@ export async function buildApp() { return app } +async function runMigrations() { + const connectionString = process.env.DATABASE_URL + if (!connectionString) throw new Error('DATABASE_URL is required') + + const migrationsFolder = process.env.MIGRATIONS_DIR ?? './src/db/migrations' + const sql = postgres(connectionString, { max: 1 }) + const db = drizzle(sql) + + await migrate(db, { migrationsFolder }) + await sql.end() +} + async function start() { + await runMigrations() const app = await buildApp() const port = parseInt(process.env.PORT ?? '8000', 10) diff --git a/packages/backend/src/plugins/webdav-auth.ts b/packages/backend/src/plugins/webdav-auth.ts index b469274..b6a9957 100644 --- a/packages/backend/src/plugins/webdav-auth.ts +++ b/packages/backend/src/plugins/webdav-auth.ts @@ -1,5 +1,5 @@ import { eq } from 'drizzle-orm' -import bcrypt from 'bcrypt' +import bcrypt from 'bcryptjs' import { users } from '../db/schema/users.js' import { RbacService } from '../services/rbac.service.js' import type { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify' diff --git a/packages/backend/src/routes/v1/auth.ts b/packages/backend/src/routes/v1/auth.ts index 5a37408..fdfbc07 100644 --- a/packages/backend/src/routes/v1/auth.ts +++ b/packages/backend/src/routes/v1/auth.ts @@ -1,6 +1,6 @@ import type { FastifyPluginAsync } from 'fastify' import { eq } from 'drizzle-orm' -import bcrypt from 'bcrypt' +import bcrypt from 'bcryptjs' import { RegisterSchema, LoginSchema } from '@lunarfront/shared/schemas' import { users } from '../../db/schema/users.js' diff --git a/packages/backend/src/routes/v1/version.ts b/packages/backend/src/routes/v1/version.ts new file mode 100644 index 0000000..a5d512b --- /dev/null +++ b/packages/backend/src/routes/v1/version.ts @@ -0,0 +1,7 @@ +import type { FastifyPluginAsync } from 'fastify' + +export const versionRoutes: FastifyPluginAsync = async (app) => { + app.get('/version', async (_request, reply) => { + reply.send({ version: process.env.APP_VERSION ?? 'dev' }) + }) +} diff --git a/packages/backend/src/services/vault.service.ts b/packages/backend/src/services/vault.service.ts index 3dd9a29..bc5cc64 100644 --- a/packages/backend/src/services/vault.service.ts +++ b/packages/backend/src/services/vault.service.ts @@ -5,7 +5,7 @@ import { userRoles } from '../db/schema/rbac.js' import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../utils/pagination.js' import type { PaginationInput } from '@lunarfront/shared/schemas' import { randomBytes, createCipheriv, createDecipheriv, pbkdf2Sync } from 'crypto' -import bcrypt from 'bcrypt' +import bcrypt from 'bcryptjs' // --- Encryption key held in memory --- let derivedKey: Buffer | null = null