17ca6399e0
New /feedback API domain backed by its own FEEDBACK_DB, mirroring the Calendar domain's router -> service -> DB pool layering: - Public endpoints (no auth): eligible-events listing, event config, submission with honeypot + rate limiting (in-memory + DB backstop). - Admin endpoints (session-header auth, reusing Calendar's users/sessions via a swappable feedback.auth.ts boundary): events/songs/questions CRUD, bulk reorder/assignment, aggregated reporting, CSV export. - Schema in sql/feedback/001_init.sql (8 tables), applied and verified against the real FEEDBACK_DB. - 64 Jest tests covering validation, auth, rate limiting, CSV escaping, and report aggregation (pure functions, no DB needed). Includes fixes from a security review: path traversal defense doesn't apply here (that's the frontend proxy, separate repo), but the rate-limiter cluster does - recordSubmission now counts every processed request (not just successful ones), the in-memory Map evicts empty entries instead of growing unbounded, FEEDBACK_IP_SALT is required at boot instead of silently degrading to unsalted hashing, and submission answer/rating arrays are capped and de-duplicated to bound insert amplification. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
143 lines
7.7 KiB
SQL
143 lines
7.7 KiB
SQL
-- Nachklang e.V. Feedback module — initial schema for FEEDBACK_DB
|
|
-- Apply manually against the FEEDBACK_DB database (separate from CALENDAR_DB).
|
|
-- See nachklang-feedback/IMPLEMENTATION_PLAN.md §2 for the full rationale
|
|
-- behind every design decision below (snapshot columns, denormalisation,
|
|
-- absence-over-sentinels, hashed IPs only).
|
|
--
|
|
-- Apply with e.g.:
|
|
-- mysql -h <DB_HOST> -u <DB_USER> -p <FEEDBACK_DB> < 001_init.sql
|
|
|
|
USE `nachklang-feedback`;
|
|
|
|
-- 1. events -------------------------------------------------------------
|
|
CREATE TABLE events (
|
|
event_id INT AUTO_INCREMENT PRIMARY KEY,
|
|
slug VARCHAR(80) NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
subtitle VARCHAR(255) NULL,
|
|
event_date DATE NOT NULL,
|
|
feedback_deadline DATETIME NOT NULL,
|
|
is_published TINYINT(1) NOT NULL DEFAULT 0,
|
|
intro_text TEXT NULL,
|
|
created_by_email VARCHAR(255) NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
UNIQUE KEY uq_events_slug (slug),
|
|
KEY idx_events_eligibility (is_published, event_date, feedback_deadline)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
-- 2. songs (per-event setlist) ------------------------------------------
|
|
CREATE TABLE songs (
|
|
song_id INT AUTO_INCREMENT PRIMARY KEY,
|
|
event_id INT NOT NULL,
|
|
title VARCHAR(255) NOT NULL,
|
|
composer VARCHAR(255) NULL,
|
|
position INT NOT NULL DEFAULT 0,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_songs_event FOREIGN KEY (event_id) REFERENCES events(event_id) ON DELETE CASCADE,
|
|
KEY idx_songs_event_position (event_id, position)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
-- 3. questions (global reusable library) ---------------------------------
|
|
CREATE TABLE questions (
|
|
question_id INT AUTO_INCREMENT PRIMARY KEY,
|
|
label VARCHAR(500) NOT NULL,
|
|
help_text VARCHAR(500) NULL,
|
|
question_type ENUM('SONG_PICK','SONG_RATING','FREE_TEXT') NOT NULL,
|
|
is_archived TINYINT(1) NOT NULL DEFAULT 0,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
KEY idx_questions_archived_type (is_archived, question_type)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
-- 4. event_questions (join + ordering) -----------------------------------
|
|
CREATE TABLE event_questions (
|
|
event_question_id INT AUTO_INCREMENT PRIMARY KEY,
|
|
event_id INT NOT NULL,
|
|
question_id INT NOT NULL,
|
|
position INT NOT NULL DEFAULT 0,
|
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_eq_event FOREIGN KEY (event_id) REFERENCES events(event_id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_eq_question FOREIGN KEY (question_id) REFERENCES questions(question_id) ON DELETE RESTRICT,
|
|
UNIQUE KEY uq_eq_event_question (event_id, question_id),
|
|
KEY idx_eq_event_position (event_id, position, is_active)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
-- 5. submissions ----------------------------------------------------------
|
|
CREATE TABLE submissions (
|
|
submission_id INT AUTO_INCREMENT PRIMARY KEY,
|
|
event_id INT NOT NULL,
|
|
submitted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
ip_hash CHAR(64) NULL,
|
|
has_guestbook TINYINT(1) NOT NULL DEFAULT 0,
|
|
has_newsletter TINYINT(1) NOT NULL DEFAULT 0,
|
|
CONSTRAINT fk_sub_event FOREIGN KEY (event_id) REFERENCES events(event_id) ON DELETE CASCADE,
|
|
KEY idx_sub_event_time (event_id, submitted_at),
|
|
KEY idx_sub_iphash_time (ip_hash, submitted_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
-- 6. submission_answers ----------------------------------------------------
|
|
-- Deliberate denormalisation: question label/type and song title are
|
|
-- snapshotted at submission time so later edits to the question library
|
|
-- never retroactively change what a past submission means.
|
|
CREATE TABLE submission_answers (
|
|
answer_id INT AUTO_INCREMENT PRIMARY KEY,
|
|
submission_id INT NOT NULL,
|
|
event_id INT NOT NULL,
|
|
question_id INT NULL,
|
|
event_question_id INT NULL,
|
|
question_label_snapshot VARCHAR(500) NOT NULL,
|
|
question_type ENUM('SONG_PICK','SONG_RATING','FREE_TEXT') NOT NULL,
|
|
position_snapshot INT NOT NULL DEFAULT 0,
|
|
song_id INT NULL,
|
|
song_title_snapshot VARCHAR(255) NULL,
|
|
rating TINYINT NULL,
|
|
text_answer TEXT NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_ans_submission FOREIGN KEY (submission_id) REFERENCES submissions(submission_id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_ans_event FOREIGN KEY (event_id) REFERENCES events(event_id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_ans_question FOREIGN KEY (question_id) REFERENCES questions(question_id) ON DELETE SET NULL,
|
|
CONSTRAINT fk_ans_song FOREIGN KEY (song_id) REFERENCES songs(song_id) ON DELETE SET NULL,
|
|
CONSTRAINT chk_ans_rating CHECK (rating IS NULL OR (rating BETWEEN 1 AND 5)),
|
|
KEY idx_ans_submission (submission_id),
|
|
KEY idx_ans_report (event_id, question_id, song_id),
|
|
KEY idx_ans_type (event_id, question_type)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
-- 7. guest_book_entries -----------------------------------------------------
|
|
-- Private, admin-only. No public wall in v1.
|
|
CREATE TABLE guest_book_entries (
|
|
entry_id INT AUTO_INCREMENT PRIMARY KEY,
|
|
submission_id INT NOT NULL,
|
|
event_id INT NOT NULL,
|
|
display_name VARCHAR(255) NULL,
|
|
message TEXT NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_gb_submission FOREIGN KEY (submission_id) REFERENCES submissions(submission_id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_gb_event FOREIGN KEY (event_id) REFERENCES events(event_id) ON DELETE CASCADE,
|
|
KEY idx_gb_event_time (event_id, created_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
-- 8. newsletter_signups -----------------------------------------------------
|
|
CREATE TABLE newsletter_signups (
|
|
signup_id INT AUTO_INCREMENT PRIMARY KEY,
|
|
submission_id INT NOT NULL,
|
|
event_id INT NOT NULL,
|
|
first_name VARCHAR(120) NOT NULL,
|
|
last_name VARCHAR(120) NOT NULL,
|
|
email VARCHAR(255) NOT NULL,
|
|
consent_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
consent_text_version VARCHAR(40) NOT NULL,
|
|
sync_status ENUM('PENDING','SENT','FAILED','SKIPPED') NOT NULL DEFAULT 'PENDING',
|
|
sync_attempts INT NOT NULL DEFAULT 0,
|
|
synced_at DATETIME NULL,
|
|
external_id VARCHAR(120) NULL,
|
|
last_error TEXT NULL,
|
|
CONSTRAINT fk_nl_submission FOREIGN KEY (submission_id) REFERENCES submissions(submission_id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_nl_event FOREIGN KEY (event_id) REFERENCES events(event_id) ON DELETE CASCADE,
|
|
KEY idx_nl_sync_status (sync_status),
|
|
KEY idx_nl_email (email)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|