From 31b3eb21b486c1632244c72dbe4629c26fb4c885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Mon, 25 May 2026 17:50:34 +0200 Subject: [PATCH] feat(auth): migrate user store from SQLite to PostgreSQL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users were wiped on container recreation because they shared the backend_data volume with ephemeral pharmacy/medicine data. Postgres gets its own named volume (postgres_data) that survives deploys. - Add postgres:16-alpine service + postgres_data volume to compose - Add pg + connect-pg-simple deps - userDbGet/userDbRun helpers: PG when PG_URL set, SQLite fallback - Sessions use connect-pg-simple when pgPool available - create-admin.js: PG-aware, SQLite fallback for local dev - push_subscriptions.user_id: plain INTEGER (cross-DB FK dropped) - Tests unchanged — no PG_URL in test env = SQLite fallback Set PG_PASSWORD in server .env before next deploy. Co-Authored-By: Claude Sonnet 4.6 --- backend/.env.example | 4 ++ backend/create-admin.js | 143 +++++++++++++++++++------------------ backend/package-lock.json | 145 ++++++++++++++++++++++++++++++++++++++ backend/package.json | 2 + backend/server.js | 108 ++++++++++++++++++++-------- docker-compose.yml | 13 ++++ 6 files changed, 319 insertions(+), 96 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index a7ac7b2..bf65dcd 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -6,6 +6,10 @@ REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= +# PostgreSQL for user accounts (leave unset to fallback to SQLite — dev/test only) +PG_URL=postgresql://farmafinder:change-me@localhost:5432/farmafinder +PG_PASSWORD=change-me + # Web Push (VAPID). Generate with: # node -e "import('web-push').then(w => console.log(w.default.generateVAPIDKeys()))" VAPID_PUBLIC_KEY= diff --git a/backend/create-admin.js b/backend/create-admin.js index a28e631..16860e4 100644 --- a/backend/create-admin.js +++ b/backend/create-admin.js @@ -3,86 +3,95 @@ import { promisify } from 'util'; import bcrypt from 'bcrypt'; import path from 'path'; import { fileURLToPath } from 'url'; +import pg from 'pg'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const dbPath = path.join(__dirname, 'database.sqlite'); -const db = new sqlite3.Database(dbPath); - -// Custom wrapper to get lastID from db.run -function dbRun(sql, params = []) { - return new Promise((resolve, reject) => { - db.run(sql, params, function(err) { - if (err) reject(err); - else resolve({ lastID: this.lastID, changes: this.changes }); - }); - }); -} - -const dbGet = promisify(db.get.bind(db)); - -// Initialize users table -async function initDatabase() { - try { - await dbRun(` - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) - `); - console.log('Database table initialized'); - } catch (error) { - console.error('Error initializing database:', error); - throw error; - } -} +const PG_URL = process.env.PG_URL; async function createAdmin() { - try { - // Initialize database tables first - await initDatabase(); + const username = process.env.ADMIN_USERNAME || 'admin'; + const password = process.env.ADMIN_PASSWORD || 'admin123'; + const passwordHash = await bcrypt.hash(password, 10); - // Default admin credentials - const username = process.env.ADMIN_USERNAME || 'admin'; - const password = process.env.ADMIN_PASSWORD || 'admin123'; + if (PG_URL) { + const { Pool } = pg; + const pool = new Pool({ connectionString: PG_URL }); + try { + await pool.query(` + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + is_admin INTEGER NOT NULL DEFAULT 0, + address TEXT, + latitude DOUBLE PRECISION, + longitude DOUBLE PRECISION, + created_at TIMESTAMPTZ DEFAULT NOW() + ) + `); - // Check if admin already exists - const existing = await dbGet( - 'SELECT * FROM users WHERE username = ?', - [username] - ); + const existing = await pool.query('SELECT id FROM users WHERE username = $1', [username]); + if (existing.rows.length > 0) { + console.log(`Admin user '${username}' already exists.`); + console.log('To reset, delete the user first and re-run.'); + await pool.end(); + return; + } - if (existing) { - console.log(`Admin user '${username}' already exists.`); - console.log('To change the password, delete the user first and run this script again.'); - db.close(); - return; + await pool.query( + 'INSERT INTO users (username, password_hash, is_admin) VALUES ($1, $2, 1)', + [username, passwordHash] + ); + console.log(`Admin user '${username}' created in PostgreSQL.`); + } finally { + await pool.end(); } + } else { + const dbPath = process.env.DATABASE_PATH || path.join(__dirname, 'database.sqlite'); + const db = new sqlite3.Database(dbPath); + const dbRun = (sql, params = []) => + new Promise((resolve, reject) => + db.run(sql, params, function (err) { err ? reject(err) : resolve({ lastID: this.lastID }); }) + ); + const dbGet = promisify(db.get.bind(db)); - // Hash password - const saltRounds = 10; - const passwordHash = await bcrypt.hash(password, saltRounds); + try { + await dbRun(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); + try { await dbRun('ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0'); } catch {} - // Create admin user - await dbRun( - 'INSERT INTO users (username, password_hash) VALUES (?, ?)', - [username, passwordHash] - ); + const existing = await dbGet('SELECT id FROM users WHERE username = ?', [username]); + if (existing) { + console.log(`Admin user '${username}' already exists.`); + console.log('To reset, delete the user first and re-run.'); + db.close(); + return; + } - console.log('✅ Admin user created successfully!'); - console.log(`Username: ${username}`); - console.log(`Password: ${password}`); - console.log('\n⚠️ IMPORTANT: Change the default password after first login!'); - console.log(' You can set ADMIN_USERNAME and ADMIN_PASSWORD environment variables to customize.'); - } catch (error) { - console.error('Error creating admin user:', error); - } finally { - db.close(); + await dbRun( + 'INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, 1)', + [username, passwordHash] + ); + console.log(`Admin user '${username}' created in SQLite.`); + } finally { + db.close(); + } } + + console.log(`Username: ${username}`); + console.log(`Password: ${password}`); + console.log('\nIMPORTANT: Change the default password after first login!'); } -createAdmin(); - +createAdmin().catch((err) => { + console.error('Error creating admin user:', err); + process.exit(1); +}); diff --git a/backend/package-lock.json b/backend/package-lock.json index 07ab54e..c86cf45 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,11 +11,13 @@ "dependencies": { "axios": "^1.6.0", "bcrypt": "^5.1.1", + "connect-pg-simple": "^10.0.0", "connect-sqlite3": "^0.9.16", "cors": "^2.8.5", "express": "^4.18.2", "express-rate-limit": "^8.5.2", "express-session": "^1.17.3", + "pg": "^8.13.0", "redis": "^4.6.0", "sqlite3": "^5.1.6", "web-push": "^3.6.7" @@ -2134,6 +2136,17 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/connect-pg-simple": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz", + "integrity": "sha512-pBGVazlqiMrackzCr0eKhn4LO5trJXsOX0nQoey9wCOayh80MYtThCbq8eoLsjpiWgiok/h+1/uti9/2/Una8A==", + "dependencies": { + "pg": "^8.12.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=22.0.0" + } + }, "node_modules/connect-sqlite3": { "version": "0.9.16", "resolved": "https://registry.npmjs.org/connect-sqlite3/-/connect-sqlite3-0.9.16.tgz", @@ -4998,6 +5011,87 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pg": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", + "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", + "dependencies": { + "pg-connection-string": "^2.13.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.14.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", + "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", + "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5041,6 +5135,41 @@ "node": ">=8" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -5712,6 +5841,14 @@ "source-map": "^0.6.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -6389,6 +6526,14 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/package.json b/backend/package.json index 5c994f6..c956cf0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,7 +20,9 @@ "dependencies": { "axios": "^1.6.0", "bcrypt": "^5.1.1", + "connect-pg-simple": "^10.0.0", "connect-sqlite3": "^0.9.16", + "pg": "^8.13.0", "cors": "^2.8.5", "express": "^4.18.2", "express-rate-limit": "^8.5.2", diff --git a/backend/server.js b/backend/server.js index 226d527..aa685ab 100644 --- a/backend/server.js +++ b/backend/server.js @@ -6,6 +6,8 @@ import path from 'path'; import { fileURLToPath } from 'url'; import session from 'express-session'; import connectSqlite3 from 'connect-sqlite3'; +import connectPg from 'connect-pg-simple'; +import pg from 'pg'; import bcrypt from 'bcrypt'; import rateLimit from 'express-rate-limit'; import webpush from 'web-push'; @@ -38,7 +40,12 @@ app.use(cors({ })); app.use(express.json()); -const SQLiteStore = connectSqlite3(session); +const PG_URL = process.env.PG_URL; +let pgPool = null; +if (PG_URL) { + const { Pool } = pg; + pgPool = new Pool({ connectionString: PG_URL }); +} const sessionConfig = { secret: process.env.SESSION_SECRET || 'farma-finder-secret-key-change-in-production', @@ -52,7 +59,13 @@ const sessionConfig = { }; if (process.env.NODE_ENV !== 'test') { - sessionConfig.store = new SQLiteStore({ db: 'sessions.sqlite', dir: __dirname }); + if (pgPool) { + const PgStore = connectPg(session); + sessionConfig.store = new PgStore({ pool: pgPool, createTableIfMissing: true }); + } else { + const SQLiteStore = connectSqlite3(session); + sessionConfig.store = new SQLiteStore({ db: 'sessions.sqlite', dir: __dirname }); + } } // Configure session @@ -90,6 +103,28 @@ function dbRun(sql, params = []) { const dbAll = promisify(db.all.bind(db)); const dbGet = promisify(db.get.bind(db)); +// User DB helpers — route to PG when available, fallback to SQLite for tests +function toPositional(sql) { + let i = 0; + return sql.replace(/\?/g, () => `$${++i}`); +} + +async function userDbGet(sql, params = []) { + if (pgPool) { + const res = await pgPool.query(toPositional(sql), params); + return res.rows[0] ?? null; + } + return dbGet(sql, params); +} + +async function userDbRun(sql, params = []) { + if (pgPool) { + const res = await pgPool.query(toPositional(sql), params); + return { lastID: res.rows[0]?.id, changes: res.rowCount }; + } + return dbRun(sql.replace(/\s+RETURNING\s+\w+\s*$/i, ''), params); +} + // Accept opening_hours as either an object or a JSON string from the client // and return a TEXT value (or null) safe to persist. function serializeOpeningHours(value) { @@ -152,15 +187,30 @@ async function initDatabase() { await dbRun(`CREATE INDEX IF NOT EXISTS idx_pharmacy_medicine ON pharmacy_medicines(medicine_nregistro)`); - // Create users table for authentication - await dbRun(` - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) - `); + // Create users table — PG when available, SQLite fallback + if (pgPool) { + await pgPool.query(` + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + is_admin INTEGER NOT NULL DEFAULT 0, + address TEXT, + latitude DOUBLE PRECISION, + longitude DOUBLE PRECISION, + created_at TIMESTAMPTZ DEFAULT NOW() + ) + `); + } else { + await dbRun(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); + } await dbRun(` CREATE TABLE IF NOT EXISTS push_subscriptions ( @@ -191,17 +241,17 @@ async function initDatabase() { `); await dbRun(`CREATE INDEX IF NOT EXISTS idx_push_subs_pharm ON push_subscriptions_pharmacy(medicine_nregistro, pharmacy_id)`); - // Add is_admin to users table if not yet present (migration for existing DBs) - try { await dbRun('ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0'); } catch {} + if (!pgPool) { + // SQLite-only migrations for existing deployments + try { await dbRun('ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0'); } catch {} + try { await dbRun('ALTER TABLE users ADD COLUMN address TEXT'); } catch {} + try { await dbRun('ALTER TABLE users ADD COLUMN latitude REAL'); } catch {} + try { await dbRun('ALTER TABLE users ADD COLUMN longitude REAL'); } catch {} + } - // Profile fields on users (migration for existing DBs) - try { await dbRun('ALTER TABLE users ADD COLUMN address TEXT'); } catch {} - try { await dbRun('ALTER TABLE users ADD COLUMN latitude REAL'); } catch {} - try { await dbRun('ALTER TABLE users ADD COLUMN longitude REAL'); } catch {} - - // Add user_id to push tables if not yet present (migration for existing DBs) - try { await dbRun('ALTER TABLE push_subscriptions ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE SET NULL'); } catch {} - try { await dbRun('ALTER TABLE push_subscriptions_pharmacy ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE SET NULL'); } catch {} + // Add user_id to push tables (SQLite — no cross-DB FK) + try { await dbRun('ALTER TABLE push_subscriptions ADD COLUMN user_id INTEGER'); } catch {} + try { await dbRun('ALTER TABLE push_subscriptions_pharmacy ADD COLUMN user_id INTEGER'); } catch {} console.log('Database initialized successfully'); } catch (error) { @@ -323,7 +373,7 @@ app.post('/api/auth/login', loginLimiter, async (req, res) => { } // Find user by username - const user = await dbGet( + const user = await userDbGet( 'SELECT * FROM users WHERE username = ?', [username.trim()] ); @@ -375,14 +425,14 @@ app.post('/api/auth/register', registerLimiter, async (req, res) => { return res.status(400).json({ error: 'Password must be at least 8 characters' }); } - const existing = await dbGet('SELECT id FROM users WHERE username = ?', [u]); + const existing = await userDbGet('SELECT id FROM users WHERE username = ?', [u]); if (existing) { return res.status(409).json({ error: 'Username already taken' }); } const passwordHash = await bcrypt.hash(p, 10); - const result = await dbRun( - 'INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, 0)', + const result = await userDbRun( + 'INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, 0) RETURNING id', [u, passwordHash] ); @@ -422,7 +472,7 @@ app.post('/api/auth/logout', (req, res) => { app.get('/api/auth/check', async (req, res) => { try { if (req.session && req.session.userId) { - const user = await dbGet( + const user = await userDbGet( 'SELECT id, username, is_admin, address, latitude, longitude FROM users WHERE id = ?', [req.session.userId] ); @@ -452,7 +502,7 @@ app.get('/api/auth/check', async (req, res) => { // Current user's profile app.get('/api/users/me', requireAuth, async (req, res) => { try { - const user = await dbGet( + const user = await userDbGet( 'SELECT id, username, is_admin, address, latitude, longitude FROM users WHERE id = ?', [req.session.userId] ); @@ -495,12 +545,12 @@ app.put('/api/users/me', requireAuth, async (req, res) => { } } - await dbRun( + await userDbRun( 'UPDATE users SET address = ?, latitude = ?, longitude = ? WHERE id = ?', [addr, lat, lon, req.session.userId] ); - const user = await dbGet( + const user = await userDbGet( 'SELECT id, username, is_admin, address, latitude, longitude FROM users WHERE id = ?', [req.session.userId] ); diff --git a/docker-compose.yml b/docker-compose.yml index df16f3a..fc8943d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,16 @@ services: image: redis:alpine restart: unless-stopped + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_DB: farmafinder + POSTGRES_USER: farmafinder + POSTGRES_PASSWORD: ${PG_PASSWORD:-change-me-in-production} + volumes: + - postgres_data:/var/lib/postgresql/data + backend: image: git.hacecalor.net/ichitux/farmafinder-backend:latest build: @@ -21,10 +31,12 @@ services: REDIS_PASSWORD: ${REDIS_PASSWORD:-} DATABASE_PATH: /app/data/database.sqlite FARMACIAS_WEBHOOK_URL: ${FARMACIAS_WEBHOOK_URL:-} + PG_URL: postgresql://farmafinder:${PG_PASSWORD:-change-me-in-production}@postgres:5432/farmafinder volumes: - backend_data:/app/data depends_on: - redis + - postgres frontend: image: git.hacecalor.net/ichitux/farmafinder-frontend:latest @@ -37,3 +49,4 @@ services: volumes: backend_data: + postgres_data: