From cfc5339a2518ebec772c114549a5e39444b764cc Mon Sep 17 00:00:00 2001 From: AyoubBenziza Date: Thu, 11 Jun 2026 23:54:56 +0200 Subject: [PATCH] fix(server): connect via PG* vars (robust to special chars in password) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit postgres-js failed to parse DATABASE_URL when the password contained URL-special characters (@ : / # ...). New createSqlClient(): uses DATABASE_URL if set, else falls back to PG* env vars (PGHOST/PGUSER/PGPASSWORD/...) which postgres-js reads directly — password passed verbatim, no URL encoding. docker-compose.prod.yml now sets PG* for the server instead of an inline DSN. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/server/src/db/client.ts | 21 +++++++++++++++++++++ apps/server/src/db/index.ts | 7 +++---- apps/server/src/db/migrate.ts | 10 ++++------ apps/server/src/db/seed-images.ts | 14 ++++++-------- apps/server/src/db/seed.ts | 10 ++++------ docker-compose.prod.yml | 7 ++++++- 6 files changed, 44 insertions(+), 25 deletions(-) create mode 100644 apps/server/src/db/client.ts diff --git a/apps/server/src/db/client.ts b/apps/server/src/db/client.ts new file mode 100644 index 0000000..16a0039 --- /dev/null +++ b/apps/server/src/db/client.ts @@ -0,0 +1,21 @@ +// Création du client postgres-js. Deux modes : +// 1. DATABASE_URL (DSN complet) — pratique en dev. +// 2. Variables PG* (PGHOST/PGPORT/PGUSER/PGPASSWORD/PGDATABASE) — recommandé en +// prod : le mot de passe est passé tel quel (pas d'encodage d'URL à gérer). +// postgres-js lit automatiquement les PG* de l'environnement quand on ne lui +// passe pas de DSN. + +import postgres from "postgres" +import { env } from "../env" + +export const hasDbConfig = !!env.databaseUrl || !!process.env.PGHOST + +export function createSqlClient(opts: { max?: number } = {}) { + if (env.databaseUrl) { + return postgres(env.databaseUrl, opts) + } + if (process.env.PGHOST) { + return postgres(opts) + } + return null +} diff --git a/apps/server/src/db/index.ts b/apps/server/src/db/index.ts index a6712e1..f0e5d6c 100644 --- a/apps/server/src/db/index.ts +++ b/apps/server/src/db/index.ts @@ -2,17 +2,16 @@ // le jeu retombe sur la banque de questions en dur (dev sans infra). import { drizzle } from "drizzle-orm/postgres-js" -import postgres from "postgres" -import { env } from "../env" +import { createSqlClient } from "./client" import * as schema from "./schema" export type Database = ReturnType> function createDb(): Database | null { - if (!env.databaseUrl) { + const client = createSqlClient() + if (!client) { return null } - const client = postgres(env.databaseUrl) return drizzle(client, { schema }) } diff --git a/apps/server/src/db/migrate.ts b/apps/server/src/db/migrate.ts index 6f708c4..f552292 100644 --- a/apps/server/src/db/migrate.ts +++ b/apps/server/src/db/migrate.ts @@ -2,15 +2,13 @@ // bun run db:migrate import { drizzle } from "drizzle-orm/postgres-js" import { migrate } from "drizzle-orm/postgres-js/migrator" -import postgres from "postgres" -import { env } from "../env" +import { createSqlClient } from "./client" -if (!env.databaseUrl) { - console.error("DATABASE_URL manquant — rien à migrer.") +const client = createSqlClient({ max: 1 }) +if (!client) { + console.error("Aucune config DB (DATABASE_URL ou PG*) — rien à migrer.") process.exit(1) } - -const client = postgres(env.databaseUrl, { max: 1 }) const db = drizzle(client) await migrate(db, { migrationsFolder: "./drizzle" }) diff --git a/apps/server/src/db/seed-images.ts b/apps/server/src/db/seed-images.ts index 7c642b4..69e31aa 100644 --- a/apps/server/src/db/seed-images.ts +++ b/apps/server/src/db/seed-images.ts @@ -6,21 +6,19 @@ import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/postgres-js" -import postgres from "postgres" -import { env } from "../env" +import { createSqlClient } from "./client" import { quizCategory, quizQuestion, type NewQuizQuestion } from "./schema" -if (!env.databaseUrl) { - console.error("DATABASE_URL manquant — impossible de seeder.") - process.exit(1) -} - const COUNT = Number(process.env.IMAGE_SEED_COUNT ?? 151) const CATEGORY = "Pokémon" const PROMPT = "Quel est ce Pokémon ?" const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) -const client = postgres(env.databaseUrl, { max: 1 }) +const client = createSqlClient({ max: 1 }) +if (!client) { + console.error("Aucune config DB (DATABASE_URL ou PG*) — impossible de seeder.") + process.exit(1) +} const db = drizzle(client, { schema: { quizCategory, quizQuestion } }) async function upsertCategory(name: string): Promise { diff --git a/apps/server/src/db/seed.ts b/apps/server/src/db/seed.ts index 6af9d3c..978b89d 100644 --- a/apps/server/src/db/seed.ts +++ b/apps/server/src/db/seed.ts @@ -5,8 +5,7 @@ import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/postgres-js" -import postgres from "postgres" -import { env } from "../env" +import { createSqlClient } from "./client" import { quizCategory, quizQuestion, @@ -14,12 +13,11 @@ import { } from "./schema" import { QUIZ_QUESTIONS } from "../game/modes/quiz/questions" -if (!env.databaseUrl) { - console.error("DATABASE_URL manquant — impossible de seeder.") +const client = createSqlClient({ max: 1 }) +if (!client) { + console.error("Aucune config DB (DATABASE_URL ou PG*) — impossible de seeder.") process.exit(1) } - -const client = postgres(env.databaseUrl, { max: 1 }) const db = drizzle(client, { schema: { quizCategory, quizQuestion } }) // Catégories Open Trivia DB pertinentes (id OpenTDB → nom affiché). diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index d7d3a9c..32ed268 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -34,7 +34,12 @@ services: NODE_ENV: production HOST: 0.0.0.0 PORT: 3001 - DATABASE_URL: postgres://${POSTGRES_USER:-nerdware}:${POSTGRES_PASSWORD:-nerdware}@postgres:5432/${POSTGRES_DB:-nerdware} + # Connexion via PG* (mot de passe passé tel quel, pas d'encodage d'URL). + PGHOST: postgres + PGPORT: 5432 + PGUSER: ${POSTGRES_USER:-nerdware} + PGPASSWORD: ${POSTGRES_PASSWORD:-nerdware} + PGDATABASE: ${POSTGRES_DB:-nerdware} ADMIN_TOKEN: ${ADMIN_TOKEN} CORS_ORIGINS: ${CORS_ORIGINS} UPLOADS_DIR: /data/uploads