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:
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user