feat(backend): add user_consents table schema

This commit is contained in:
Antoni Nuñez Romeu
2026-08-26 13:30:21 +02:00
parent ae93c2efbd
commit bc689dad68
+62
View File
@@ -9,6 +9,7 @@ import axios from 'axios';
import redisClient from './redis-client.js';
import * as appMetrics from './src/metrics.js';
import cors from 'cors';
import helmet from 'helmet';
import sqlite3 from 'sqlite3';
import { promisify } from 'util';
import path from 'path';
@@ -39,6 +40,23 @@ const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3001;
// Security headers via helmet. Strict-Transport-Security is only sent when
// behind HTTPS (NODE_ENV=production). CSP is permissive enough for an API
// that serves JSON + serves the frontend from a different origin.
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
frameAncestors: ["'none'"],
},
},
crossOriginResourcePolicy: { policy: 'cross-origin' },
}));
// Structured JSON logger. The Pino OTel instrumentation attaches trace_id /
// span_id to every log line so they can be correlated in Grafana.
const logger = pino({
@@ -632,6 +650,50 @@ if (!pgPool) {
`);
await dbRun(`CREATE INDEX IF NOT EXISTS idx_pwd_reset_token ON password_reset_tokens(token)`);
}
// ========== USER CONSENTS ==========
if (pgPool) {
await pgPool.query(`
CREATE TABLE IF NOT EXISTS user_consents (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
session_id VARCHAR(255),
category VARCHAR(20) NOT NULL CHECK (category IN ('essential', 'analytics', 'preferences', 'health_data')),
granted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await pgPool.query(`
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_consents_user_cat
ON user_consents(user_id, category) WHERE user_id IS NOT NULL
`);
await pgPool.query(`
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_consents_sess_cat
ON user_consents(session_id, category) WHERE session_id IS NOT NULL
`);
await pgPool.query(`CREATE INDEX IF NOT EXISTS idx_user_consents_user_id ON user_consents(user_id) WHERE user_id IS NOT NULL`);
await pgPool.query(`CREATE INDEX IF NOT EXISTS idx_user_consents_session_id ON user_consents(session_id) WHERE session_id IS NOT NULL`);
}
if (!pgPool) {
await dbRun(`
CREATE TABLE IF NOT EXISTS user_consents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
session_id TEXT,
category TEXT NOT NULL CHECK (category IN ('essential', 'analytics', 'preferences', 'health_data')),
granted INTEGER NOT NULL DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
try {
await dbRun(`CREATE UNIQUE INDEX IF NOT EXISTS idx_user_consents_user_cat ON user_consents(user_id, category) WHERE user_id IS NOT NULL`);
} catch (e) { if (!/duplicate/i.test(e.message)) throw e; }
try {
await dbRun(`CREATE UNIQUE INDEX IF NOT EXISTS idx_user_consents_sess_cat ON user_consents(session_id, category) WHERE session_id IS NOT NULL`);
} catch (e) { if (!/duplicate/i.test(e.message)) throw e; }
}
} catch (err) {
console.error('initDatabase failed:', err);
throw err;