feat(auth): migrate user store from SQLite to PostgreSQL
Build & Push Docker Images / test-backend (push) Successful in 44s
Build & Push Docker Images / test-frontend (push) Successful in 53s
Build & Push Docker Images / build-backend (push) Failing after 50s
Build & Push Docker Images / build-frontend (push) Successful in 54s

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 <noreply@anthropic.com>
This commit is contained in:
Antoni Nuñez Romeu
2026-05-25 17:50:34 +02:00
parent 34733c121c
commit 31b3eb21b4
6 changed files with 319 additions and 96 deletions
+4
View File
@@ -6,6 +6,10 @@ REDIS_HOST=localhost
REDIS_PORT=6379 REDIS_PORT=6379
REDIS_PASSWORD= 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: # Web Push (VAPID). Generate with:
# node -e "import('web-push').then(w => console.log(w.default.generateVAPIDKeys()))" # node -e "import('web-push').then(w => console.log(w.default.generateVAPIDKeys()))"
VAPID_PUBLIC_KEY= VAPID_PUBLIC_KEY=
+58 -49
View File
@@ -3,27 +3,60 @@ import { promisify } from 'util';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import pg from 'pg';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const dbPath = path.join(__dirname, 'database.sqlite'); const PG_URL = process.env.PG_URL;
const db = new sqlite3.Database(dbPath);
// Custom wrapper to get lastID from db.run async function createAdmin() {
function dbRun(sql, params = []) { const username = process.env.ADMIN_USERNAME || 'admin';
return new Promise((resolve, reject) => { const password = process.env.ADMIN_PASSWORD || 'admin123';
db.run(sql, params, function(err) { const passwordHash = await bcrypt.hash(password, 10);
if (err) reject(err);
else resolve({ lastID: this.lastID, changes: this.changes }); 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()
)
`);
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;
} }
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)); const dbGet = promisify(db.get.bind(db));
// Initialize users table
async function initDatabase() {
try { try {
await dbRun(` await dbRun(`
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
@@ -33,56 +66,32 @@ async function initDatabase() {
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) )
`); `);
console.log('Database table initialized'); try { await dbRun('ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0'); } catch {}
} catch (error) {
console.error('Error initializing database:', error);
throw error;
}
}
async function createAdmin() {
try {
// Initialize database tables first
await initDatabase();
// Default admin credentials
const username = process.env.ADMIN_USERNAME || 'admin';
const password = process.env.ADMIN_PASSWORD || 'admin123';
// Check if admin already exists
const existing = await dbGet(
'SELECT * FROM users WHERE username = ?',
[username]
);
const existing = await dbGet('SELECT id FROM users WHERE username = ?', [username]);
if (existing) { if (existing) {
console.log(`Admin user '${username}' already exists.`); console.log(`Admin user '${username}' already exists.`);
console.log('To change the password, delete the user first and run this script again.'); console.log('To reset, delete the user first and re-run.');
db.close(); db.close();
return; return;
} }
// Hash password
const saltRounds = 10;
const passwordHash = await bcrypt.hash(password, saltRounds);
// Create admin user
await dbRun( await dbRun(
'INSERT INTO users (username, password_hash) VALUES (?, ?)', 'INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, 1)',
[username, passwordHash] [username, passwordHash]
); );
console.log(`Admin user '${username}' created in SQLite.`);
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 { } finally {
db.close(); db.close();
} }
} }
createAdmin(); console.log(`Username: ${username}`);
console.log(`Password: ${password}`);
console.log('\nIMPORTANT: Change the default password after first login!');
}
createAdmin().catch((err) => {
console.error('Error creating admin user:', err);
process.exit(1);
});
+145
View File
@@ -11,11 +11,13 @@
"dependencies": { "dependencies": {
"axios": "^1.6.0", "axios": "^1.6.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"connect-pg-simple": "^10.0.0",
"connect-sqlite3": "^0.9.16", "connect-sqlite3": "^0.9.16",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^8.5.2", "express-rate-limit": "^8.5.2",
"express-session": "^1.17.3", "express-session": "^1.17.3",
"pg": "^8.13.0",
"redis": "^4.6.0", "redis": "^4.6.0",
"sqlite3": "^5.1.6", "sqlite3": "^5.1.6",
"web-push": "^3.6.7" "web-push": "^3.6.7"
@@ -2134,6 +2136,17 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT" "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": { "node_modules/connect-sqlite3": {
"version": "0.9.16", "version": "0.9.16",
"resolved": "https://registry.npmjs.org/connect-sqlite3/-/connect-sqlite3-0.9.16.tgz", "resolved": "https://registry.npmjs.org/connect-sqlite3/-/connect-sqlite3-0.9.16.tgz",
@@ -4998,6 +5011,87 @@
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT" "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": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -5041,6 +5135,41 @@
"node": ">=8" "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": { "node_modules/prebuild-install": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
@@ -5712,6 +5841,14 @@
"source-map": "^0.6.0" "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": { "node_modules/sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "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": "^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": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+2
View File
@@ -20,7 +20,9 @@
"dependencies": { "dependencies": {
"axios": "^1.6.0", "axios": "^1.6.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"connect-pg-simple": "^10.0.0",
"connect-sqlite3": "^0.9.16", "connect-sqlite3": "^0.9.16",
"pg": "^8.13.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^8.5.2", "express-rate-limit": "^8.5.2",
+66 -16
View File
@@ -6,6 +6,8 @@ import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import session from 'express-session'; import session from 'express-session';
import connectSqlite3 from 'connect-sqlite3'; import connectSqlite3 from 'connect-sqlite3';
import connectPg from 'connect-pg-simple';
import pg from 'pg';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
import webpush from 'web-push'; import webpush from 'web-push';
@@ -38,7 +40,12 @@ app.use(cors({
})); }));
app.use(express.json()); 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 = { const sessionConfig = {
secret: process.env.SESSION_SECRET || 'farma-finder-secret-key-change-in-production', secret: process.env.SESSION_SECRET || 'farma-finder-secret-key-change-in-production',
@@ -52,8 +59,14 @@ const sessionConfig = {
}; };
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {
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 }); sessionConfig.store = new SQLiteStore({ db: 'sessions.sqlite', dir: __dirname });
} }
}
// Configure session // Configure session
app.use(session(sessionConfig)); app.use(session(sessionConfig));
@@ -90,6 +103,28 @@ function dbRun(sql, params = []) {
const dbAll = promisify(db.all.bind(db)); const dbAll = promisify(db.all.bind(db));
const dbGet = promisify(db.get.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 // Accept opening_hours as either an object or a JSON string from the client
// and return a TEXT value (or null) safe to persist. // and return a TEXT value (or null) safe to persist.
function serializeOpeningHours(value) { function serializeOpeningHours(value) {
@@ -152,7 +187,21 @@ async function initDatabase() {
await dbRun(`CREATE INDEX IF NOT EXISTS idx_pharmacy_medicine ON pharmacy_medicines(medicine_nregistro)`); await dbRun(`CREATE INDEX IF NOT EXISTS idx_pharmacy_medicine ON pharmacy_medicines(medicine_nregistro)`);
// Create users table for authentication // 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(` await dbRun(`
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -161,6 +210,7 @@ async function initDatabase() {
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) )
`); `);
}
await dbRun(` await dbRun(`
CREATE TABLE IF NOT EXISTS push_subscriptions ( 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)`); 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) 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 is_admin INTEGER NOT NULL DEFAULT 0'); } 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 address TEXT'); } catch {}
try { await dbRun('ALTER TABLE users ADD COLUMN latitude REAL'); } catch {} try { await dbRun('ALTER TABLE users ADD COLUMN latitude REAL'); } catch {}
try { await dbRun('ALTER TABLE users ADD COLUMN longitude 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) // Add user_id to push tables (SQLite — no cross-DB FK)
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 ADD COLUMN user_id INTEGER'); } catch {}
try { await dbRun('ALTER TABLE push_subscriptions_pharmacy 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'); } catch {}
console.log('Database initialized successfully'); console.log('Database initialized successfully');
} catch (error) { } catch (error) {
@@ -323,7 +373,7 @@ app.post('/api/auth/login', loginLimiter, async (req, res) => {
} }
// Find user by username // Find user by username
const user = await dbGet( const user = await userDbGet(
'SELECT * FROM users WHERE username = ?', 'SELECT * FROM users WHERE username = ?',
[username.trim()] [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' }); 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) { if (existing) {
return res.status(409).json({ error: 'Username already taken' }); return res.status(409).json({ error: 'Username already taken' });
} }
const passwordHash = await bcrypt.hash(p, 10); const passwordHash = await bcrypt.hash(p, 10);
const result = await dbRun( const result = await userDbRun(
'INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, 0)', 'INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, 0) RETURNING id',
[u, passwordHash] [u, passwordHash]
); );
@@ -422,7 +472,7 @@ app.post('/api/auth/logout', (req, res) => {
app.get('/api/auth/check', async (req, res) => { app.get('/api/auth/check', async (req, res) => {
try { try {
if (req.session && req.session.userId) { 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 = ?', 'SELECT id, username, is_admin, address, latitude, longitude FROM users WHERE id = ?',
[req.session.userId] [req.session.userId]
); );
@@ -452,7 +502,7 @@ app.get('/api/auth/check', async (req, res) => {
// Current user's profile // Current user's profile
app.get('/api/users/me', requireAuth, async (req, res) => { app.get('/api/users/me', requireAuth, async (req, res) => {
try { try {
const user = await dbGet( const user = await userDbGet(
'SELECT id, username, is_admin, address, latitude, longitude FROM users WHERE id = ?', 'SELECT id, username, is_admin, address, latitude, longitude FROM users WHERE id = ?',
[req.session.userId] [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 = ?', 'UPDATE users SET address = ?, latitude = ?, longitude = ? WHERE id = ?',
[addr, lat, lon, req.session.userId] [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 = ?', 'SELECT id, username, is_admin, address, latitude, longitude FROM users WHERE id = ?',
[req.session.userId] [req.session.userId]
); );
+13
View File
@@ -3,6 +3,16 @@ services:
image: redis:alpine image: redis:alpine
restart: unless-stopped 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: backend:
image: git.hacecalor.net/ichitux/farmafinder-backend:latest image: git.hacecalor.net/ichitux/farmafinder-backend:latest
build: build:
@@ -21,10 +31,12 @@ services:
REDIS_PASSWORD: ${REDIS_PASSWORD:-} REDIS_PASSWORD: ${REDIS_PASSWORD:-}
DATABASE_PATH: /app/data/database.sqlite DATABASE_PATH: /app/data/database.sqlite
FARMACIAS_WEBHOOK_URL: ${FARMACIAS_WEBHOOK_URL:-} FARMACIAS_WEBHOOK_URL: ${FARMACIAS_WEBHOOK_URL:-}
PG_URL: postgresql://farmafinder:${PG_PASSWORD:-change-me-in-production}@postgres:5432/farmafinder
volumes: volumes:
- backend_data:/app/data - backend_data:/app/data
depends_on: depends_on:
- redis - redis
- postgres
frontend: frontend:
image: git.hacecalor.net/ichitux/farmafinder-frontend:latest image: git.hacecalor.net/ichitux/farmafinder-frontend:latest
@@ -37,3 +49,4 @@ services:
volumes: volumes:
backend_data: backend_data:
postgres_data: