Initial commit: Personal Dashboard

Next.js 16 dashboard with configurable widgets (favorites, notes, calendar,
clock, calculator, search, domain-check), multi-tab support, user auth,
dark mode, and Docker deployment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-18 10:02:05 +02:00
commit a4051ae132
74 changed files with 18317 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
import { mkdir, stat, writeFile } from "fs/promises";
import path from "path";
import { randomUUID } from "crypto";
import { NextResponse } from "next/server";
import { requireCurrentUser, UnauthorizedError } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
const uploadRoot = "/data/uploads";
const maxFileSizeBytes = 2 * 1024 * 1024;
const allowedMimeTypes: Record<string, string> = {
"image/png": "png",
"image/svg+xml": "svg",
"image/x-icon": "ico",
"image/vnd.microsoft.icon": "ico"
};
function getExtension(file: File): string | null {
return allowedMimeTypes[file.type] ?? null;
}
async function ensureSettings(userId: string, email: string) {
const existingSettings = await prisma.settings.findUnique({
where: { userId }
});
if (existingSettings) {
return existingSettings;
}
return prisma.settings.create({
data: {
userId,
darkMode: false,
calendarIcsUrl: null,
calendarMaxEvents: 8,
calendarLookaheadDays: 60,
dashboardTitle: "Personal Dashboard",
dashboardSubtitle: email,
logoUrl: "/logo.svg",
faviconUrl: "/favicon.ico",
backgroundImageUrl: "/background-fancy.svg",
backgroundImageOpacity: 32,
primaryColor: "#2563eb",
secondaryColor: "#dbeafe",
customCss: ""
}
});
}
export async function POST(request: Request) {
try {
const user = await requireCurrentUser();
await ensureSettings(user.id, user.email);
const formData = await request.formData();
const file = formData.get("file");
if (!(file instanceof File)) {
return NextResponse.json({ error: "Keine Datei hochgeladen." }, { status: 400 });
}
if (file.size <= 0 || file.size > maxFileSizeBytes) {
return NextResponse.json({ error: "Datei ist leer oder größer als 2 MB." }, { status: 400 });
}
const extension = getExtension(file);
if (!extension) {
return NextResponse.json({ error: "Nur ICO, PNG oder SVG sind erlaubt." }, { status: 400 });
}
const userUploadDirectory = path.join(uploadRoot, "users", user.id);
await mkdir(userUploadDirectory, { recursive: true });
const fileName = `favicon-${Date.now()}-${randomUUID()}.${extension}`;
const filePath = path.join(userUploadDirectory, fileName);
const bytes = await file.arrayBuffer();
await writeFile(filePath, Buffer.from(bytes));
const writtenFile = await stat(filePath);
if (!writtenFile.isFile() || writtenFile.size <= 0) {
return NextResponse.json({ error: "Favicon wurde nicht korrekt gespeichert." }, { status: 500 });
}
const faviconUrl = `/api/uploads/users/${user.id}/${fileName}`;
const settings = await prisma.settings.update({
where: { userId: user.id },
data: { faviconUrl }
});
return NextResponse.json({ settings });
} catch (error) {
if (error instanceof UnauthorizedError) {
return NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 });
}
return NextResponse.json({ error: "Favicon konnte nicht hochgeladen werden." }, { status: 500 });
}
}
export async function DELETE() {
try {
const user = await requireCurrentUser();
await ensureSettings(user.id, user.email);
const settings = await prisma.settings.update({
where: { userId: user.id },
data: { faviconUrl: "/favicon.ico" }
});
return NextResponse.json({ settings });
} catch (error) {
if (error instanceof UnauthorizedError) {
return NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 });
}
return NextResponse.json({ error: "Favicon konnte nicht zurückgesetzt werden." }, { status: 500 });
}
}