Run migrations on server startup
This commit is contained in:
@@ -67,3 +67,9 @@ Node 서버에 Auth API를 추가했다.
|
|||||||
2026-07-14 Auth API 커밋 배포 후 `POST /api/auth/login` 응답이 404에서 500으로 변경되었다.
|
2026-07-14 Auth API 커밋 배포 후 `POST /api/auth/login` 응답이 404에서 500으로 변경되었다.
|
||||||
|
|
||||||
판단: 라우터는 배포에 반영되었고, 남은 문제는 운영 DB의 사용자 seed 또는 로그인 처리 중 DB 상태 문제로 좁혀졌다. 다음 배포부터 테스트 계정이 자동 보장되도록 `migrations/005_seed_test_users.sql`을 추가한다.
|
판단: 라우터는 배포에 반영되었고, 남은 문제는 운영 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.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { apiRouter } from './routes/index.js';
|
|||||||
import { platformStatusRouter } from './routes/health.js';
|
import { platformStatusRouter } from './routes/health.js';
|
||||||
import { errorHandler } from './http/errors.js';
|
import { errorHandler } from './http/errors.js';
|
||||||
import { getPool } from './db/pool.js';
|
import { getPool } from './db/pool.js';
|
||||||
|
import { runMigrations } from './db/migrate.js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const frontendDist = path.resolve(process.cwd(), 'frontend', 'dist');
|
const frontendDist = path.resolve(process.cwd(), 'frontend', 'dist');
|
||||||
@@ -45,6 +46,12 @@ app.get('*', (_req, res) => {
|
|||||||
|
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|
||||||
app.listen(env.port, () => {
|
try {
|
||||||
console.log(`ACS Node app listening on port ${env.port}`);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -181,15 +181,19 @@ router.post('/change-password', asyncRoute(async (req, res) => {
|
|||||||
|
|
||||||
router.post('/diagnostics', asyncRoute(async (_req, res) => {
|
router.post('/diagnostics', asyncRoute(async (_req, res) => {
|
||||||
const tableResult = await query<{
|
const tableResult = await query<{
|
||||||
|
current_schema: string;
|
||||||
users_table: string | null;
|
users_table: string | null;
|
||||||
user_roles_table: string | null;
|
user_roles_table: string | null;
|
||||||
sessions_table: string | null;
|
sessions_table: string | null;
|
||||||
|
migrations_table: string | null;
|
||||||
}>(
|
}>(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
to_regclass('public.users')::text AS users_table,
|
current_schema() AS current_schema,
|
||||||
to_regclass('public.user_roles')::text AS user_roles_table,
|
to_regclass('users')::text AS users_table,
|
||||||
to_regclass('public.user_sessions')::text AS sessions_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];
|
const tables = tableResult.rows[0];
|
||||||
@@ -202,6 +206,8 @@ router.post('/diagnostics', asyncRoute(async (_req, res) => {
|
|||||||
roles: string[];
|
roles: string[];
|
||||||
}> = [];
|
}> = [];
|
||||||
let userQueryError: string | undefined;
|
let userQueryError: string | undefined;
|
||||||
|
let migrations: string[] = [];
|
||||||
|
let migrationQueryError: string | undefined;
|
||||||
|
|
||||||
if (tables?.users_table && tables.user_roles_table) {
|
if (tables?.users_table && tables.user_roles_table) {
|
||||||
try {
|
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, {
|
ok(res, {
|
||||||
authApi: true,
|
authApi: true,
|
||||||
checkedAt: new Date().toISOString(),
|
checkedAt: new Date().toISOString(),
|
||||||
tables,
|
tables,
|
||||||
users,
|
users,
|
||||||
userQueryError,
|
userQueryError,
|
||||||
|
migrations,
|
||||||
|
migrationQueryError,
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user