Update 10 files
This commit is contained in:
1 parent
28a3c6121a
commit
28680e396e
10 files changed
+46
-108
No files matched your search
+2
-2
@@ -20,7 +20,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build --webpack",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"postinstall": "prisma generate",
|
"postinstall": "prisma generate",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"db:seed": "bun prisma/seed.ts",
|
"db:seed": "bun prisma/seed.ts",
|
||||||
"db:seed-demo": "bun prisma/seed-demo.ts",
|
"db:seed-demo": "bun prisma/seed-demo.ts",
|
||||||
"db:setup-demo": "bun run db:push && bun run db:seed-demo",
|
"db:setup-demo": "bun run db:push && bun run db:seed-demo",
|
||||||
"test": "playwright test --config playwright-dyad.config.ts"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
@@ -3,20 +3,11 @@
|
|||||||
* This document lists all data entities, their purposes, legal basis, and retention categories
|
* This document lists all data entities, their purposes, legal basis, and retention categories
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require("fs");
|
import fs from "node:fs";
|
||||||
const path = require("path");
|
import path from "node:path";
|
||||||
|
|
||||||
interface DataEntity {
|
|
||||||
name: string;
|
|
||||||
dataCategories: string[];
|
|
||||||
legalBasis: string;
|
|
||||||
purpose: string;
|
|
||||||
retentionCategory: string;
|
|
||||||
notes?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Core data model definition
|
// Core data model definition
|
||||||
const dataModel: DataEntity[] = [
|
const dataModel = [
|
||||||
{
|
{
|
||||||
name: "User",
|
name: "User",
|
||||||
dataCategories: ["firstName", "lastName", "email", "role", "createdAt"],
|
dataCategories: ["firstName", "lastName", "email", "role", "createdAt"],
|
||||||
@@ -112,12 +103,12 @@ const dataModel: DataEntity[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function generateDataModel(outputPath: string = "docs/legal"): void {
|
function generateDataModel(outputPath = "docs/legal") {
|
||||||
if (!fs.existsSync(outputPath)) {
|
if (!fs.existsSync(outputPath)) {
|
||||||
fs.mkdirSync(outputPath, { recursive: true });
|
fs.mkdirSync(outputPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines: string[] = [
|
const lines = [
|
||||||
"# Data Model Documentation",
|
"# Data Model Documentation",
|
||||||
"",
|
"",
|
||||||
"**Status**: Schema Reference for ExaMetra (Module L)",
|
"**Status**: Schema Reference for ExaMetra (Module L)",
|
||||||
@@ -166,7 +157,7 @@ export function generateDataModel(outputPath: string = "docs/legal"): void {
|
|||||||
"",
|
"",
|
||||||
`Generated: ${new Date().toISOString()}`,
|
`Generated: ${new Date().toISOString()}`,
|
||||||
"Source: ExaMetra Prisma schema (Module L)",
|
"Source: ExaMetra Prisma schema (Module L)",
|
||||||
];
|
);
|
||||||
|
|
||||||
const filePath = path.join(outputPath, "DATA_MODEL.md");
|
const filePath = path.join(outputPath, "DATA_MODEL.md");
|
||||||
fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
|
fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
|
||||||
|
|||||||
@@ -3,26 +3,15 @@
|
|||||||
* Plain-language German, based on school configuration
|
* Plain-language German, based on school configuration
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require("fs");
|
import fs from "node:fs";
|
||||||
const path = require("path");
|
import path from "node:path";
|
||||||
|
|
||||||
interface PrivacyNoticeParams {
|
function generatePrivacyNotice(params, outputPath = "docs/legal") {
|
||||||
schoolName: string;
|
|
||||||
dpoName: string;
|
|
||||||
dpoEmail: string;
|
|
||||||
schultragerName: string;
|
|
||||||
bundesland: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generatePrivacyNotice(
|
|
||||||
params: PrivacyNoticeParams,
|
|
||||||
outputPath: string = "docs/legal"
|
|
||||||
): void {
|
|
||||||
if (!fs.existsSync(outputPath)) {
|
if (!fs.existsSync(outputPath)) {
|
||||||
fs.mkdirSync(outputPath, { recursive: true });
|
fs.mkdirSync(outputPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines: string[] = [
|
const lines = [
|
||||||
"# Datenschutzerklärung",
|
"# Datenschutzerklärung",
|
||||||
"# (Privacy Notice for Parents & Students)",
|
"# (Privacy Notice for Parents & Students)",
|
||||||
"",
|
"",
|
||||||
@@ -139,7 +128,7 @@ export function generatePrivacyNotice(
|
|||||||
console.log(`[v0] Generated privacy notice at ${filePath}`);
|
console.log(`[v0] Generated privacy notice at ${filePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
if (process.argv[1] && path.resolve(process.argv[1]) === new URL(import.meta.url).pathname) {
|
||||||
generatePrivacyNotice({
|
generatePrivacyNotice({
|
||||||
schoolName: "Gymnasium Berlin",
|
schoolName: "Gymnasium Berlin",
|
||||||
dpoName: "Max Mustermann",
|
dpoName: "Max Mustermann",
|
||||||
|
|||||||
@@ -4,20 +4,10 @@
|
|||||||
* German legal document for school's DPO review
|
* German legal document for school's DPO review
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require("fs");
|
import fs from "node:fs";
|
||||||
const path = require("path");
|
import path from "node:path";
|
||||||
|
|
||||||
interface RopaEntry {
|
const ropas = [
|
||||||
name: string;
|
|
||||||
purposes: string[];
|
|
||||||
dataCategories: string[];
|
|
||||||
recipients: string[];
|
|
||||||
retentionPeriod: string;
|
|
||||||
legalBasis: string;
|
|
||||||
securityMeasures: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const ropas: RopaEntry[] = [
|
|
||||||
{
|
{
|
||||||
name: "Student Information Management",
|
name: "Student Information Management",
|
||||||
purposes: [
|
purposes: [
|
||||||
@@ -126,12 +116,12 @@ const ropas: RopaEntry[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function generateRopa(outputPath: string = "docs/legal"): void {
|
function generateRopa(outputPath = "docs/legal") {
|
||||||
if (!fs.existsSync(outputPath)) {
|
if (!fs.existsSync(outputPath)) {
|
||||||
fs.mkdirSync(outputPath, { recursive: true });
|
fs.mkdirSync(outputPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines: string[] = [
|
const lines = [
|
||||||
"# Verzeichnis von Verarbeitungstätigkeiten",
|
"# Verzeichnis von Verarbeitungstätigkeiten",
|
||||||
"# (Records of Processing Activities - Art. 30 GDPR)",
|
"# (Records of Processing Activities - Art. 30 GDPR)",
|
||||||
"",
|
"",
|
||||||
@@ -198,7 +188,7 @@ export function generateRopa(outputPath: string = "docs/legal"): void {
|
|||||||
"",
|
"",
|
||||||
`**Generiert**: ${new Date().toISOString()}`,
|
`**Generiert**: ${new Date().toISOString()}`,
|
||||||
"**Quelle**: ExaMetra Module L - Compliance Framework",
|
"**Quelle**: ExaMetra Module L - Compliance Framework",
|
||||||
];
|
);
|
||||||
|
|
||||||
const filePath = path.join(
|
const filePath = path.join(
|
||||||
outputPath,
|
outputPath,
|
||||||
|
|||||||
@@ -6,18 +6,11 @@
|
|||||||
* Usage: npx ts-node scripts/run-retention-job.ts
|
* Usage: npx ts-node scripts/run-retention-job.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { PrismaClient } = require("@prisma/client");
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
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
|
* Check records for each school and flag those exceeding retention period
|
||||||
*/
|
*/
|
||||||
@@ -32,7 +25,7 @@ async function runRetentionJob() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let totalFlagged = 0;
|
let totalFlagged = 0;
|
||||||
const report: { school: string; flagged: number }[] = [];
|
const report = [];
|
||||||
|
|
||||||
for (const school of schools) {
|
for (const school of schools) {
|
||||||
const schoolFlagged = await flagExpiredRecordsForSchool(school.id);
|
const schoolFlagged = await flagExpiredRecordsForSchool(school.id);
|
||||||
@@ -75,7 +68,7 @@ async function runRetentionJob() {
|
|||||||
/**
|
/**
|
||||||
* Flag expired records for a specific school
|
* Flag expired records for a specific school
|
||||||
*/
|
*/
|
||||||
async function flagExpiredRecordsForSchool(schoolId: string): Promise<number> {
|
async function flagExpiredRecordsForSchool(schoolId) {
|
||||||
// Get all retention policies for this school
|
// Get all retention policies for this school
|
||||||
const policies = await prisma.dataRetentionPolicy.findMany({
|
const policies = await prisma.dataRetentionPolicy.findMany({
|
||||||
where: { schoolId },
|
where: { schoolId },
|
||||||
@@ -106,11 +99,7 @@ async function flagExpiredRecordsForSchool(schoolId: string): Promise<number> {
|
|||||||
/**
|
/**
|
||||||
* Flag records of a specific category that are older than expiryDate
|
* Flag records of a specific category that are older than expiryDate
|
||||||
*/
|
*/
|
||||||
async function flagRecordsByCategory(
|
async function flagRecordsByCategory(schoolId, dataCategory, expiryDate) {
|
||||||
schoolId: string,
|
|
||||||
dataCategory: string,
|
|
||||||
expiryDate: Date
|
|
||||||
): Promise<number> {
|
|
||||||
let flaggedCount = 0;
|
let flaggedCount = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -201,11 +190,7 @@ async function flagRecordsByCategory(
|
|||||||
/**
|
/**
|
||||||
* Generic function to flag expired records by entity type
|
* Generic function to flag expired records by entity type
|
||||||
*/
|
*/
|
||||||
async function flagRecordsByEntityType(
|
async function flagRecordsByEntityType(schoolId, entityType, expiryDate) {
|
||||||
schoolId: string,
|
|
||||||
entityType: string,
|
|
||||||
expiryDate: Date
|
|
||||||
): Promise<number> {
|
|
||||||
// Check if records of this type already have deletion flags
|
// Check if records of this type already have deletion flags
|
||||||
const alreadyFlagged = await prisma.deletionFlag.findMany({
|
const alreadyFlagged = await prisma.deletionFlag.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -255,7 +240,7 @@ async function flagRecordsByEntityType(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run the job if this script is executed directly
|
// Run the job if this script is executed directly
|
||||||
if (require.main === module) {
|
if (process.argv[1] && path.resolve(process.argv[1]) === new URL(import.meta.url).pathname) {
|
||||||
runRetentionJob()
|
runRetentionJob()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
console.log(
|
console.log(
|
||||||
@@ -270,4 +255,4 @@ if (require.main === module) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { runRetentionJob, flagExpiredRecordsForSchool };
|
export { runRetentionJob, flagExpiredRecordsForSchool };
|
||||||
@@ -8,16 +8,16 @@ import { checkModuleAccess } from "@/lib/compliance";
|
|||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const schoolId = req.nextUrl.searchParams.get("schoolId");
|
const schoolId = req.nextUrl.searchParams.get("schoolId");
|
||||||
const module = req.nextUrl.searchParams.get("module");
|
const moduleName = req.nextUrl.searchParams.get("module");
|
||||||
|
|
||||||
if (!schoolId || !module) {
|
if (!schoolId || !moduleName) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Missing schoolId or module parameter" },
|
{ error: "Missing schoolId or module parameter" },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await checkModuleAccess(schoolId, module);
|
const result = await checkModuleAccess(schoolId, moduleName);
|
||||||
|
|
||||||
return NextResponse.json(result);
|
return NextResponse.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
+2
-2
@@ -6,8 +6,8 @@
|
|||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: Arial, Helvetica, sans-serif;
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
|||||||
+1
-14
@@ -1,19 +1,8 @@
|
|||||||
import type { Metadata, Viewport } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
|
||||||
const geistSans = Geist({
|
|
||||||
variable: "--font-geist-sans",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "SchulOS — Free Competency & Grading Platform",
|
title: "SchulOS — Free Competency & Grading Platform",
|
||||||
description: "Open-Source School Software for Competency-Based Assessment and Grading. Built with Next.js, TypeScript, and shadcn/ui.",
|
description: "Open-Source School Software for Competency-Based Assessment and Grading. Built with Next.js, TypeScript, and shadcn/ui.",
|
||||||
@@ -48,9 +37,7 @@ export default function RootLayout({
|
|||||||
<head>
|
<head>
|
||||||
<link rel="apple-touch-icon" href="/icon-192.png" />
|
<link rel="apple-touch-icon" href="/icon-192.png" />
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body className="antialiased bg-background text-foreground">
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
|
|
||||||
>
|
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="light"
|
defaultTheme="light"
|
||||||
|
|||||||
@@ -107,7 +107,9 @@ export const serverError = (error: Error | string, path?: string) => {
|
|||||||
/**
|
/**
|
||||||
* Wrap handler with automatic error handling
|
* Wrap handler with automatic error handling
|
||||||
*/
|
*/
|
||||||
export const withErrorHandling = (handler: Function) => {
|
export const withErrorHandling = (
|
||||||
|
handler: (...args: any[]) => any
|
||||||
|
) => {
|
||||||
return async (...args: any[]) => {
|
return async (...args: any[]) => {
|
||||||
try {
|
try {
|
||||||
return await handler(...args);
|
return await handler(...args);
|
||||||
|
|||||||
+10
-16
@@ -145,23 +145,17 @@ export function useApiQueries<T = any>(
|
|||||||
queries: Array<{ url: string | null; key?: string }>,
|
queries: Array<{ url: string | null; key?: string }>,
|
||||||
options?: SWRConfiguration
|
options?: SWRConfiguration
|
||||||
) {
|
) {
|
||||||
const results = queries.map((query) => {
|
const activeQueries = queries.filter((query) => query.url);
|
||||||
const { data, error, isLoading, mutate } = useApiGet(query.url, options);
|
const cacheKey = activeQueries.map((query) => query.url).join("|") || null;
|
||||||
return {
|
const { data: values, error, isLoading } = useSWR<T[]>(
|
||||||
key: query.key || query.url,
|
cacheKey,
|
||||||
data,
|
() => Promise.all(activeQueries.map((query) => apiFetcher(query.url!))),
|
||||||
error,
|
options
|
||||||
isLoading,
|
);
|
||||||
mutate,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const isLoading = results.some((r) => r.isLoading);
|
const data = Object.fromEntries(
|
||||||
const error = results.find((r) => r.error);
|
activeQueries.map((query, index) => [query.key || query.url, values?.[index]])
|
||||||
const data = Object.fromEntries(results.map((r) => [r.key, r.data])) as Record<
|
) as Record<string, T | undefined>;
|
||||||
string,
|
|
||||||
T | undefined
|
|
||||||
>;
|
|
||||||
|
|
||||||
return { data, error, isLoading };
|
return { data, error, isLoading };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user