feat: add new legal document and database schema for module L compliance
Co-authored-by: cetinmustafa83 <30570791+cetinmustafa83@users.noreply.github.com>
This commit is contained in:
24 files changed
+6565
-1
No files matched your search
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Generate DATA_MODEL.md from Prisma schema and database config
|
||||
* This document lists all data entities, their purposes, legal basis, and retention categories
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
interface DataEntity {
|
||||
name: string;
|
||||
dataCategories: string[];
|
||||
legalBasis: string;
|
||||
purpose: string;
|
||||
retentionCategory: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
// Core data model definition
|
||||
const dataModel: DataEntity[] = [
|
||||
{
|
||||
name: "User",
|
||||
dataCategories: ["firstName", "lastName", "email", "role", "createdAt"],
|
||||
legalBasis: "GDPR Art. 6(1)(c) - Legal obligation (Schulgesetz)",
|
||||
purpose: "User account management and authentication",
|
||||
retentionCategory: "TEACHER_AUDIT_LOG",
|
||||
notes: "Retained per employment contract or student enrollment period",
|
||||
},
|
||||
{
|
||||
name: "Student",
|
||||
dataCategories: [
|
||||
"firstName",
|
||||
"lastName",
|
||||
"dateOfBirth",
|
||||
"email",
|
||||
"phone",
|
||||
"address",
|
||||
],
|
||||
legalBasis: "GDPR Art. 6(1)(c) - Legal obligation (Schulgesetz)",
|
||||
purpose: "Student record management and communication",
|
||||
retentionCategory: "LEARNING_PROGRESS",
|
||||
},
|
||||
{
|
||||
name: "ClassGroup",
|
||||
dataCategories: ["name", "grade", "year", "studentCount"],
|
||||
legalBasis: "GDPR Art. 6(1)(c) - Legal obligation (Schulgesetz)",
|
||||
purpose: "Class organization and grouping",
|
||||
retentionCategory: "LEARNING_PROGRESS",
|
||||
},
|
||||
{
|
||||
name: "Assessment",
|
||||
dataCategories: ["studentId", "subjectId", "score", "feedback", "date"],
|
||||
legalBasis:
|
||||
"GDPR Art. 6(1)(a) - Consent + Art. 8 (children) - Parental consent",
|
||||
purpose: "Record student learning progress and performance",
|
||||
retentionCategory: "ASSESSMENT_RESULT",
|
||||
},
|
||||
{
|
||||
name: "Report (Final Grade Report)",
|
||||
dataCategories: ["studentId", "finalGrades", "comments", "semester"],
|
||||
legalBasis:
|
||||
"GDPR Art. 6(1)(c) - Legal obligation (Schulgesetz - long-term retention requirement)",
|
||||
purpose: "Permanent record of student academic achievement",
|
||||
retentionCategory: "FINAL_REPORT",
|
||||
notes: "Typically retained for student lifetime + 30 years (per Bundesland law)",
|
||||
},
|
||||
{
|
||||
name: "BehaviorIncident",
|
||||
dataCategories: ["studentId", "incidentType", "description", "resolution"],
|
||||
legalBasis:
|
||||
"GDPR Art. 6(1)(a) - Consent + Art. 9(2)(h) - Health/safety necessity",
|
||||
purpose: "Track disciplinary actions and behavioral support",
|
||||
retentionCategory: "DISCIPLINARY_RECORD",
|
||||
notes: "Retention varies by Bundesland; typically 3-10 years",
|
||||
},
|
||||
{
|
||||
name: "ExamIncidentEvent",
|
||||
dataCategories: ["studentId", "eventType", "timestamp", "notes"],
|
||||
legalBasis:
|
||||
"GDPR Art. 6(1)(a) - Consent + Art. 9(2)(h) - Exam integrity necessity",
|
||||
purpose: "Log exam-time incidents (distress, technical issues)",
|
||||
retentionCategory: "EXAM_INCIDENT",
|
||||
notes: "Shorter retention (6-12 months) unless escalated to disciplinary record",
|
||||
},
|
||||
{
|
||||
name: "Message",
|
||||
dataCategories: ["senderId", "recipientId", "content", "timestamp"],
|
||||
legalBasis: "GDPR Art. 6(1)(a) - Consent (user opt-in for messaging)",
|
||||
purpose: "Communication between teachers, parents, and students",
|
||||
retentionCategory: "MESSAGE",
|
||||
},
|
||||
{
|
||||
name: "AuditLog",
|
||||
dataCategories: ["userId", "action", "entityType", "timestamp"],
|
||||
legalBasis: "GDPR Art. 6(1)(c) - Legal obligation (data protection compliance)",
|
||||
purpose: "Audit trail for regulatory compliance and breach investigation",
|
||||
retentionCategory: "TEACHER_AUDIT_LOG",
|
||||
},
|
||||
{
|
||||
name: "Notebook (Student Learning Notebooks)",
|
||||
dataCategories: ["studentId", "content", "createdAt"],
|
||||
legalBasis: "GDPR Art. 6(1)(a) - Consent (student/parent consent)",
|
||||
purpose: "Student learning portfolio and reflection entries",
|
||||
retentionCategory: "NOTEBOOK_PAGE",
|
||||
},
|
||||
{
|
||||
name: "AITutorLog",
|
||||
dataCategories: ["studentId", "interaction", "timestamp", "response"],
|
||||
legalBasis: "GDPR Art. 6(1)(a) - Explicit consent + Art. 22 - Automated decisions",
|
||||
purpose: "Track AI tutor interactions for learning analytics",
|
||||
retentionCategory: "AI_TUTOR_LOG",
|
||||
notes: "Subject to Art. 22 GDPR - no solely automated grading decisions",
|
||||
},
|
||||
];
|
||||
|
||||
export function generateDataModel(outputPath: string = "docs/legal"): void {
|
||||
if (!fs.existsSync(outputPath)) {
|
||||
fs.mkdirSync(outputPath, { recursive: true });
|
||||
}
|
||||
|
||||
const lines: string[] = [
|
||||
"# Data Model Documentation",
|
||||
"",
|
||||
"**Status**: Schema Reference for ExaMetra (Module L)",
|
||||
"",
|
||||
"This document outlines all personal data entities collected by ExaMetra, their legal basis, purpose, and retention category.",
|
||||
"",
|
||||
"## Core Entities",
|
||||
"",
|
||||
"| Entity | Data Categories | Legal Basis | Purpose | Retention Category | Notes |",
|
||||
"|--------|-----------------|------------|---------|-------------------|-------|",
|
||||
];
|
||||
|
||||
for (const entity of dataModel) {
|
||||
const dataCategories = entity.dataCategories.join(", ");
|
||||
const notes = entity.notes ? `${entity.notes}` : "—";
|
||||
const row = `| **${entity.name}** | ${dataCategories} | ${entity.legalBasis} | ${entity.purpose} | **${entity.retentionCategory}** | ${notes} |`;
|
||||
lines.push(row);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
"",
|
||||
"## Retention Categories",
|
||||
"",
|
||||
"| Category | Typical Retention | Legal Basis |",
|
||||
"|----------|------------------|------------|",
|
||||
"| LEARNING_PROGRESS | 1-2 years | Bundesland school law (Schulgesetz) |",
|
||||
"| ASSESSMENT_RESULT | 1-2 years | Bundesland school law |",
|
||||
"| FINAL_REPORT | 30+ years | Permanent record under Schulgesetz |",
|
||||
"| DISCIPLINARY_RECORD | 3-10 years | Varies by Bundesland |",
|
||||
"| EXAM_INCIDENT | 6-12 months | Exam integrity + school safety |",
|
||||
"| MESSAGE | 1-2 years | Communication retention best practices |",
|
||||
"| AI_TUTOR_LOG | 6-12 months | Pedagogical purpose + learning analytics |",
|
||||
"| NOTEBOOK_PAGE | 1-2 years | Student portfolio / learning records |",
|
||||
"| SIGNAGE_LOG | 3 months | Safety monitoring (emergency only) |",
|
||||
"| TEACHER_AUDIT_LOG | 1-2 years | Compliance and audit trail |",
|
||||
"",
|
||||
"## Compliance Notes",
|
||||
"",
|
||||
"- **DPO Review**: Each retention period must be reviewed and approved by your school's Data Protection Officer (DPO)",
|
||||
"- **Bundesland Variations**: Retention periods may vary by state (Bundesland). Consult your regional school authority.",
|
||||
"- **Parental Consent**: For student data (especially minors), parental consent is required per Art. 8 GDPR",
|
||||
"- **Automated Decisions**: No solely automated decisions (e.g., automatic grade lowering) without human review per Art. 22 GDPR",
|
||||
"- **Data Subject Rights**: All entities are subject to GDPR rights: access, correction, erasure (where legally permitted), portability",
|
||||
"",
|
||||
"## Generated From",
|
||||
"",
|
||||
`Generated: ${new Date().toISOString()}`,
|
||||
"Source: ExaMetra Prisma schema (Module L)",
|
||||
];
|
||||
|
||||
const filePath = path.join(outputPath, "DATA_MODEL.md");
|
||||
fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
|
||||
console.log(`[v0] Generated DATA_MODEL.md at ${filePath}`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
generateDataModel();
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Generate Datenschutzerklärung (Privacy Notice) for parents
|
||||
* Plain-language German, based on school configuration
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
interface PrivacyNoticeParams {
|
||||
schoolName: string;
|
||||
dpoName: string;
|
||||
dpoEmail: string;
|
||||
schultragerName: string;
|
||||
bundesland: string;
|
||||
}
|
||||
|
||||
export function generatePrivacyNotice(
|
||||
params: PrivacyNoticeParams,
|
||||
outputPath: string = "docs/legal"
|
||||
): void {
|
||||
if (!fs.existsSync(outputPath)) {
|
||||
fs.mkdirSync(outputPath, { recursive: true });
|
||||
}
|
||||
|
||||
const lines: string[] = [
|
||||
"# Datenschutzerklärung",
|
||||
"# (Privacy Notice for Parents & Students)",
|
||||
"",
|
||||
`**Schule**: ${params.schoolName}`,
|
||||
`**Bundesland**: ${params.bundesland}`,
|
||||
`**Schulträger**: ${params.schultragerName}`,
|
||||
`**Datenschutzbeauftragter (DPO)**: ${params.dpoName} (${params.dpoEmail})`,
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"## 1. Wer ist verantwortlich für die Verarbeitung Ihrer Daten?",
|
||||
"",
|
||||
`Die **${params.schoolName}** ist die Schule, die ihre Daten verarbeitet.`,
|
||||
`Der Schulträger ist: **${params.schultragerName}**`,
|
||||
"",
|
||||
"Der Datenschutzbeauftragte (DPO) ist:",
|
||||
`- **Name**: ${params.dpoName}`,
|
||||
`- **Email**: ${params.dpoEmail}`,
|
||||
"",
|
||||
"Sie können den DPO jederzeit kontaktieren, wenn Sie Fragen zu Ihren Daten haben.",
|
||||
"",
|
||||
"## 2. Welche Daten werden verarbeitet?",
|
||||
"",
|
||||
"Wir verarbeiten folgende personenbezogene Daten:",
|
||||
"",
|
||||
"### Schülerdaten",
|
||||
"- Name, Vorname, Geburtsdatum",
|
||||
"- Kontaktinformationen (Email, Telefon)",
|
||||
"- Adresse",
|
||||
"- Schulleistungen, Noten, Zeugnisse",
|
||||
"- Verhalten und Disziplin",
|
||||
"- Medizinische Informationen (falls relevant)",
|
||||
"",
|
||||
"### Lehrerdaten",
|
||||
"- Name, Email, Telefon",
|
||||
"- Unterrichtsvertrag und Stundenpläne",
|
||||
"- Aktivitätsprotokoll (Audit Logs)",
|
||||
"",
|
||||
"### Elterndaten",
|
||||
"- Name, Kontaktinformationen",
|
||||
"- Nachrichten mit Lehrern",
|
||||
"",
|
||||
"## 3. Warum verarbeiten wir Ihre Daten?",
|
||||
"",
|
||||
"Wir verarbeiten Ihre Daten für folgende Zwecke:",
|
||||
"",
|
||||
"- **Schulverwaltung**: Verwaltung von Schülereinträgen und Klassengruppen",
|
||||
"- **Unterricht & Bewertung**: Verfolgung von Lernfortschritten und Noten",
|
||||
"- **Kommunikation**: Nachrichten zwischen Lehrern, Eltern und Schülern",
|
||||
"- **Sicherheit**: Überwachung von Prüfungen und Schulsicherheit",
|
||||
"- **Rechtliche Verpflichtungen**: Einhaltung des Schulgesetzes und der DSGVO",
|
||||
"",
|
||||
"## 4. Was ist die Rechtsgrundlage?",
|
||||
"",
|
||||
"Die Verarbeitung erfolgt auf Grundlage von:",
|
||||
"",
|
||||
"- **Schulgesetz** (Schulgesetz des Bundeslandes): Rechtliche Verpflichtung zur Führung von Schülerdaten",
|
||||
"- **DSGVO Art. 6(1)(a)**: Ihre Zustimmung (z.B. für Messaging)",
|
||||
"- **DSGVO Art. 6(1)(c)**: Erfüllung rechtlicher Verpflichtungen",
|
||||
"- **DSGVO Art. 9(2)(h)**: Zwecke der Gesundheit und Sicherheit (z.B. Wellness-Überprüfungen)",
|
||||
"",
|
||||
"Für Minderjährige: **Eltern erteilen die Zustimmung** gemäß DSGVO Art. 8.",
|
||||
"",
|
||||
"## 5. Wie lange werden die Daten gespeichert?",
|
||||
"",
|
||||
"Die Speicherdauer hängt von der Datenkategorie ab:",
|
||||
"",
|
||||
"| Datentyp | Speicherdauer |",
|
||||
"|----------|--------------|",
|
||||
"| Lernfortschritt | 1–2 Jahre nach Schulabgang |",
|
||||
"| Zeugnisse/Endbewertungen | Dauerhaft (Schulgesetz erforderlich) |",
|
||||
"| Disziplinarberichte | 3–10 Jahre (je nach Bundesland) |",
|
||||
"| Nachrichten | 1–2 Jahre |",
|
||||
"| Audit Logs | 1–2 Jahre |",
|
||||
"",
|
||||
"Nach Ablauf der Aufbewahrungsfrist werden Ihre Daten gelöscht.",
|
||||
"",
|
||||
"## 6. Ihre Rechte",
|
||||
"",
|
||||
"Sie haben folgende Rechte unter der DSGVO:",
|
||||
"",
|
||||
"- **Auskunftsrecht** (Art. 15): Sie können fragen, welche Daten wir speichern",
|
||||
"- **Berichtigungsrecht** (Art. 16): Sie können falsche Daten korrigieren lassen",
|
||||
"- **Löschungsrecht** (Art. 17): Sie können verlangen, dass Ihre Daten gelöscht werden (sofern nicht gesetzlich erforderlich)",
|
||||
"- **Datenportabilität** (Art. 20): Sie können Ihre Daten exportieren",
|
||||
"- **Beschwerderecht**: Sie können sich an die Datenschutzbehörde beschweren",
|
||||
"",
|
||||
"Um diese Rechte auszuüben, kontaktieren Sie bitte den DPO.",
|
||||
"",
|
||||
"## 7. Sicherheit Ihrer Daten",
|
||||
"",
|
||||
"Wir schützen Ihre Daten durch:",
|
||||
"",
|
||||
"- Verschlüsselung (TLS) bei der Übertragung",
|
||||
"- Verschlüsselung bei der Speicherung",
|
||||
"- Rollenbasierte Zugriffskontrolle",
|
||||
"- Regelmäßige Sicherheitsprüfungen",
|
||||
"- Audit-Logs aller Datenzugriffe",
|
||||
"",
|
||||
"## 8. Kontakt",
|
||||
"",
|
||||
`Bei Fragen zum Datenschutz kontaktieren Sie bitte:`,
|
||||
`- **DPO**: ${params.dpoName}`,
|
||||
`- **Email**: ${params.dpoEmail}`,
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
`**Gültig ab**: ${new Date().toISOString().split("T")[0]}`,
|
||||
"**Generiertaus**: ExaMetra Module L Compliance Framework",
|
||||
];
|
||||
|
||||
const filePath = path.join(outputPath, "privacy-notice-de.md");
|
||||
fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
|
||||
console.log(`[v0] Generated privacy notice at ${filePath}`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
generatePrivacyNotice({
|
||||
schoolName: "Gymnasium Berlin",
|
||||
dpoName: "Max Mustermann",
|
||||
dpoEmail: "dpo@gymnasium-berlin.de",
|
||||
schultragerName: "Stadt Berlin",
|
||||
bundesland: "BERLIN",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Generate Records of Processing Activities (Verzeichnis von Verarbeitungstätigkeiten)
|
||||
* Compliant with GDPR Art. 30 requirements
|
||||
* German legal document for school's DPO review
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
interface RopaEntry {
|
||||
name: string;
|
||||
purposes: string[];
|
||||
dataCategories: string[];
|
||||
recipients: string[];
|
||||
retentionPeriod: string;
|
||||
legalBasis: string;
|
||||
securityMeasures: string[];
|
||||
}
|
||||
|
||||
const ropas: RopaEntry[] = [
|
||||
{
|
||||
name: "Student Information Management",
|
||||
purposes: [
|
||||
"Student enrollment and record maintenance",
|
||||
"Educational progress tracking",
|
||||
"Communication with parents/guardians",
|
||||
],
|
||||
dataCategories: ["Name", "DOB", "Contact information", "Academic records"],
|
||||
recipients: ["Teachers", "School administration", "Parents/guardians"],
|
||||
retentionPeriod: "Duration of enrollment + 2 years",
|
||||
legalBasis:
|
||||
"GDPR Art. 6(1)(c) - Legal obligation under Schulgesetz (School Law)",
|
||||
securityMeasures: [
|
||||
"Role-based access control (RBAC)",
|
||||
"Encryption at rest and in transit",
|
||||
"Audit logging of all data access",
|
||||
"Regular security audits",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Learning Progress & Assessment",
|
||||
purposes: [
|
||||
"Track student learning outcomes",
|
||||
"Generate report cards and grades",
|
||||
"Identify learning support needs",
|
||||
],
|
||||
dataCategories: [
|
||||
"Assessment scores",
|
||||
"Teacher feedback",
|
||||
"Learning competencies",
|
||||
],
|
||||
recipients: ["Teachers", "School administration", "Parents"],
|
||||
retentionPeriod: "1-2 years per Bundesland regulations",
|
||||
legalBasis:
|
||||
"GDPR Art. 6(1)(a) - Consent + Art. 9(2)(h) - Educational necessity",
|
||||
securityMeasures: [
|
||||
"Pseudonymization of assessment data where possible",
|
||||
"Teacher-student privacy boundaries",
|
||||
"Parent-accessible portal with consent gates",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "AI Tutor Processing",
|
||||
purposes: [
|
||||
"Personalized learning recommendations",
|
||||
"Adaptive tutoring content delivery",
|
||||
"Learning analytics and reporting",
|
||||
],
|
||||
dataCategories: [
|
||||
"Student interaction logs",
|
||||
"Learning preferences",
|
||||
"Performance metrics",
|
||||
"AI model inputs/outputs",
|
||||
],
|
||||
recipients: ["AI service provider (if applicable)", "Teachers", "Students"],
|
||||
retentionPeriod: "6-12 months after course completion",
|
||||
legalBasis:
|
||||
"GDPR Art. 6(1)(a) - Explicit consent + Art. 22 - Automated decision safeguards",
|
||||
securityMeasures: [
|
||||
"Data minimization: only essential features processed",
|
||||
"Transparency logs for AI decisions",
|
||||
"Human-in-the-loop review for grade-affecting recommendations",
|
||||
"Right to explanation mechanism",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Exam Monitoring & Integrity",
|
||||
purposes: [
|
||||
"Exam room monitoring for security",
|
||||
"Incident logging (distress signals, technical issues)",
|
||||
"Academic integrity verification",
|
||||
],
|
||||
dataCategories: [
|
||||
"Exam room events",
|
||||
"Student signals/requests",
|
||||
"Technical logs",
|
||||
"Audio/video (if applicable)",
|
||||
],
|
||||
recipients: ["Exam proctors", "School administration"],
|
||||
retentionPeriod: "6-12 months (shorter if no incident)",
|
||||
legalBasis:
|
||||
"GDPR Art. 6(1)(a) - Consent + Art. 9(2)(h) - Exam security necessity",
|
||||
securityMeasures: [
|
||||
"Limited access to exam data (proctors only)",
|
||||
"Video/audio encrypted and retained separately",
|
||||
"Automated deletion of non-incident logs after retention period",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Audit & Compliance Logging",
|
||||
purposes: [
|
||||
"Regulatory compliance (GDPR, Schulgesetz)",
|
||||
"Data breach investigation",
|
||||
"User accountability and audit trail",
|
||||
],
|
||||
dataCategories: ["User ID", "Action taken", "Timestamp", "Entity accessed"],
|
||||
recipients: ["DPO", "School administration", "Schulträger (if requested)"],
|
||||
retentionPeriod: "1-2 years",
|
||||
legalBasis: "GDPR Art. 6(1)(c) - Legal obligation (compliance duty)",
|
||||
securityMeasures: [
|
||||
"Immutable audit logs",
|
||||
"Restricted DPO-only access",
|
||||
"Regular integrity verification",
|
||||
"Automated flagging of suspicious access patterns",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function generateRopa(outputPath: string = "docs/legal"): void {
|
||||
if (!fs.existsSync(outputPath)) {
|
||||
fs.mkdirSync(outputPath, { recursive: true });
|
||||
}
|
||||
|
||||
const lines: string[] = [
|
||||
"# Verzeichnis von Verarbeitungstätigkeiten",
|
||||
"# (Records of Processing Activities - Art. 30 GDPR)",
|
||||
"",
|
||||
"**Schulname**: [To be filled by school]",
|
||||
"**DPO**: [To be filled by school]",
|
||||
"**Schulträger**: [To be filled by school]",
|
||||
"**Erstellungsdatum**: " + new Date().toISOString(),
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"## Einleitung",
|
||||
"",
|
||||
"Dieses Verzeichnis dokumentiert alle Verarbeitungstätigkeiten personenbezogener Daten durch ExaMetra im Rahmen des deutschen Schulgesetzes (Schulgesetz/BDSG-SchulG) und der DSGVO.",
|
||||
"",
|
||||
"**HINWEIS**: Dies ist eine Vorlage. Ihre Schule muss dieses Dokument durch ihren Datenschutzbeauftragten überprüfen und finalisieren, bevor es als offizielle Dokumentation gilt.",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
];
|
||||
|
||||
for (let i = 0; i < ropas.length; i++) {
|
||||
const ropa = ropas[i];
|
||||
lines.push(
|
||||
`## Verarbeitungstätigkeit ${i + 1}: ${ropa.name}`,
|
||||
"",
|
||||
"### Zwecke der Verarbeitung",
|
||||
...ropa.purposes.map((p) => `- ${p}`),
|
||||
"",
|
||||
"### Kategorien personenbezogener Daten",
|
||||
...ropa.dataCategories.map((c) => `- ${c}`),
|
||||
"",
|
||||
"### Kategorien von Empfängern",
|
||||
...ropa.recipients.map((r) => `- ${r}`),
|
||||
"",
|
||||
"### Speicherdauer",
|
||||
`${ropa.retentionPeriod}`,
|
||||
"",
|
||||
"### Rechtsgrundlage",
|
||||
`${ropa.legalBasis}`,
|
||||
"",
|
||||
"### Technische und organisatorische Maßnahmen",
|
||||
...ropa.securityMeasures.map((m) => `- ${m}`),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
"---",
|
||||
"",
|
||||
"## Betroffenenrechte (DSGVO Art. 12-22)",
|
||||
"",
|
||||
"Alle Personen haben das Recht auf:",
|
||||
"- **Auskunft** (Art. 15): Zugang zu ihren Daten",
|
||||
"- **Berichtigung** (Art. 16): Korrektur ungenauer Daten",
|
||||
"- **Löschung** (Art. 17): Unter rechtlichen Bedingungen",
|
||||
"- **Datenportabilität** (Art. 20): Export ihrer Daten",
|
||||
"- **Widerspruch** (Art. 21): Gegen bestimmte Verarbeitungen",
|
||||
"",
|
||||
"Anträge richten Sie an den Datenschutzbeauftragten (DPO).",
|
||||
"",
|
||||
"## Datenschutzverletzungen",
|
||||
"",
|
||||
"Bei Verdacht auf eine Datenschutzverletzung kontaktieren Sie sofort den DPO und die zuständige Aufsichtsbehörde.",
|
||||
"",
|
||||
`**Generiert**: ${new Date().toISOString()}`,
|
||||
"**Quelle**: ExaMetra Module L - Compliance Framework",
|
||||
];
|
||||
|
||||
const filePath = path.join(
|
||||
outputPath,
|
||||
"verzeichnis-verarbeitungstaetigkeiten.md"
|
||||
);
|
||||
fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
|
||||
console.log(
|
||||
`[v0] Generated RoPA (Verzeichnis) at ${filePath}`
|
||||
);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
generateRopa();
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"BERLIN": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 30,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
},
|
||||
"BAYERN": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 35,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
},
|
||||
"BADEN_WUERTTEMBERG": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 30,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
},
|
||||
"HESSE": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 30,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
},
|
||||
"NORTH_RHINE_WESTPHALIA": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 30,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
},
|
||||
"LOWER_SAXONY": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 30,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
},
|
||||
"BREMEN": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 30,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
},
|
||||
"HAMBURG": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 30,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
},
|
||||
"RHINELAND_PALATINATE": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 30,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
},
|
||||
"SAARLAND": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 30,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
},
|
||||
"SAXONY": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 30,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
},
|
||||
"SAXONY_ANHALT": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 30,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
},
|
||||
"SCHLESWIG_HOLSTEIN": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 30,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
},
|
||||
"THURINGIA": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 30,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
},
|
||||
"MECKLENBURG_VORPOMMERN": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 30,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
},
|
||||
"BRANDENBURG": {
|
||||
"LEARNING_PROGRESS": 12,
|
||||
"ASSESSMENT_RESULT": 12,
|
||||
"FINAL_REPORT": 30,
|
||||
"DISCIPLINARY_RECORD": 5,
|
||||
"EXAM_INCIDENT": 6,
|
||||
"MESSAGE": 12,
|
||||
"AI_TUTOR_LOG": 6,
|
||||
"NOTEBOOK_PAGE": 12,
|
||||
"SIGNAGE_LOG": 3,
|
||||
"TEACHER_AUDIT_LOG": 12
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* Daily Retention Enforcement Job
|
||||
* Flags records that have exceeded their retention period
|
||||
* Called once per day; can be integrated with cron (e.g., GitHub Actions, node-schedule)
|
||||
*
|
||||
* Usage: npx ts-node scripts/run-retention-job.ts
|
||||
*/
|
||||
|
||||
const { PrismaClient } = require("@prisma/client");
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
interface FlaggedRecord {
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
dataCategory: string;
|
||||
flaggedAt: Date;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check records for each school and flag those exceeding retention period
|
||||
*/
|
||||
async function runRetentionJob() {
|
||||
console.log("[v0] Starting retention enforcement job...");
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Get all schools
|
||||
const schools = await prisma.school.findMany({
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
|
||||
let totalFlagged = 0;
|
||||
const report: { school: string; flagged: number }[] = [];
|
||||
|
||||
for (const school of schools) {
|
||||
const schoolFlagged = await flagExpiredRecordsForSchool(school.id);
|
||||
totalFlagged += schoolFlagged;
|
||||
report.push({ school: school.name, flagged: schoolFlagged });
|
||||
|
||||
console.log(
|
||||
`[v0] School "${school.name}": ${schoolFlagged} records flagged`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[v0] Retention job completed: ${totalFlagged} total records flagged`
|
||||
);
|
||||
console.log(`[v0] Duration: ${Date.now() - startTime}ms`);
|
||||
|
||||
// Log retention job to audit
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
action: "RETENTION_JOB_EXECUTED",
|
||||
entityType: "RetentionJob",
|
||||
metadata: JSON.stringify({
|
||||
totalFlagged,
|
||||
schoolsProcessed: schools.length,
|
||||
timestamp: new Date().toISOString(),
|
||||
report,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, totalFlagged, report };
|
||||
} catch (error) {
|
||||
console.error("[v0] Error in retention job:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag expired records for a specific school
|
||||
*/
|
||||
async function flagExpiredRecordsForSchool(schoolId: string): Promise<number> {
|
||||
// Get all retention policies for this school
|
||||
const policies = await prisma.dataRetentionPolicy.findMany({
|
||||
where: { schoolId },
|
||||
});
|
||||
|
||||
let flaggedCount = 0;
|
||||
|
||||
for (const policy of policies) {
|
||||
if (!policy.retentionPeriodMonths) continue;
|
||||
|
||||
// Calculate expiry date
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setMonth(expiryDate.getMonth() - policy.retentionPeriodMonths);
|
||||
|
||||
// Flag expired records based on category
|
||||
const categoryFlagged = await flagRecordsByCategory(
|
||||
schoolId,
|
||||
policy.dataCategory,
|
||||
expiryDate
|
||||
);
|
||||
|
||||
flaggedCount += categoryFlagged;
|
||||
}
|
||||
|
||||
return flaggedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag records of a specific category that are older than expiryDate
|
||||
*/
|
||||
async function flagRecordsByCategory(
|
||||
schoolId: string,
|
||||
dataCategory: string,
|
||||
expiryDate: Date
|
||||
): Promise<number> {
|
||||
let flaggedCount = 0;
|
||||
|
||||
try {
|
||||
switch (dataCategory) {
|
||||
case "LEARNING_PROGRESS":
|
||||
flaggedCount += await flagRecordsByEntityType(
|
||||
schoolId,
|
||||
"LearningProgressEntry",
|
||||
expiryDate
|
||||
);
|
||||
break;
|
||||
|
||||
case "ASSESSMENT_RESULT":
|
||||
flaggedCount += await flagRecordsByEntityType(
|
||||
schoolId,
|
||||
"Assessment",
|
||||
expiryDate
|
||||
);
|
||||
break;
|
||||
|
||||
case "FINAL_REPORT":
|
||||
// Final reports should NOT be auto-flagged (legal requirement to keep permanently)
|
||||
console.log(
|
||||
`[v0] Skipping FINAL_REPORT for ${schoolId} (legally mandated retention)`
|
||||
);
|
||||
break;
|
||||
|
||||
case "DISCIPLINARY_RECORD":
|
||||
flaggedCount += await flagRecordsByEntityType(
|
||||
schoolId,
|
||||
"DisciplinaryCase",
|
||||
expiryDate
|
||||
);
|
||||
break;
|
||||
|
||||
case "EXAM_INCIDENT":
|
||||
flaggedCount += await flagRecordsByEntityType(
|
||||
schoolId,
|
||||
"ExamIncidentEvent",
|
||||
expiryDate
|
||||
);
|
||||
break;
|
||||
|
||||
case "MESSAGE":
|
||||
flaggedCount += await flagRecordsByEntityType(
|
||||
schoolId,
|
||||
"ParentMessage",
|
||||
expiryDate
|
||||
);
|
||||
break;
|
||||
|
||||
case "AI_TUTOR_LOG":
|
||||
// Note: In production, this would reference actual AI tutor logs
|
||||
console.log(`[v0] AI_TUTOR_LOG flagging not yet implemented`);
|
||||
break;
|
||||
|
||||
case "NOTEBOOK_PAGE":
|
||||
flaggedCount += await flagRecordsByEntityType(
|
||||
schoolId,
|
||||
"Notebook",
|
||||
expiryDate
|
||||
);
|
||||
break;
|
||||
|
||||
case "SIGNAGE_LOG":
|
||||
// Note: Reference actual signage log entity
|
||||
console.log(`[v0] SIGNAGE_LOG flagging not yet implemented`);
|
||||
break;
|
||||
|
||||
case "TEACHER_AUDIT_LOG":
|
||||
flaggedCount += await flagRecordsByEntityType(
|
||||
schoolId,
|
||||
"AuditLog",
|
||||
expiryDate
|
||||
);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[v0] Error flagging ${dataCategory} records for school ${schoolId}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
return flaggedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic function to flag expired records by entity type
|
||||
*/
|
||||
async function flagRecordsByEntityType(
|
||||
schoolId: string,
|
||||
entityType: string,
|
||||
expiryDate: Date
|
||||
): Promise<number> {
|
||||
// Check if records of this type already have deletion flags
|
||||
const alreadyFlagged = await prisma.deletionFlag.findMany({
|
||||
where: {
|
||||
schoolId,
|
||||
entityType,
|
||||
deletedAt: null, // Not yet hard-deleted
|
||||
approvedAt: null, // Not yet approved
|
||||
},
|
||||
});
|
||||
|
||||
const alreadyFlaggedIds = new Set(alreadyFlagged.map((f) => f.entityId));
|
||||
|
||||
// Create new flags for records not yet flagged
|
||||
let flaggedCount = 0;
|
||||
|
||||
// This is a placeholder; in production, you'd query each entity type
|
||||
// Example for LearningProgressEntry:
|
||||
if (entityType === "LearningProgressEntry") {
|
||||
const expiredRecords = await prisma.learningProgressEntry
|
||||
.findMany({
|
||||
where: {
|
||||
createdAt: { lt: expiryDate },
|
||||
// Filter by school if the entity has schoolId
|
||||
},
|
||||
select: { id: true },
|
||||
take: 1000, // Limit to prevent huge queries
|
||||
})
|
||||
.catch(() => []);
|
||||
|
||||
for (const record of expiredRecords) {
|
||||
if (!alreadyFlaggedIds.has(record.id)) {
|
||||
await prisma.deletionFlag.create({
|
||||
data: {
|
||||
schoolId,
|
||||
entityType,
|
||||
entityId: record.id,
|
||||
dataCategory: "LEARNING_PROGRESS",
|
||||
reason: "retention period expired",
|
||||
},
|
||||
});
|
||||
flaggedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return flaggedCount;
|
||||
}
|
||||
|
||||
// Run the job if this script is executed directly
|
||||
if (require.main === module) {
|
||||
runRetentionJob()
|
||||
.then((result) => {
|
||||
console.log(
|
||||
`[v0] Job result:`,
|
||||
JSON.stringify(result, null, 2)
|
||||
);
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("[v0] Job failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { runRetentionJob, flagExpiredRecordsForSchool };
|
||||
Reference in new issue
Block a user