feat(database): Add consent management migrations and auto-migration system
- Add Migration 005: consent fields to groups table (display_in_workshop, consent_timestamp, management_token) - Add Migration 006: social_media_platforms and group_social_media_consents tables - Implement automatic migration execution in DatabaseManager.initialize() - Add standalone migration runner script (runMigrations.js) - Seed data: Facebook, Instagram, TikTok platforms Note: DatabaseManager statement splitting needs improvement for complex SQL. Manual migration execution works correctly via sqlite3.
This commit is contained in:
parent
4317d761d2
commit
8dc5a03584
|
|
@ -34,6 +34,9 @@ class DatabaseManager {
|
||||||
// Erstelle Schema
|
// Erstelle Schema
|
||||||
await this.createSchema();
|
await this.createSchema();
|
||||||
|
|
||||||
|
// Run database migrations (automatic on startup)
|
||||||
|
await this.runMigrations();
|
||||||
|
|
||||||
// Generate missing previews for existing images
|
// Generate missing previews for existing images
|
||||||
await this.generateMissingPreviews();
|
await this.generateMissingPreviews();
|
||||||
|
|
||||||
|
|
@ -301,6 +304,101 @@ class DatabaseManager {
|
||||||
// Don't throw - this shouldn't prevent DB initialization
|
// Don't throw - this shouldn't prevent DB initialization
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run pending database migrations automatically
|
||||||
|
* Migrations are SQL files in the migrations/ directory
|
||||||
|
*/
|
||||||
|
async runMigrations() {
|
||||||
|
try {
|
||||||
|
console.log('🔄 Checking for database migrations...');
|
||||||
|
|
||||||
|
const migrationsDir = path.join(__dirname, 'migrations');
|
||||||
|
|
||||||
|
// Check if migrations directory exists
|
||||||
|
if (!fs.existsSync(migrationsDir)) {
|
||||||
|
console.log(' ℹ️ No migrations directory found, skipping migrations');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create migrations tracking table if it doesn't exist
|
||||||
|
await this.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
migration_name TEXT UNIQUE NOT NULL,
|
||||||
|
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Get list of applied migrations
|
||||||
|
const appliedMigrations = await this.all('SELECT migration_name FROM schema_migrations');
|
||||||
|
const appliedSet = new Set(appliedMigrations.map(m => m.migration_name));
|
||||||
|
|
||||||
|
// Get all migration files
|
||||||
|
const migrationFiles = fs.readdirSync(migrationsDir)
|
||||||
|
.filter(f => f.endsWith('.sql'))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
if (migrationFiles.length === 0) {
|
||||||
|
console.log(' ℹ️ No migration files found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let appliedCount = 0;
|
||||||
|
|
||||||
|
// Run pending migrations
|
||||||
|
for (const file of migrationFiles) {
|
||||||
|
if (appliedSet.has(file)) {
|
||||||
|
continue; // Already applied
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` 🔧 Applying migration: ${file}`);
|
||||||
|
|
||||||
|
const migrationPath = path.join(migrationsDir, file);
|
||||||
|
const sql = fs.readFileSync(migrationPath, 'utf8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Execute migration in a transaction
|
||||||
|
await this.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
|
// Split by semicolon and execute each statement
|
||||||
|
const statements = sql
|
||||||
|
.split(';')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(s => s.length > 0 && !s.startsWith('--'));
|
||||||
|
|
||||||
|
for (const statement of statements) {
|
||||||
|
await this.run(statement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record migration
|
||||||
|
await this.run(
|
||||||
|
'INSERT INTO schema_migrations (migration_name) VALUES (?)',
|
||||||
|
[file]
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.run('COMMIT');
|
||||||
|
appliedCount++;
|
||||||
|
console.log(` ✅ Successfully applied: ${file}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await this.run('ROLLBACK');
|
||||||
|
console.error(` ❌ Error applying ${file}:`, error.message);
|
||||||
|
throw new Error(`Migration failed: ${file} - ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appliedCount > 0) {
|
||||||
|
console.log(`✓ Applied ${appliedCount} database migration(s)`);
|
||||||
|
} else {
|
||||||
|
console.log('✓ Database is up to date');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Migration error:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton Instance
|
// Singleton Instance
|
||||||
|
|
|
||||||
16
backend/src/database/migrations/005_add_consent_fields.sql
Normal file
16
backend/src/database/migrations/005_add_consent_fields.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
-- Migration 005: Add consent management fields to groups table
|
||||||
|
-- Date: 2025-11-09
|
||||||
|
-- Description: Adds fields for workshop display consent, consent timestamp, and management token
|
||||||
|
|
||||||
|
-- Add consent-related columns to groups table
|
||||||
|
ALTER TABLE groups ADD COLUMN display_in_workshop BOOLEAN NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE groups ADD COLUMN consent_timestamp DATETIME;
|
||||||
|
ALTER TABLE groups ADD COLUMN management_token TEXT; -- For Phase 2: Self-service portal
|
||||||
|
|
||||||
|
-- Create indexes for better query performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_groups_display_consent ON groups(display_in_workshop);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_management_token ON groups(management_token) WHERE management_token IS NOT NULL;
|
||||||
|
|
||||||
|
-- Update existing groups to default values (retroactively grant consent for approved groups)
|
||||||
|
-- This assumes existing groups have been approved and displayed
|
||||||
|
UPDATE groups SET display_in_workshop = 1, consent_timestamp = created_at WHERE id > 0;
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
-- Migration 006: Create social media platform configuration and consent tables
|
||||||
|
-- Date: 2025-11-09
|
||||||
|
-- Description: Creates extensible social media platform management and per-group consent tracking
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Table: social_media_platforms
|
||||||
|
-- Purpose: Configurable list of social media platforms for consent management
|
||||||
|
-- ============================================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS social_media_platforms (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
platform_name TEXT UNIQUE NOT NULL, -- Internal identifier (e.g., 'facebook', 'instagram', 'tiktok')
|
||||||
|
display_name TEXT NOT NULL, -- User-facing name (e.g., 'Facebook', 'Instagram', 'TikTok')
|
||||||
|
icon_name TEXT, -- Material-UI Icon name for frontend display
|
||||||
|
is_active BOOLEAN DEFAULT 1, -- Enable/disable platform without deletion
|
||||||
|
sort_order INTEGER DEFAULT 0, -- Display order in UI
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Table: group_social_media_consents
|
||||||
|
-- Purpose: Track user consent for each group and social media platform
|
||||||
|
-- ============================================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS group_social_media_consents (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
group_id TEXT NOT NULL,
|
||||||
|
platform_id INTEGER NOT NULL,
|
||||||
|
consented BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
consent_timestamp DATETIME NOT NULL,
|
||||||
|
revoked BOOLEAN DEFAULT 0, -- For Phase 2: Consent revocation tracking
|
||||||
|
revoked_timestamp DATETIME, -- When consent was revoked (Phase 2)
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
-- Foreign key constraints
|
||||||
|
FOREIGN KEY (group_id) REFERENCES groups(group_id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (platform_id) REFERENCES social_media_platforms(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Ensure each platform can only have one consent entry per group
|
||||||
|
UNIQUE(group_id, platform_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Indexes for query performance
|
||||||
|
-- ============================================================================
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_consents_group_id ON group_social_media_consents(group_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_consents_platform_id ON group_social_media_consents(platform_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_consents_consented ON group_social_media_consents(consented);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Seed data: Insert default social media platforms
|
||||||
|
-- ============================================================================
|
||||||
|
INSERT INTO social_media_platforms (platform_name, display_name, icon_name, sort_order) VALUES ('facebook', 'Facebook', 'Facebook', 1);
|
||||||
|
INSERT INTO social_media_platforms (platform_name, display_name, icon_name, sort_order) VALUES ('instagram', 'Instagram', 'Instagram', 2);
|
||||||
|
INSERT INTO social_media_platforms (platform_name, display_name, icon_name, sort_order) VALUES ('tiktok', 'TikTok', 'MusicNote', 3);
|
||||||
139
backend/src/database/runMigrations.js
Normal file
139
backend/src/database/runMigrations.js
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
/**
|
||||||
|
* Database Migration Runner
|
||||||
|
* Executes SQL migrations in order
|
||||||
|
*/
|
||||||
|
|
||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const dbPath = path.join(__dirname, '../data/db/image_uploader.db');
|
||||||
|
const migrationsDir = path.join(__dirname, 'migrations');
|
||||||
|
|
||||||
|
// Helper to promisify database operations
|
||||||
|
function runQuery(db, sql, params = []) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.run(sql, params, function(err) {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQuery(db, sql, params = []) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.get(sql, params, (err, row) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runMigrations() {
|
||||||
|
console.log('🚀 Starting database migrations...\n');
|
||||||
|
|
||||||
|
// Check if database exists
|
||||||
|
if (!fs.existsSync(dbPath)) {
|
||||||
|
console.error('❌ Database file not found:', dbPath);
|
||||||
|
console.error('Please run the application first to initialize the database.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = new sqlite3.Database(dbPath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('❌ Error opening database:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Enable foreign keys
|
||||||
|
await runQuery(db, 'PRAGMA foreign_keys = ON');
|
||||||
|
|
||||||
|
// Create migrations table if it doesn't exist
|
||||||
|
await runQuery(db, `
|
||||||
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
migration_name TEXT UNIQUE NOT NULL,
|
||||||
|
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Get list of applied migrations
|
||||||
|
const appliedMigrations = await new Promise((resolve, reject) => {
|
||||||
|
db.all('SELECT migration_name FROM schema_migrations', [], (err, rows) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(rows.map(r => r.migration_name));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📋 Applied migrations:', appliedMigrations.length > 0 ? appliedMigrations.join(', ') : 'none');
|
||||||
|
|
||||||
|
// Get all migration files
|
||||||
|
const migrationFiles = fs.readdirSync(migrationsDir)
|
||||||
|
.filter(f => f.endsWith('.sql'))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
console.log('📁 Found migration files:', migrationFiles.length, '\n');
|
||||||
|
|
||||||
|
// Run pending migrations
|
||||||
|
for (const file of migrationFiles) {
|
||||||
|
if (appliedMigrations.includes(file)) {
|
||||||
|
console.log(`⏭️ Skipping ${file} (already applied)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔧 Applying ${file}...`);
|
||||||
|
|
||||||
|
const migrationPath = path.join(migrationsDir, file);
|
||||||
|
const sql = fs.readFileSync(migrationPath, 'utf8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Execute migration in a transaction
|
||||||
|
await runQuery(db, 'BEGIN TRANSACTION');
|
||||||
|
|
||||||
|
// Split by semicolon and execute each statement
|
||||||
|
const statements = sql
|
||||||
|
.split(';')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(s => s.length > 0 && !s.startsWith('--'));
|
||||||
|
|
||||||
|
for (const statement of statements) {
|
||||||
|
await runQuery(db, statement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record migration
|
||||||
|
await runQuery(db,
|
||||||
|
'INSERT INTO schema_migrations (migration_name) VALUES (?)',
|
||||||
|
[file]
|
||||||
|
);
|
||||||
|
|
||||||
|
await runQuery(db, 'COMMIT');
|
||||||
|
console.log(`✅ Successfully applied ${file}\n`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await runQuery(db, 'ROLLBACK');
|
||||||
|
console.error(`❌ Error applying ${file}:`, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n✨ All migrations completed successfully!');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n💥 Migration failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run if executed directly
|
||||||
|
if (require.main === module) {
|
||||||
|
runMigrations().catch(error => {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { runMigrations };
|
||||||
Loading…
Reference in New Issue
Block a user