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 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
7
packages/backend/src/routes/v1/version.ts
Normal file
7
packages/backend/src/routes/v1/version.ts
Normal file
@@ -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' })
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user