import { App, Context, staticFiles } from "fresh"; import { type State } from "./utils.ts"; import { loadSync } from "$std/dotenv/mod.ts"; import { sql } from "drizzle-orm"; // Allow empty values for optional Stripe variables loadSync({ allowEmptyValues: true, export: true }); import { checkDatabaseConnection } from "./db/db.ts"; // Development mode can be checked via: Deno.env.get("DEVELOPMENT") === "true" /** Error page HTML for service unavailability */ const SERVICE_UNAVAILABLE_HTML = ` Service Unavailable
🔧

The problem is on our side!

We will fix this as soon as we can.

`; /** Check if required KV cache files exist */ function checkKvCacheFiles(): boolean { // const requiredFiles = ["./data/kv-cache-shm", "./data/kv-cache-wal"]; // for (const file of requiredFiles) { // try { // Deno.statSync(file); // } catch { // console.error(`⚠️ Required file missing: ${file}`); // return false; // } // } return true; } /** Perform all startup checks */ async function _performStartupChecks(): Promise { //console.log("Checking startup requirements..."); // Check KV cache files const kvFilesExist = checkKvCacheFiles(); if (!kvFilesExist) { console.error( "⚠️ KV cache files missing - app will display maintenance page", ); return false; } //console.log("✅ KV cache files found"); // Check database connection //console.log("Checking database connection..."); const dbAvailable = await checkDatabaseConnection(); if (!dbAvailable) { console.error( "⚠️ Database connection failed - app will display maintenance page", ); return false; } //console.log("✅ Database connection successful"); return true; } // Run startup checks first const isAppReady = true; // await _performStartupChecks(); // Create app based on startup check result export const app = new App(); if (isAppReady) { // Full app with all routes and middleware const { InitiPGSessionMiddleware } = await import( "./services/stateMiddleware.ts" ); const { createAuthMiddleware } = await import( "./services/authMiddleware.ts" ); app.use(staticFiles()); app.get("/health", async (_ctx: Context) => { console.log("Health check - verifying database connection..."); const dbConnected = await checkDatabaseConnection(); if (dbConnected) { console.log("✅ Health check: Database connected"); return new Response( JSON.stringify({ status: "healthy", database: "connected", timestamp: new Date().toISOString(), }), { status: 200, headers: { "Content-Type": "application/json" }, }, ); } else { console.log("❌ Health check: Database connection failed"); return new Response( JSON.stringify({ status: "unhealthy", database: "disconnected", timestamp: new Date().toISOString(), }), { status: 503, headers: { "Content-Type": "application/json" }, }, ); } }); // Debug endpoint to verify database schema (no auth required) app.get("/api/debug/schema", async (_ctx: Context) => { try { const { db } = await import("./db/db.ts"); // Query PostgreSQL information_schema to get all tables const query = ` SELECT table_name, table_schema FROM information_schema.tables WHERE table_schema = 'memory' OR table_schema = 'checkpoints' OR table_schema = 'public' ORDER BY table_schema, table_name `; const result = await db.execute(query); // Expected tables based on schema.ts const expectedTables = [ "users", "roles", "user_roles", "state_keys", "user_tokens", "web_tokens", "obligations", "incomes", "financial_balances", "properties", "rent_schedules", "leases", "payment_methods", "property_media", "lease_documents", "tenant_applications", "tenant_invites", "payment_transactions", "reminders", "messages", "maintenance_requests", "user_pins", "session_configs", ]; // Extract table names from query result const existingTables = result.rows.map((row: Record) => String(row["table_name"]) ); // Find missing tables const missingTables = expectedTables.filter( (table) => !existingTables.includes(table), ); return new Response( JSON.stringify({ status: "success", existing_tables: existingTables, expected_tables: expectedTables, missing_tables: missingTables, all_tables_present: missingTables.length === 0, total_existing: existingTables.length, total_expected: expectedTables.length, }), { status: 200, headers: { "Content-Type": "application/json" }, }, ); } catch (error) { console.error("Error checking database schema:", error); return new Response( JSON.stringify({ status: "error", message: error instanceof Error ? error.message : "Unknown error", }), { status: 500, headers: { "Content-Type": "application/json" }, }, ); } }); // Debug endpoint to test session persistence across server restarts app.get("/api/debug/persistence-test", async (_ctx: Context) => { const url = new URL(_ctx.req.url); const action = url.searchParams.get("action") || "check"; try { const { db } = await import("./db/db.ts"); const TEST_USER_ID = "PERSIST_TEST_12345"; const TEST_STATE_ID = "test-persist-state-" + Date.now(); if (action === "create") { // Use Drizzle ORM functions for proper database operations const { sqlSetStateKey } = await import("./db/sqlUserRepository.ts"); const createdOn = Date.now(); const sealKey = "test-seal-key-" + createdOn; // Insert test state key using Drizzle ORM await sqlSetStateKey( sealKey, TEST_STATE_ID, "test-cookie-hash-" + createdOn, ); // For user_tokens, use raw SQL with schema qualification to avoid drizzle issues try { await db.execute(sql` INSERT INTO memory.user_tokens (user_id, state_id, access_token, refresh_token, created_on, updated_on) VALUES (${TEST_USER_ID}, ${TEST_STATE_ID}, ${"test-access-token"}, ${"test-refresh-token"}, ${createdOn}, ${createdOn}) ON CONFLICT (user_id, state_id) DO UPDATE SET access_token = EXCLUDED.access_token, refresh_token = EXCLUDED.refresh_token, updated_on = EXCLUDED.updated_on `); } catch (tokenError) { console.error("Failed to insert user_token:", tokenError); // Continue anyway - state_key is the critical test } return new Response( JSON.stringify({ status: "created", message: "Test data created in PostgreSQL", test_user_id: TEST_USER_ID, test_state_id: TEST_STATE_ID, seal_key: sealKey, next_step: "Restart the server and call with action=verify&state_id=" + TEST_STATE_ID, }), { status: 200, headers: { "Content-Type": "application/json" } }, ); } if (action === "verify") { const stateIdParam = url.searchParams.get("state_id"); const stateIdToCheck = stateIdParam || TEST_STATE_ID; // Use existing SQL repository functions const { sqlGetStateKeys, sqlGetUserTokens } = await import( "./db/sqlUserRepository.ts" ); const stateKeys = await sqlGetStateKeys(stateIdToCheck); const userTokens = await sqlGetUserTokens(TEST_USER_ID); const stateKeyExists = stateKeys && stateKeys.length > 0; const userTokenExists = userTokens && userTokens.length > 0; // State key persistence is the critical test - it's the session data // User tokens are secondary const persistenceVerified = stateKeyExists; return new Response( JSON.stringify({ status: "verified", test_user_id: TEST_USER_ID, test_state_id: stateIdToCheck, state_key_exists: stateKeyExists, state_key_data: stateKeyExists ? stateKeys[0] : null, user_token_exists: userTokenExists, user_token_data: userTokenExists ? userTokens[0] : null, persistence_verified: persistenceVerified, message: persistenceVerified ? "✅ Session state_key PERSISTS across server restart - PostgreSQL is working!" : "❌ Session data LOST - using in-memory storage (CRITICAL FAILURE)", details: stateKeyExists && !userTokenExists ? "State key persisted (primary session data), user token may have had insertion error" : null, next_step: "Call with action=cleanup&state_id=" + stateIdToCheck + " to remove test data", }), { status: 200, headers: { "Content-Type": "application/json" } }, ); } if (action === "cleanup") { // Remove test data using SQL repository functions const { sqlDeleteStateKey, sqlDeleteUserTokens } = await import( "./db/sqlUserRepository.ts" ); const stateIdParam = url.searchParams.get("state_id"); const stateIdToClean = stateIdParam || TEST_STATE_ID; // Get the seal key first const { sqlGetStateKeys } = await import("./db/sqlUserRepository.ts"); const stateKeys = await sqlGetStateKeys(stateIdToClean); if (stateKeys && stateKeys.length > 0) { await sqlDeleteStateKey(stateKeys[0].seal_key); } await sqlDeleteUserTokens(TEST_USER_ID); return new Response( JSON.stringify({ status: "cleaned", message: "Test data removed from database", }), { status: 200, headers: { "Content-Type": "application/json" } }, ); } // Default: show instructions return new Response( JSON.stringify({ status: "ready", message: "Persistence test endpoint", instructions: [ "1. Call with action=create to insert test data", "2. Restart the Deno server completely", "3. Call with action=verify to check if data persisted", "4. Call with action=cleanup to remove test data", ], }), { status: 200, headers: { "Content-Type": "application/json" } }, ); } catch (error) { console.error("Persistence test error:", error); return new Response( JSON.stringify({ status: "error", message: error instanceof Error ? error.message : "Unknown error", }), { status: 500, headers: { "Content-Type": "application/json" } }, ); } }); // app.post("/graph-update", async (ctx: Context) => { // console.log("Graph update"); // const body = await ctx.req.json(); // //console.log(body); // //const { userId, roles, organization } = body; // // await inMemoryCacheKv.set(["ciam", userId], { // // userId, // // roles, // // organization, // // lastSynced: Date.now(), // // }); // //console.log(`Graph update applied for user ${userId}`); // return new Response("Graph update stored in cache", { status: 202 }); // }); // TODO: add a well-known endpoint // app.get("/.well-known/acme-challenge/:token", (ctx: Context) => { // const token = ctx.params.token; // const content = `${token}.EXAMPLE_ACCOUNT_THUMBPRINT`; // return new Response(content, { // status: 200, // headers: { // "content-type": "text/plain; charset=utf-8", // "cache-control": "public, max-age=0, must-revalidate", // }, // }); // }); app.use(InitiPGSessionMiddleware()); app.use(createAuthMiddleware()); app.fsRoutes(); //console.log("✅ Full application loaded"); } else { // Maintenance mode - serve error page for all routes app.use(staticFiles()); app.get("/health", () => { return new Response("Service Unavailable", { status: 503 }); }); app.use(() => { return new Response(SERVICE_UNAVAILABLE_HTML, { status: 503, headers: { "Content-Type": "text/html; charset=utf-8" }, }); }); console.log("⚠️ Maintenance mode - serving error page only"); // Start periodic database connection check (every 60 seconds) // setInterval(async () => { // console.log("🔄 Checking if database connection is restored..."); // const dbAvailable = await checkDatabaseConnection(); // if (dbAvailable) { // console.log("✅ Database connection restored! Restarting app..."); // // Check if running in development mode // // Use exec to replace shell with new process, keeping terminal connection // const command = new Deno.Command("sh", { // args: [ // "-c", // `lsof -ti:8080 | xargs kill -9 2>/dev/null || true; ${restartCmd}`, // ], // cwd: Deno.cwd(), // stdin: "inherit", // stdout: "inherit", // stderr: "inherit", // }); // command.spawn(); // Deno.exit(0); // } else { // console.log("❌ Database still unavailable, will retry in 60 seconds"); // } // }, 60_000); } // if (import.meta.main) { // await app.listen(); // }