feat(backend): add user_consents table schema
This commit is contained in:
@@ -9,6 +9,7 @@ import axios from 'axios';
|
|||||||
import redisClient from './redis-client.js';
|
import redisClient from './redis-client.js';
|
||||||
import * as appMetrics from './src/metrics.js';
|
import * as appMetrics from './src/metrics.js';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
|
import helmet from 'helmet';
|
||||||
import sqlite3 from 'sqlite3';
|
import sqlite3 from 'sqlite3';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -39,6 +40,23 @@ const __dirname = path.dirname(__filename);
|
|||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3001;
|
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 /
|
// Structured JSON logger. The Pino OTel instrumentation attaches trace_id /
|
||||||
// span_id to every log line so they can be correlated in Grafana.
|
// span_id to every log line so they can be correlated in Grafana.
|
||||||
const logger = pino({
|
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)`);
|
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) {
|
} catch (err) {
|
||||||
console.error('initDatabase failed:', err);
|
console.error('initDatabase failed:', err);
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
Reference in New Issue
Block a user