feat(core): Verschluesselung, Validierung und gemeinsame Helfer
- crypto.ts: AES-256-GCM fuer die Git-Access-Tokens, Schluessel per scrypt aus APP_SECRET abgeleitet. Chiffrat ist versioniert (v1.<iv>.<tag>.<data>), damit ein spaeterer Formatwechsel moeglich bleibt. Fehlt APP_SECRET, warnt die App und faellt auf einen Entwicklungsschluessel zurueck. - validation.ts: Zod-Schemas fuer alle Eingaben an der API-Grenze, inklusive Umwandlung leerer Strings zu null und Feldfehlern fuer die Oberflaeche. - status.ts: Metadaten zu Status, Prioritaeten und Akzentfarben an einer Stelle, damit Board, Karte und Detailseite nicht auseinanderlaufen. - utils.ts: Slug-Erzeugung, deutsche Datumsformate mit fester Zeitzone (verhindert Abweichungen zwischen Server-Render und Hydration) und relative Zeitangaben. - projects.ts: wiederverwendete Prisma-Selects. Die Verbindungsauswahl laesst tokenCipher bewusst weg, damit kein Token in eine Client-Komponente gelangt. - db.ts: Prisma-Client mit Cache ueber globalThis gegen Verbindungslecks beim Hot Reload. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const STATUS = ["IDEA", "OPEN", "PLANNING", "IN_DEVELOPMENT", "DONE", "ARCHIVED"] as const;
|
||||
const PROVIDERS = ["GITHUB", "GITLAB", "GITEA"] as const;
|
||||
const ACCENTS = ["violet", "cyan", "emerald", "amber", "rose", "blue"] as const;
|
||||
|
||||
const emptyToNull = (value: unknown) =>
|
||||
typeof value === "string" && value.trim() === "" ? null : value;
|
||||
|
||||
export const projectCreateSchema = z.object({
|
||||
name: z.string().trim().min(1, "Name darf nicht leer sein").max(120),
|
||||
summary: z.preprocess(emptyToNull, z.string().trim().max(280).nullable().optional()),
|
||||
description: z.preprocess(emptyToNull, z.string().trim().max(20000).nullable().optional()),
|
||||
status: z.enum(STATUS).default("IDEA"),
|
||||
priority: z.coerce.number().int().min(1).max(4).default(2),
|
||||
progress: z.coerce.number().int().min(0).max(100).default(0),
|
||||
accent: z.enum(ACCENTS).default("violet"),
|
||||
tags: z.array(z.string().trim().min(1).max(30)).max(12).default([]),
|
||||
favorite: z.boolean().default(false),
|
||||
repoUrl: z.preprocess(emptyToNull, z.string().trim().max(500).nullable().optional()),
|
||||
repoOwner: z.preprocess(emptyToNull, z.string().trim().max(200).nullable().optional()),
|
||||
repoName: z.preprocess(emptyToNull, z.string().trim().max(200).nullable().optional()),
|
||||
connectionId: z.preprocess(emptyToNull, z.string().trim().nullable().optional()),
|
||||
});
|
||||
|
||||
export const projectUpdateSchema = projectCreateSchema.partial();
|
||||
|
||||
export const noteCreateSchema = z.object({
|
||||
title: z.preprocess(emptyToNull, z.string().trim().max(160).nullable().optional()),
|
||||
content: z.string().trim().min(1, "Notiz darf nicht leer sein").max(20000),
|
||||
pinned: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const noteUpdateSchema = noteCreateSchema.partial();
|
||||
|
||||
export const connectionCreateSchema = z.object({
|
||||
name: z.string().trim().min(1, "Name darf nicht leer sein").max(80),
|
||||
provider: z.enum(PROVIDERS),
|
||||
baseUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Basis-URL fehlt")
|
||||
.max(300)
|
||||
.refine((value) => /^https?:\/\//i.test(value), "URL muss mit http:// oder https:// beginnen"),
|
||||
username: z.preprocess(emptyToNull, z.string().trim().max(120).nullable().optional()),
|
||||
token: z.preprocess(emptyToNull, z.string().trim().max(500).nullable().optional()),
|
||||
});
|
||||
|
||||
export const connectionUpdateSchema = connectionCreateSchema.partial();
|
||||
|
||||
export type ProjectCreateInput = z.infer<typeof projectCreateSchema>;
|
||||
export type ProjectUpdateInput = z.infer<typeof projectUpdateSchema>;
|
||||
export type NoteCreateInput = z.infer<typeof noteCreateSchema>;
|
||||
export type ConnectionCreateInput = z.infer<typeof connectionCreateSchema>;
|
||||
|
||||
/** Formt Zod-Fehler in ein flaches Objekt fuer die API-Antwort. */
|
||||
export function formatZodError(error: z.ZodError): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const issue of error.issues) {
|
||||
const key = issue.path.join(".") || "_";
|
||||
if (!result[key]) result[key] = issue.message;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user