diff --git a/docs/ACS-login-404-analysis.md b/docs/ACS-login-404-analysis.md index 7f4ec17..6410e0a 100644 --- a/docs/ACS-login-404-analysis.md +++ b/docs/ACS-login-404-analysis.md @@ -67,3 +67,9 @@ Node 서버에 Auth API를 추가했다. 2026-07-14 Auth API 커밋 배포 후 `POST /api/auth/login` 응답이 404에서 500으로 변경되었다. 판단: 라우터는 배포에 반영되었고, 남은 문제는 운영 DB의 사용자 seed 또는 로그인 처리 중 DB 상태 문제로 좁혀졌다. 다음 배포부터 테스트 계정이 자동 보장되도록 `migrations/005_seed_test_users.sql`을 추가한다. + +2026-07-14 after Coolify redeploy, `/api/health` returned `build: auth-seed-20260714`. + +However, `/api/auth/diagnostics` reported no `users` or `user_roles` table, and login still returned 500. + +Conclusion: runtime migration is not guaranteed when Coolify overrides Dockerfile CMD. Added startup migration in `server/index.ts` so `runMigrations()` executes before the Express server starts. diff --git a/server/index.ts b/server/index.ts index f1c4644..5c495a9 100644 --- a/server/index.ts +++ b/server/index.ts @@ -8,6 +8,7 @@ import { apiRouter } from './routes/index.js'; import { platformStatusRouter } from './routes/health.js'; import { errorHandler } from './http/errors.js'; import { getPool } from './db/pool.js'; +import { runMigrations } from './db/migrate.js'; const app = express(); const frontendDist = path.resolve(process.cwd(), 'frontend', 'dist'); @@ -45,6 +46,12 @@ app.get('*', (_req, res) => { app.use(errorHandler); -app.listen(env.port, () => { - console.log(`ACS Node app listening on port ${env.port}`); -}); +try { + await runMigrations(); + app.listen(env.port, () => { + console.log(`ACS Node app listening on port ${env.port}`); + }); +} catch (error) { + console.error('ACS startup failed', error); + process.exitCode = 1; +} diff --git a/server/routes/auth.ts b/server/routes/auth.ts index fa37555..db6cd23 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -181,15 +181,19 @@ router.post('/change-password', asyncRoute(async (req, res) => { router.post('/diagnostics', asyncRoute(async (_req, res) => { const tableResult = await query<{ + current_schema: string; users_table: string | null; user_roles_table: string | null; sessions_table: string | null; + migrations_table: string | null; }>( ` SELECT - to_regclass('public.users')::text AS users_table, - to_regclass('public.user_roles')::text AS user_roles_table, - to_regclass('public.user_sessions')::text AS sessions_table + current_schema() AS current_schema, + to_regclass('users')::text AS users_table, + to_regclass('user_roles')::text AS user_roles_table, + to_regclass('user_sessions')::text AS sessions_table, + to_regclass('schema_migrations')::text AS migrations_table `, ); const tables = tableResult.rows[0]; @@ -202,6 +206,8 @@ router.post('/diagnostics', asyncRoute(async (_req, res) => { roles: string[]; }> = []; let userQueryError: string | undefined; + let migrations: string[] = []; + let migrationQueryError: string | undefined; if (tables?.users_table && tables.user_roles_table) { try { @@ -234,12 +240,25 @@ router.post('/diagnostics', asyncRoute(async (_req, res) => { } } + if (tables?.migrations_table) { + try { + const migrationResult = await query<{ filename: string }>( + 'SELECT filename FROM schema_migrations ORDER BY filename', + ); + migrations = migrationResult.rows.map((row) => row.filename); + } catch (error) { + migrationQueryError = error instanceof Error ? error.message : String(error); + } + } + ok(res, { authApi: true, checkedAt: new Date().toISOString(), tables, users, userQueryError, + migrations, + migrationQueryError, }); }));