import app from "./app"; import { logger } from "./lib/logger"; import bcrypt from "bcryptjs"; import { db, usersTable, toolsTable } from "@workspace/db"; import { sql } from "drizzle-orm"; const rawPort = process.env["PORT"]; if (!rawPort) { throw new Error( "PORT environment variable is required but was not provided.", ); } const port = Number(rawPort); if (Number.isNaN(port) || port <= 0) { throw new Error(`Invalid PORT value: "${rawPort}"`); } async function ensureSessionsTable(): Promise { try { const exists = await db.execute( sql`SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'sessions')`, ); const rows = exists.rows as [{ exists: boolean }]; if (rows[0]?.exists) return; await db.execute( sql`CREATE TABLE "sessions" ("sid" varchar NOT NULL, "sess" json NOT NULL, "expire" timestamp(6) NOT NULL)`, ); await db.execute( sql`ALTER TABLE "sessions" ADD PRIMARY KEY ("sid")`, ); logger.info("Sessions table created"); } catch (err) { logger.error({ err }, "Failed to ensure sessions table"); } } async function seedAdminUser(): Promise { try { const [row] = await db.select({ count: sql`count(*)::int` }).from(usersTable); if ((row?.count ?? 0) > 0) return; const adminUsername = process.env.LOCAL_ADMIN_USERNAME || "admin"; const adminPassword = process.env.LOCAL_ADMIN_PASSWORD; if (!adminPassword) { logger.warn("LOCAL_ADMIN_PASSWORD is not set — skipping admin seed. Set it to enable local login."); return; } const passwordHash = await bcrypt.hash(adminPassword, 10); await db.insert(usersTable).values({ username: adminUsername, passwordHash, role: "admin", tier: "enterprise", }); logger.info({ username: adminUsername }, "Admin user created"); } catch (err) { logger.error({ err }, "Failed to seed admin user"); } } async function ensureToolRelationsTable(): Promise { try { const exists = await db.execute( sql`SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'tool_relations')`, ); const rows = exists.rows as [{ exists: boolean }]; if (rows[0]?.exists) return; await db.execute( sql`CREATE TABLE "tool_relations" ("id" serial NOT NULL, "tool_id" integer NOT NULL REFERENCES "tools"("id") ON DELETE CASCADE, "related_tool_id" integer NOT NULL REFERENCES "tools"("id") ON DELETE CASCADE, "relation_type" text NOT NULL DEFAULT 'similar', "notes" text, "created_by" integer REFERENCES "users"("id"), "created_at" timestamp with time zone NOT NULL DEFAULT NOW())`, ); await db.execute(sql`ALTER TABLE "tool_relations" ADD PRIMARY KEY ("id")`); logger.info("tool_relations table created"); } catch (err) { logger.error({ err }, "Failed to ensure tool_relations table"); } } async function ensureUserColumns(): Promise { try { const rows = (await db.execute( sql`SELECT column_name, is_nullable FROM information_schema.columns WHERE table_name = 'users'`, )).rows as { column_name: string; is_nullable: string }[]; const existing = new Map(rows.map((r) => [r.column_name, r.is_nullable])); if (!existing.has("tier")) { await db.execute(sql`ALTER TABLE "users" ADD COLUMN "tier" text NOT NULL DEFAULT 'free'`); logger.info("Added column: tier"); } if (!existing.has("auth_provider")) { await db.execute(sql`ALTER TABLE "users" ADD COLUMN "auth_provider" text NOT NULL DEFAULT 'local'`); logger.info("Added column: auth_provider"); } if (!existing.has("auth_provider_id")) { await db.execute(sql`ALTER TABLE "users" ADD COLUMN "auth_provider_id" text`); logger.info("Added column: auth_provider_id"); } if (!existing.has("display_name")) { await db.execute(sql`ALTER TABLE "users" ADD COLUMN "display_name" text`); logger.info("Added column: display_name"); } if (!existing.has("password_hash")) { await db.execute(sql`ALTER TABLE "users" ADD COLUMN "password_hash" text`); logger.info("Added column: password_hash"); } else if (existing.get("password_hash") === "NO") { await db.execute(sql`ALTER TABLE "users" ALTER COLUMN "password_hash" DROP NOT NULL`); logger.info("Made password_hash nullable"); } } catch (err) { logger.error({ err }, "Failed to ensure user columns"); } } async function ensureToolCostsTable(): Promise { try { const exists = await db.execute( sql`SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'tool_costs')`, ); const rows = exists.rows as [{ exists: boolean }]; if (rows[0]?.exists) return; await db.execute( sql`CREATE TABLE "tool_costs" ("id" serial NOT NULL, "tool_id" integer NOT NULL REFERENCES "tools"("id") ON DELETE CASCADE, "license_type" text NOT NULL DEFAULT 'free', "cost" numeric(10,2), "currency" text DEFAULT 'EUR', "renewal_date" timestamp with time zone, "notes" text, "created_by" integer REFERENCES "users"("id"), "created_at" timestamp with time zone NOT NULL DEFAULT NOW())`, ); await db.execute(sql`ALTER TABLE "tool_costs" ADD PRIMARY KEY ("id")`); logger.info("tool_costs table created"); } catch (err) { logger.error({ err }, "Failed to ensure tool_costs table"); } } async function ensureVoterTokenColumn(): Promise { try { const exists = await db.execute( sql`SELECT EXISTS (SELECT FROM information_schema.columns WHERE table_name = 'ratings' AND column_name = 'voter_token')`, ); const rows = exists.rows as [{ exists: boolean }]; if (rows[0]?.exists) return; await db.execute(sql`ALTER TABLE "ratings" ADD COLUMN "voter_token" text`); logger.info("voter_token column added to ratings"); } catch (err) { logger.error({ err }, "Failed to add voter_token column"); } } async function ensureBillingPeriodColumn(): Promise { try { const exists = await db.execute( sql`SELECT EXISTS (SELECT FROM information_schema.columns WHERE table_name = 'tool_costs' AND column_name = 'billing_period')`, ); const rows = exists.rows as [{ exists: boolean }]; if (rows[0]?.exists) return; await db.execute(sql`ALTER TABLE "tool_costs" ADD COLUMN "billing_period" text`); logger.info("billing_period column added to tool_costs"); } catch (err) { logger.error({ err }, "Failed to add billing_period column"); } } async function ensureAdminTier(): Promise { try { const result = await db .update(usersTable) .set({ tier: "enterprise" }) .where(sql`role = 'admin' AND (tier IS NULL OR tier = 'free')`) .returning({ id: usersTable.id, username: usersTable.username }); if (result.length > 0) { logger.info({ count: result.length }, "Admin users upgraded to enterprise tier"); } } catch (err) { logger.error({ err }, "Failed to upgrade admin tier"); } } async function purgeTrash(): Promise { const retentionDays = Number(process.env["TRASH_RETENTION_DAYS"] ?? "0"); if (!Number.isFinite(retentionDays) || retentionDays <= 0) return; try { const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000); const result = await db .delete(toolsTable) .where(sql`${toolsTable.deletedAt} IS NOT NULL AND ${toolsTable.deletedAt} < ${cutoff}`) .returning({ id: toolsTable.id }); if (result.length > 0) { logger.info({ count: result.length }, "Purged expired trash entries"); } } catch (err) { logger.error({ err }, "Failed to purge trash"); } } async function start(): Promise { await ensureSessionsTable(); await ensureToolRelationsTable(); await ensureToolCostsTable(); await ensureVoterTokenColumn(); await ensureBillingPeriodColumn(); await ensureUserColumns(); await seedAdminUser(); await ensureAdminTier(); await purgeTrash(); const retentionDays = Number(process.env["TRASH_RETENTION_DAYS"] ?? "0"); if (Number.isFinite(retentionDays) && retentionDays > 0) { setInterval(() => { void purgeTrash(); }, 60 * 60 * 1000); logger.info({ retentionDays }, "Trash auto-purge enabled"); } app.listen(port, (err) => { if (err) { logger.error({ err }, "Error listening on port"); process.exit(1); } logger.info({ port }, "Server listening"); }); } start().catch((err) => { logger.error({ err }, "Server startup failed"); process.exit(1); });