Privacy settings #60
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user