mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
i18n: migrate from German-first to English-first default locale
Switch defaultLocale from "de" to "en" and make English the code-level language throughout. All ~145 hardcoded German strings translated: - API error messages (52 routes) now return English - Zod validation messages in English - Classification/analytics functions return English categories - Notification type labels in English - Component strings use i18n t() calls (heatmap, mood chart, medication form, intake history, password strength, insights card) - ~50 new i18n keys in both de.json and en.json Route migration: - /stimmung → /mood, /zielwerte → /targets (new pages) - 301 redirects in proxy.ts for legacy URLs - Sidebar + bottom nav updated Bump version to 1.0.1.
This commit is contained in:
1 parent
38309932f3
commit
0c0c345e82
74 files changed
+871
-402
No files matched your search
@@ -30,11 +30,11 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
|
||||
const body = await request.json();
|
||||
confirm = typeof body?.confirm === "string" ? body.confirm : "";
|
||||
} catch {
|
||||
return apiError("Ungültige Anfrage", 422);
|
||||
return apiError("Invalid request", 422);
|
||||
}
|
||||
|
||||
if (confirm !== "DELETE ALL") {
|
||||
return apiError("Bestätigung fehlt", 422);
|
||||
return apiError("Confirmation missing", 422);
|
||||
}
|
||||
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
|
||||
@@ -12,10 +12,10 @@ export const POST = apiHandler(async () => {
|
||||
|
||||
const settings = await getGlitchtipSettings();
|
||||
if (!settings.glitchtipEnabled) {
|
||||
return apiError("Glitchtip ist deaktiviert", 422);
|
||||
return apiError("Glitchtip is disabled", 422);
|
||||
}
|
||||
if (!settings.glitchtipDsn) {
|
||||
return apiError("Glitchtip DSN fehlt", 422);
|
||||
return apiError("Glitchtip DSN is missing", 422);
|
||||
}
|
||||
|
||||
const delivery = await sendGlitchtipEvent({
|
||||
@@ -32,13 +32,13 @@ export const POST = apiHandler(async () => {
|
||||
if (!delivery.ok) {
|
||||
getEvent()?.addWarning("Glitchtip test event rejected: " + delivery.method + " " + delivery.status + " " + delivery.details);
|
||||
return apiError(
|
||||
`Glitchtip-Testevent wurde abgelehnt (HTTP ${delivery.status ?? 502})`,
|
||||
`Glitchtip test event rejected (HTTP ${delivery.status ?? 502})`,
|
||||
502,
|
||||
);
|
||||
}
|
||||
|
||||
return apiSuccess({
|
||||
sent: true,
|
||||
message: "Glitchtip-Testevent wurde gesendet",
|
||||
message: "Glitchtip test event sent",
|
||||
});
|
||||
});
|
||||
@@ -51,18 +51,18 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
const settings = await getPublicMonitoringSettings();
|
||||
if (!settings.umamiEnabled) {
|
||||
return apiError("Umami ist deaktiviert", 422);
|
||||
return apiError("Umami is disabled", 422);
|
||||
}
|
||||
if (!settings.umamiScriptUrl || !settings.umamiWebsiteId) {
|
||||
return apiError(
|
||||
"Umami Script-URL und Website-ID müssen konfiguriert sein",
|
||||
"Umami script URL and website ID must be configured",
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
const targetUrls = resolveUmamiSendUrls(settings.umamiScriptUrl);
|
||||
if (targetUrls.length === 0) {
|
||||
return apiError("Umami Script-URL ist ungültig", 422);
|
||||
return apiError("Umami script URL is invalid", 422);
|
||||
}
|
||||
|
||||
const appUrl = resolveAppUrl(request);
|
||||
@@ -100,7 +100,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
if (upstream.ok) {
|
||||
return apiSuccess({
|
||||
sent: true,
|
||||
message: "Umami-Testevent wurde gesendet",
|
||||
message: "Umami test event sent",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
getEvent()?.addWarning("Umami test event rejected: " + lastStatus);
|
||||
return apiError(
|
||||
`Umami-Testevent wurde abgelehnt (HTTP ${lastStatus})`,
|
||||
`Umami test event rejected (HTTP ${lastStatus})`,
|
||||
502,
|
||||
);
|
||||
});
|
||||
@@ -79,7 +79,7 @@ export const POST = apiHandler(async () => {
|
||||
if (channels.length === 0) {
|
||||
return apiSuccess({
|
||||
sent: false,
|
||||
message: "Keine aktivierten Benachrichtigungskanäle gefunden",
|
||||
message: "No enabled notification channels found",
|
||||
results: [],
|
||||
});
|
||||
}
|
||||
@@ -89,7 +89,7 @@ export const POST = apiHandler(async () => {
|
||||
userId: user.id,
|
||||
title: "Test-Notification",
|
||||
message:
|
||||
"<b>HealthLog Test:</b> Wenn du diese Nachricht siehst, funktionieren deine Benachrichtigungen!",
|
||||
"<b>HealthLog Test:</b> If you see this message, your notifications are working!",
|
||||
};
|
||||
|
||||
const results: Array<{
|
||||
@@ -104,7 +104,7 @@ export const POST = apiHandler(async () => {
|
||||
results.push({
|
||||
channel: channel.type,
|
||||
success: false,
|
||||
error: "SYSTEM_ALERT deaktiviert in Einstellungen",
|
||||
error: "SYSTEM_ALERT disabled in settings",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -124,7 +124,7 @@ export const POST = apiHandler(async () => {
|
||||
results.push({
|
||||
channel: "TELEGRAM",
|
||||
success: false,
|
||||
error: `Telegram API hat Fehler gemeldet (chatId: ${config.chatId})`,
|
||||
error: `Telegram API reported error (chatId: ${config.chatId})`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -139,7 +139,7 @@ export const POST = apiHandler(async () => {
|
||||
results.push({
|
||||
channel: "NTFY",
|
||||
success: false,
|
||||
error: `Senden fehlgeschlagen (topic: ${config.topic})`,
|
||||
error: `Send failed (topic: ${config.topic})`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -153,7 +153,7 @@ export const POST = apiHandler(async () => {
|
||||
results.push({
|
||||
channel: type,
|
||||
success: false,
|
||||
error: "Unbekannter Kanaltyp",
|
||||
error: "Unknown channel type",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -163,7 +163,7 @@ export const POST = apiHandler(async () => {
|
||||
results.push({
|
||||
channel: channel.type,
|
||||
success: false,
|
||||
error: err instanceof Error ? err.message : "Unbekannter Fehler",
|
||||
error: err instanceof Error ? err.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -173,11 +173,11 @@ export const POST = apiHandler(async () => {
|
||||
|
||||
let message: string;
|
||||
if (failCount === 0) {
|
||||
message = `Alle ${successCount} Kanäle erfolgreich`;
|
||||
message = `All ${successCount} channels successful`;
|
||||
} else if (successCount === 0) {
|
||||
message = `Alle ${failCount} Kanäle fehlgeschlagen`;
|
||||
message = `All ${failCount} channels failed`;
|
||||
} else {
|
||||
message = `${successCount} erfolgreich, ${failCount} fehlgeschlagen`;
|
||||
message = `${successCount} successful, ${failCount} failed`;
|
||||
}
|
||||
|
||||
return apiSuccess({ sent: successCount > 0, message, results });
|
||||
|
||||
@@ -180,7 +180,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return apiError("Keine gültigen Felder", 422);
|
||||
return apiError("No valid fields", 422);
|
||||
}
|
||||
|
||||
const settings = await prisma.appSettings.upsert({
|
||||
|
||||
@@ -32,16 +32,16 @@ export const POST = apiHandler(async (
|
||||
const { password } = body as { password?: string };
|
||||
|
||||
if (!password || typeof password !== "string") {
|
||||
return apiError("Passwort erforderlich", 422);
|
||||
return apiError("Password required", 422);
|
||||
}
|
||||
|
||||
const target = await prisma.user.findUnique({ where: { id } });
|
||||
if (!target) return apiError("Benutzer nicht gefunden", 404);
|
||||
if (!target) return apiError("User not found", 404);
|
||||
|
||||
const strength = checkPasswordStrength(password, [target.username]);
|
||||
if (!strength.isAcceptable) {
|
||||
return apiError(
|
||||
strength.feedback[0] || "Passwort zu schwach (Score < 3)",
|
||||
strength.feedback[0] || "Password too weak (score < 3)",
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@ export const PUT = apiHandler(async (
|
||||
}
|
||||
|
||||
const target = await prisma.user.findUnique({ where: { id } });
|
||||
if (!target) return apiError("Benutzer nicht gefunden", 404);
|
||||
if (!target) return apiError("User not found", 404);
|
||||
|
||||
// Prevent removing the last admin
|
||||
if (parsed.data.role === "USER" && target.role === "ADMIN") {
|
||||
const adminCount = await prisma.user.count({ where: { role: "ADMIN" } });
|
||||
if (adminCount <= 1) {
|
||||
return apiError("Der letzte Admin kann nicht degradiert werden", 400);
|
||||
return apiError("The last admin cannot be demoted", 400);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
return NextResponse.json(
|
||||
{
|
||||
data: null,
|
||||
error: "Zu viele Anmeldeversuche. Bitte später erneut versuchen.",
|
||||
error: "Too many login attempts. Please try again later.",
|
||||
},
|
||||
{ status: 429, headers: rateLimitHeaders(rl) },
|
||||
);
|
||||
@@ -31,7 +31,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const parsed = loginPasswordSchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
return apiError("Ungültige Anmeldedaten", 422);
|
||||
return apiError("Invalid credentials", 422);
|
||||
}
|
||||
|
||||
const { email, password } = parsed.data;
|
||||
@@ -51,7 +51,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
ipAddress: ip,
|
||||
details: { identifier, reason: "user_not_found_or_no_password" },
|
||||
});
|
||||
return apiError("Ungültige Anmeldedaten", 401);
|
||||
return apiError("Invalid credentials", 401);
|
||||
}
|
||||
|
||||
const valid = await verifyPassword(user.passwordHash, password);
|
||||
@@ -61,7 +61,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
ipAddress: ip,
|
||||
details: { reason: "invalid_password" },
|
||||
});
|
||||
return apiError("Ungültige Anmeldedaten", 401);
|
||||
return apiError("Invalid credentials", 401);
|
||||
}
|
||||
|
||||
const ua = request.headers.get("user-agent");
|
||||
|
||||
@@ -16,7 +16,7 @@ export const POST = apiHandler(async (request: Request) => {
|
||||
return NextResponse.json(
|
||||
{
|
||||
data: null,
|
||||
error: "Zu viele Passkey-Anfragen. Bitte später erneut versuchen.",
|
||||
error: "Too many passkey requests. Please try again later.",
|
||||
},
|
||||
{ status: 429, headers: rateLimitHeaders(rl) },
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const ip = getClientIp(request);
|
||||
const rl = await checkRateLimit(`auth:passkey-verify:${ip}`, 10, 15 * 60 * 1000);
|
||||
if (!rl.allowed) {
|
||||
return apiError("Zu viele Versuche. Bitte 15 Minuten warten.", 429);
|
||||
return apiError("Too many attempts. Please wait 15 minutes.", 429);
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson<Record<string, unknown>>(request);
|
||||
@@ -25,7 +25,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const credential = body.credential;
|
||||
|
||||
if (!challengeId || !credential) {
|
||||
return apiError("challengeId und credential erforderlich", 422);
|
||||
return apiError("challengeId and credential required", 422);
|
||||
}
|
||||
|
||||
const { verification, passkey } = await verifyAuthentication(
|
||||
@@ -38,7 +38,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
ipAddress: ip,
|
||||
details: { reason: "passkey_verification_failed" },
|
||||
});
|
||||
return apiError("Passkey-Verifizierung fehlgeschlagen", 401);
|
||||
return apiError("Passkey verification failed", 401);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
@@ -46,7 +46,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return apiError("Benutzer nicht gefunden", 404);
|
||||
return apiError("User not found", 404);
|
||||
}
|
||||
|
||||
const ua = request.headers.get("user-agent");
|
||||
|
||||
@@ -16,13 +16,13 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const credential = body.credential as Record<string, unknown> | undefined;
|
||||
|
||||
if (!challengeId || !credential) {
|
||||
return apiError("challengeId und credential erforderlich", 422);
|
||||
return apiError("challengeId and credential required", 422);
|
||||
}
|
||||
|
||||
const verification = await verifyRegistration(challengeId, credential);
|
||||
|
||||
if (!verification.verified || !verification.registrationInfo) {
|
||||
return apiError("Passkey-Verifizierung fehlgeschlagen", 400);
|
||||
return apiError("Passkey verification failed", 400);
|
||||
}
|
||||
|
||||
const { registrationInfo } = verification;
|
||||
|
||||
@@ -18,7 +18,7 @@ export const DELETE = apiHandler(async (
|
||||
});
|
||||
|
||||
if (!passkey || passkey.userId !== user.id) {
|
||||
return apiError("Passkey nicht gefunden", 404);
|
||||
return apiError("Passkey not found", 404);
|
||||
}
|
||||
|
||||
// Check: at least 1 auth method must remain
|
||||
@@ -29,7 +29,7 @@ export const DELETE = apiHandler(async (
|
||||
|
||||
if (passkeyCount <= 1 && !hasPassword) {
|
||||
return apiError(
|
||||
"Kann nicht gelöscht werden — es muss mindestens eine Anmeldemethode bestehen bleiben",
|
||||
"Cannot delete — at least one authentication method must remain",
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
);
|
||||
if (!rl.allowed) {
|
||||
return apiError(
|
||||
"Zu viele Versuche. Bitte 15 Minuten warten.",
|
||||
"Too many attempts. Please wait 15 minutes.",
|
||||
429,
|
||||
);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
const { currentPassword, newPassword } = parsed.data;
|
||||
if (!user.passwordHash) {
|
||||
return apiError("Für dieses Konto ist kein Passwort gesetzt", 400);
|
||||
return apiError("No password set for this account", 400);
|
||||
}
|
||||
|
||||
const currentValid = await verifyPassword(
|
||||
@@ -46,12 +46,12 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
currentPassword,
|
||||
);
|
||||
if (!currentValid) {
|
||||
return apiError("Aktuelles Passwort ist falsch", 401);
|
||||
return apiError("Current password is incorrect", 401);
|
||||
}
|
||||
|
||||
if (currentPassword === newPassword) {
|
||||
return apiError(
|
||||
"Neues Passwort muss sich vom aktuellen unterscheiden",
|
||||
"New password must differ from current password",
|
||||
422,
|
||||
);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
]);
|
||||
if (!strength.isAcceptable) {
|
||||
return apiError(
|
||||
strength.feedback[0] || "Passwort zu schwach (Score < 3)",
|
||||
strength.feedback[0] || "Password too weak (score < 3)",
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
|
||||
if (existingUser && existingUser.id !== user.id) {
|
||||
return apiError("E-Mail-Adresse wird bereits verwendet", 409);
|
||||
return apiError("Email already in use", 409);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
{
|
||||
data: null,
|
||||
error:
|
||||
"Zu viele Registrierungsversuche. Bitte später erneut versuchen.",
|
||||
"Too many registration attempts. Please try again later.",
|
||||
},
|
||||
{ status: 429, headers: rateLimitHeaders(rl) },
|
||||
);
|
||||
@@ -42,7 +42,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
// Table may not exist yet; allow registration
|
||||
}
|
||||
if (!registrationEnabled) {
|
||||
return apiError("Registrierung ist deaktiviert", 403);
|
||||
return apiError("Registration is disabled", 403);
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
@@ -62,14 +62,14 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
prisma.user.findUnique({ where: { username } }),
|
||||
]);
|
||||
if (existingEmail || existingUsername) {
|
||||
return apiError("Benutzername oder E-Mail-Adresse bereits vergeben", 409);
|
||||
return apiError("Username or email already taken", 409);
|
||||
}
|
||||
|
||||
// Validate password strength
|
||||
const strength = checkPasswordStrength(password, [username, email]);
|
||||
if (!strength.isAcceptable) {
|
||||
return apiError(
|
||||
strength.feedback[0] || "Passwort zu schwach (Score < 3)",
|
||||
strength.feedback[0] || "Password too weak (score < 3)",
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
const bugReportSchema = z.object({
|
||||
description: z.string().min(10, "Beschreibung zu kurz").max(5000),
|
||||
description: z.string().min(10, "Description too short").max(5000),
|
||||
screenshot: z
|
||||
.string()
|
||||
.max(7_000_000, "Screenshot zu groß (max. 5 MB)")
|
||||
.max(7_000_000, "Screenshot too large (max 5 MB)")
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
const rl = await checkRateLimit(`bugreport:${user.id}`, 3, 60 * 60 * 1000);
|
||||
if (!rl.allowed) {
|
||||
return apiError("Maximal 3 Bugreports pro Stunde", 429);
|
||||
return apiError("Maximum 3 bug reports per hour", 429);
|
||||
}
|
||||
|
||||
annotate({ action: { name: "bugreport.submit" } });
|
||||
@@ -48,7 +48,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
if (!ghToken || !ghRepo) {
|
||||
return apiError(
|
||||
"Bugreport ist nicht konfiguriert (GitHub-Issue-Token/Repository in Admin-Einstellungen fehlt)",
|
||||
"Bug report not configured (GitHub issue token/repository missing in admin settings)",
|
||||
500,
|
||||
);
|
||||
}
|
||||
@@ -75,15 +75,15 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
.slice(0, 5000);
|
||||
|
||||
// Build issue body
|
||||
let issueBody = `**Gemeldet von:** ${safeUsername}\n`;
|
||||
issueBody += `**Datum:** ${dateStr}\n\n`;
|
||||
issueBody += `## Beschreibung\n\n${safeDescription}\n`;
|
||||
issueBody += `\n---\n*Erstellt über die HealthLog Bugreport-Funktion*`;
|
||||
let issueBody = `**Reported by:** ${safeUsername}\n`;
|
||||
issueBody += `**Date:** ${dateStr}\n\n`;
|
||||
issueBody += `## Description\n\n${safeDescription}\n`;
|
||||
issueBody += `\n---\n*Created via HealthLog bug report*`;
|
||||
|
||||
// If screenshot provided, upload to GitHub as a gist or include as base64
|
||||
// For simplicity, we'll note there's a screenshot but GitHub issues don't support inline base64
|
||||
if (screenshot) {
|
||||
issueBody += `\n\n**Screenshot:** Ein Screenshot wurde angehängt (siehe Kommentar).`;
|
||||
issueBody += `\n\n**Screenshot:** A screenshot was attached (see comment).`;
|
||||
}
|
||||
|
||||
// Create the issue
|
||||
@@ -107,7 +107,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
if (!issueRes.ok) {
|
||||
const errBody = await issueRes.text();
|
||||
getEvent()?.addWarning("GitHub issue creation failed: " + errBody);
|
||||
return apiError("Issue konnte nicht erstellt werden", 500);
|
||||
return apiError("Failed to create issue", 500);
|
||||
}
|
||||
|
||||
const issue = (await issueRes.json()) as {
|
||||
@@ -123,7 +123,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
// Upload screenshot as issue comment with embedded image
|
||||
// GitHub doesn't support base64 in comments, so we note the limitation
|
||||
const commentBody = `### Screenshot\n\n}...)\n\n*Hinweis: Der vollständige Screenshot wurde aus Größengründen gekürzt. Bitte den Nutzer direkt kontaktieren.*`;
|
||||
const commentBody = `### Screenshot\n\n}...)\n\n*Note: The full screenshot was truncated due to size limits. Please contact the user directly.*`;
|
||||
|
||||
await fetch(
|
||||
`https://api.github.com/repos/${ghRepo}/issues/${issue.number}/comments`,
|
||||
|
||||
@@ -16,7 +16,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
const rl = await checkRateLimit(`doctor-report:${user.id}`, 10, 60 * 60 * 1000);
|
||||
if (!rl.allowed) {
|
||||
return apiError("Maximal 10 Reports pro Stunde", 429);
|
||||
return apiError("Maximum 10 reports per hour", 429);
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
|
||||
@@ -26,7 +26,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
const rl = await checkRateLimit(`export:${user.id}`, 10, 60 * 60 * 1000);
|
||||
if (!rl.allowed) {
|
||||
return apiError("Maximal 10 Exports pro Stunde", 429);
|
||||
return apiError("Maximum 10 exports per hour", 429);
|
||||
}
|
||||
|
||||
annotate({ action: { name: "export.download" } });
|
||||
@@ -36,10 +36,10 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
const type = searchParams.get("type") ?? "all";
|
||||
|
||||
if (!["csv", "json"].includes(format)) {
|
||||
return apiError("Format muss csv oder json sein", 422);
|
||||
return apiError("Format must be csv or json", 422);
|
||||
}
|
||||
if (!["measurements", "medications", "intake", "mood", "all"].includes(type)) {
|
||||
return apiError("Typ muss measurements, medications, intake, mood oder all sein", 422);
|
||||
return apiError("Type must be measurements, medications, intake, mood, or all", 422);
|
||||
}
|
||||
|
||||
const userId = user.id;
|
||||
|
||||
@@ -19,7 +19,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
annotate({ action: { name: "ingest.medication" } });
|
||||
|
||||
if (!(await isApiGloballyEnabled())) {
|
||||
return apiError("API ist global deaktiviert", 403);
|
||||
return apiError("API is globally disabled", 403);
|
||||
}
|
||||
|
||||
const ip = getClientIp(request);
|
||||
@@ -109,12 +109,12 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
|
||||
if (!medication) {
|
||||
return apiError("Medikament nicht gefunden", 404);
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
const medicationScope = `medication:${medication.id}:ingest`;
|
||||
if (!apiToken.permissions.includes(medicationScope)) {
|
||||
return apiError("API-Endpoint fuer dieses Medikament ist deaktiviert", 403);
|
||||
return apiError("API endpoint for this medication is disabled", 403);
|
||||
}
|
||||
|
||||
const event = await prisma.medicationIntakeEvent.create({
|
||||
|
||||
@@ -27,7 +27,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const rl = await checkRateLimit(`insights:${userId}`, 2, 60 * 60 * 1000);
|
||||
if (!rl.allowed) {
|
||||
return apiError(
|
||||
"Maximal 2 Insights-Generierungen pro Stunde. Bitte spaeter erneut versuchen.",
|
||||
"Maximum 2 insight generations per hour. Please try again later.",
|
||||
429,
|
||||
);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
if (!dbUser?.openaiKeyEncrypted) {
|
||||
return apiError(
|
||||
"Kein OpenAI API-Key hinterlegt. Bitte in den Einstellungen konfigurieren.",
|
||||
"No OpenAI API key configured. Please set it in settings.",
|
||||
422,
|
||||
);
|
||||
}
|
||||
@@ -105,23 +105,23 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const errBody = await openaiRes.text();
|
||||
annotate({ meta: { openai_status: openaiRes.status, openai_error: errBody } });
|
||||
if (openaiRes.status === 401) {
|
||||
return apiError("Ungueltiger OpenAI API-Key", 422);
|
||||
return apiError("Invalid OpenAI API key", 422);
|
||||
}
|
||||
return apiError("OpenAI-Anfrage fehlgeschlagen", 502);
|
||||
return apiError("OpenAI request failed", 502);
|
||||
}
|
||||
|
||||
const openaiJson = await openaiRes.json();
|
||||
const content = openaiJson.choices?.[0]?.message?.content;
|
||||
|
||||
if (!content) {
|
||||
return apiError("Keine Antwort von OpenAI erhalten", 502);
|
||||
return apiError("No response from OpenAI", 502);
|
||||
}
|
||||
|
||||
let insights: InsightsOutput;
|
||||
try {
|
||||
insights = JSON.parse(content);
|
||||
} catch {
|
||||
return apiError("OpenAI-Antwort konnte nicht verarbeitet werden", 502);
|
||||
return apiError("Failed to parse OpenAI response", 502);
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
|
||||
@@ -51,7 +51,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
} else {
|
||||
if (!key.startsWith("sk-")) {
|
||||
return apiError(
|
||||
"Ungueltiges API-Key-Format (muss mit sk- beginnen)",
|
||||
"Invalid API key format (must start with sk-)",
|
||||
422,
|
||||
);
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
if (typeof body.privacyMode === "string") {
|
||||
const mode = body.privacyMode as string;
|
||||
if (!["aggregated", "raw"].includes(mode)) {
|
||||
return apiError("Ungueltiger Privacy-Modus", 422);
|
||||
return apiError("Invalid privacy mode", 422);
|
||||
}
|
||||
data.insightsPrivacyMode = mode;
|
||||
// Clear cache when privacy mode changes
|
||||
@@ -72,7 +72,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
}
|
||||
|
||||
if (Object.keys(data).length === 0) {
|
||||
return apiError("Keine Aenderungen", 422);
|
||||
return apiError("No changes", 422);
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
|
||||
@@ -161,7 +161,7 @@ export const GET = apiHandler(async () => {
|
||||
}
|
||||
targets.push({
|
||||
type: "WEIGHT",
|
||||
label: "Gewicht",
|
||||
label: "Weight",
|
||||
current: latestByType.WEIGHT ?? null,
|
||||
average30: avg30ByType.WEIGHT ?? null,
|
||||
trend: computeTrend("WEIGHT"),
|
||||
@@ -186,7 +186,7 @@ export const GET = apiHandler(async () => {
|
||||
}
|
||||
targets.push({
|
||||
type: "BLOOD_PRESSURE",
|
||||
label: "Blutdruck",
|
||||
label: "Blood pressure",
|
||||
current: latestByType.BLOOD_PRESSURE_SYS ?? null,
|
||||
average30: avg30ByType.BLOOD_PRESSURE_SYS ?? null,
|
||||
trend: computeTrend("BLOOD_PRESSURE_SYS"),
|
||||
@@ -227,7 +227,7 @@ export const GET = apiHandler(async () => {
|
||||
|
||||
targets.push({
|
||||
type: "BLOOD_PRESSURE_IN_TARGET",
|
||||
label: "Blutdruck im Zielbereich",
|
||||
label: "Blood pressure on target",
|
||||
current: bpInTargetRate,
|
||||
average30: bpInTargetRate,
|
||||
trend: null,
|
||||
@@ -236,10 +236,10 @@ export const GET = apiHandler(async () => {
|
||||
classification:
|
||||
bpInTargetRate != null
|
||||
? bpInTargetRate >= 70
|
||||
? { category: "Gut", color: "#50fa7b" }
|
||||
? { category: "Good", color: "#50fa7b" }
|
||||
: bpInTargetRate >= 40
|
||||
? { category: "Moderat", color: "#f1fa8c" }
|
||||
: { category: "Niedrig", color: "#ff5555" }
|
||||
? { category: "Moderate", color: "#f1fa8c" }
|
||||
: { category: "Low", color: "#ff5555" }
|
||||
: null,
|
||||
source: "ESC/ESH 2018",
|
||||
});
|
||||
@@ -254,7 +254,7 @@ export const GET = apiHandler(async () => {
|
||||
}
|
||||
targets.push({
|
||||
type: "PULSE",
|
||||
label: "Ruhepuls",
|
||||
label: "Resting pulse",
|
||||
current: latestByType.PULSE ?? null,
|
||||
average30: avg30ByType.PULSE ?? null,
|
||||
trend: computeTrend("PULSE"),
|
||||
@@ -273,7 +273,7 @@ export const GET = apiHandler(async () => {
|
||||
}
|
||||
targets.push({
|
||||
type: "SLEEP_DURATION",
|
||||
label: "Schlafdauer",
|
||||
label: "Sleep duration",
|
||||
current: latestByType.SLEEP_DURATION ?? null,
|
||||
average30: avg30ByType.SLEEP_DURATION ?? null,
|
||||
trend: computeTrend("SLEEP_DURATION"),
|
||||
@@ -335,7 +335,7 @@ export const GET = apiHandler(async () => {
|
||||
}
|
||||
targets.push({
|
||||
type: "BODY_FAT",
|
||||
label: "Koerperfett",
|
||||
label: "Body fat",
|
||||
current: latestByType.BODY_FAT ?? null,
|
||||
average30: avg30ByType.BODY_FAT ?? null,
|
||||
trend: computeTrend("BODY_FAT"),
|
||||
@@ -354,11 +354,11 @@ export const GET = apiHandler(async () => {
|
||||
}
|
||||
targets.push({
|
||||
type: "ACTIVITY_STEPS",
|
||||
label: "Schritte/Tag",
|
||||
label: "Steps/day",
|
||||
current: latestByType.ACTIVITY_STEPS ?? null,
|
||||
average30: avg30ByType.ACTIVITY_STEPS ?? null,
|
||||
trend: computeTrend("ACTIVITY_STEPS"),
|
||||
unit: "Schritte",
|
||||
unit: "steps",
|
||||
range: stepsRange,
|
||||
classification: stepsClassification,
|
||||
source: "WHO",
|
||||
@@ -453,7 +453,7 @@ export const GET = apiHandler(async () => {
|
||||
|
||||
targets.push({
|
||||
type: "MEDICATION_COMPLIANCE",
|
||||
label: "Einnahmetreue",
|
||||
label: "Medication compliance",
|
||||
current: complianceRate7,
|
||||
average30: complianceRate30,
|
||||
trend: null,
|
||||
@@ -462,12 +462,12 @@ export const GET = apiHandler(async () => {
|
||||
classification:
|
||||
complianceRate7 != null
|
||||
? complianceRate7 >= 90
|
||||
? { category: "Sehr gut", color: "#50fa7b" }
|
||||
? { category: "Very good", color: "#50fa7b" }
|
||||
: complianceRate7 >= 70
|
||||
? { category: "Gut", color: "#f1fa8c" }
|
||||
: { category: "Niedrig", color: "#ff5555" }
|
||||
? { category: "Good", color: "#f1fa8c" }
|
||||
: { category: "Low", color: "#ff5555" }
|
||||
: null,
|
||||
source: "7-Tage",
|
||||
source: "7-day",
|
||||
details: {
|
||||
medications: medicationStats.map((medication) => ({
|
||||
name: medication.name,
|
||||
@@ -523,15 +523,15 @@ export const GET = apiHandler(async () => {
|
||||
const moodClassification =
|
||||
latestMoodScore != null
|
||||
? latestMoodScore >= 3.5
|
||||
? { category: "Gut", color: "#50fa7b" }
|
||||
? { category: "Good", color: "#50fa7b" }
|
||||
: latestMoodScore >= 2
|
||||
? { category: "Moderat", color: "#f1fa8c" }
|
||||
: { category: "Niedrig", color: "#ff5555" }
|
||||
? { category: "Moderate", color: "#f1fa8c" }
|
||||
: { category: "Low", color: "#ff5555" }
|
||||
: null;
|
||||
|
||||
targets.push({
|
||||
type: "MOOD_SCORE",
|
||||
label: "Stimmung",
|
||||
label: "Mood",
|
||||
current: latestMoodScore,
|
||||
average30: moodAvg30,
|
||||
trend: moodTrend,
|
||||
@@ -551,14 +551,14 @@ export const GET = apiHandler(async () => {
|
||||
|
||||
const stabilityClassification =
|
||||
stdDev <= 0.5
|
||||
? { category: "Sehr stabil", color: "#50fa7b" }
|
||||
? { category: "Very stable", color: "#50fa7b" }
|
||||
: stdDev <= 1.0
|
||||
? { category: "Stabil", color: "#f1fa8c" }
|
||||
: { category: "Schwankend", color: "#ff5555" };
|
||||
? { category: "Stable", color: "#f1fa8c" }
|
||||
: { category: "Fluctuating", color: "#ff5555" };
|
||||
|
||||
targets.push({
|
||||
type: "MOOD_STABILITY",
|
||||
label: "Stimmungsstabilitaet",
|
||||
label: "Mood stability",
|
||||
current: stdDev,
|
||||
average30: stdDev,
|
||||
trend: null,
|
||||
|
||||
@@ -17,7 +17,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
select: { moodLogGlobal: true },
|
||||
});
|
||||
if (appSettings && !appSettings.moodLogGlobal) {
|
||||
return apiError("moodLog-Integration ist deaktiviert", 403);
|
||||
return apiError("moodLog integration is disabled", 403);
|
||||
}
|
||||
|
||||
let fullSync = false;
|
||||
|
||||
@@ -18,7 +18,7 @@ export const GET = apiHandler(async (_request: NextRequest, { params }: RoutePar
|
||||
});
|
||||
|
||||
if (!measurement || measurement.userId !== user.id) {
|
||||
return apiError("Messwert nicht gefunden", 404);
|
||||
return apiError("Measurement not found", 404);
|
||||
}
|
||||
|
||||
annotate({
|
||||
@@ -42,7 +42,7 @@ export const PUT = apiHandler(async (request: NextRequest, { params }: RoutePara
|
||||
});
|
||||
|
||||
if (!existing || existing.userId !== user.id) {
|
||||
return apiError("Messwert nicht gefunden", 404);
|
||||
return apiError("Measurement not found", 404);
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
@@ -90,7 +90,7 @@ export const DELETE = apiHandler(async (request: NextRequest, { params }: RouteP
|
||||
});
|
||||
|
||||
if (!existing || existing.userId !== user.id) {
|
||||
return apiError("Messwert nicht gefunden", 404);
|
||||
return apiError("Measurement not found", 404);
|
||||
}
|
||||
|
||||
await prisma.measurement.delete({ where: { id } });
|
||||
|
||||
@@ -134,7 +134,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
|
||||
return apiError("Ein Messwert mit diesen Daten existiert bereits", 409);
|
||||
return apiError("A measurement with this data already exists", 409);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@ function medicationScope(medicationId: string): string {
|
||||
export const GET = apiHandler(async (_request: NextRequest, { params }: RouteParams) => {
|
||||
const { user } = await requireAuth();
|
||||
if (!(await isApiGloballyEnabled())) {
|
||||
return apiError("API ist global deaktiviert", 403);
|
||||
return apiError("API is globally disabled", 403);
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const medication = await prisma.medication.findUnique({ where: { id } });
|
||||
if (!medication || medication.userId !== user.id) {
|
||||
return apiError("Medikament nicht gefunden", 404);
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
const scope = medicationScope(id);
|
||||
@@ -56,13 +56,13 @@ export const GET = apiHandler(async (_request: NextRequest, { params }: RoutePar
|
||||
export const PUT = apiHandler(async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { user } = await requireAuth();
|
||||
if (!(await isApiGloballyEnabled())) {
|
||||
return apiError("API ist global deaktiviert", 403);
|
||||
return apiError("API is globally disabled", 403);
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const medication = await prisma.medication.findUnique({ where: { id } });
|
||||
if (!medication || medication.userId !== user.id) {
|
||||
return apiError("Medikament nicht gefunden", 404);
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
let enabled = false;
|
||||
@@ -70,7 +70,7 @@ export const PUT = apiHandler(async (request: NextRequest, { params }: RoutePara
|
||||
const body = await request.json();
|
||||
enabled = body?.enabled === true;
|
||||
} catch {
|
||||
return apiError("Ungültige Anfrage", 422);
|
||||
return apiError("Invalid request", 422);
|
||||
}
|
||||
|
||||
const scope = medicationScope(id);
|
||||
|
||||
@@ -20,7 +20,7 @@ export const GET = apiHandler(async (_request: Request, { params }: RouteParams)
|
||||
});
|
||||
|
||||
if (!medication || medication.userId !== user.id) {
|
||||
return apiError("Medikament nicht gefunden", 404);
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
const events = await prisma.medicationIntakeEvent.findMany({
|
||||
|
||||
@@ -22,7 +22,7 @@ export const PUT = apiHandler(async (request: NextRequest, { params }: RoutePara
|
||||
event.userId !== user.id ||
|
||||
event.medicationId !== id
|
||||
) {
|
||||
return apiError("Einnahme nicht gefunden", 404);
|
||||
return apiError("Intake not found", 404);
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
@@ -77,7 +77,7 @@ export const DELETE = apiHandler(async (request: NextRequest, { params }: RouteP
|
||||
event.userId !== user.id ||
|
||||
event.medicationId !== id
|
||||
) {
|
||||
return apiError("Einnahme nicht gefunden", 404);
|
||||
return apiError("Intake not found", 404);
|
||||
}
|
||||
|
||||
await prisma.medicationIntakeEvent.delete({ where: { id: eventId } });
|
||||
|
||||
@@ -22,7 +22,7 @@ export const POST = apiHandler(async (request: NextRequest, { params }: RoutePar
|
||||
const { id } = await params;
|
||||
const medication = await prisma.medication.findUnique({ where: { id } });
|
||||
if (!medication || medication.userId !== user.id) {
|
||||
return apiError("Medikament nicht gefunden", 404);
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
@@ -39,7 +39,7 @@ export const POST = apiHandler(async (request: NextRequest, { params }: RoutePar
|
||||
const parsed = importSchema.safeParse(payload);
|
||||
if (!parsed.success) {
|
||||
return apiError(
|
||||
`Ungültiges Format: ${parsed.error.issues[0].message}`,
|
||||
`Invalid format: ${parsed.error.issues[0].message}`,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export const DELETE = apiHandler(async (request: NextRequest, { params }: RouteP
|
||||
const { id } = await params;
|
||||
const medication = await prisma.medication.findUnique({ where: { id } });
|
||||
if (!medication || medication.userId !== user.id) {
|
||||
return apiError("Medikament nicht gefunden", 404);
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
const { count } = await prisma.medicationIntakeEvent.deleteMany({
|
||||
|
||||
@@ -17,7 +17,7 @@ export const POST = apiHandler(async (request: NextRequest, { params }: RoutePar
|
||||
const { id } = await params;
|
||||
const medication = await prisma.medication.findUnique({ where: { id } });
|
||||
if (!medication || medication.userId !== user.id) {
|
||||
return apiError("Medikament nicht gefunden", 404);
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
@@ -108,7 +108,7 @@ export const GET = apiHandler(async (request: NextRequest, { params }: RoutePara
|
||||
const { id } = await params;
|
||||
const medication = await prisma.medication.findUnique({ where: { id } });
|
||||
if (!medication || medication.userId !== user.id) {
|
||||
return apiError("Medikament nicht gefunden", 404);
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
const searchParams = Object.fromEntries(request.nextUrl.searchParams);
|
||||
|
||||
@@ -16,7 +16,7 @@ export const GET = apiHandler(async (_request: NextRequest, { params }: RoutePar
|
||||
select: { userId: true },
|
||||
});
|
||||
if (!medication || medication.userId !== user.id) {
|
||||
return apiError("Medikament nicht gefunden", 404);
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
const config = await prisma.reminderPhaseConfig.findUnique({
|
||||
@@ -55,7 +55,7 @@ export const PUT = apiHandler(async (request: NextRequest, { params }: RoutePara
|
||||
select: { userId: true },
|
||||
});
|
||||
if (!medication || medication.userId !== user.id) {
|
||||
return apiError("Medikament nicht gefunden", 404);
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
@@ -63,7 +63,7 @@ export const PUT = apiHandler(async (request: NextRequest, { params }: RoutePara
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = phaseConfigSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiError("Ungültige Eingabe", 400);
|
||||
return apiError("Invalid input", 400);
|
||||
}
|
||||
|
||||
const config = await prisma.reminderPhaseConfig.upsert({
|
||||
@@ -95,7 +95,7 @@ export const DELETE = apiHandler(async (_request: NextRequest, { params }: Route
|
||||
select: { userId: true },
|
||||
});
|
||||
if (!medication || medication.userId !== user.id) {
|
||||
return apiError("Medikament nicht gefunden", 404);
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
await prisma.reminderPhaseConfig.deleteMany({
|
||||
|
||||
@@ -24,7 +24,7 @@ export const GET = apiHandler(async (_request: NextRequest, { params }: RoutePar
|
||||
});
|
||||
|
||||
if (!medication || medication.userId !== user.id) {
|
||||
return apiError("Medikament nicht gefunden", 404);
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
let category = "OTHER";
|
||||
@@ -52,7 +52,7 @@ export const PUT = apiHandler(async (request: NextRequest, { params }: RoutePara
|
||||
const { id } = await params;
|
||||
const existing = await prisma.medication.findUnique({ where: { id } });
|
||||
if (!existing || existing.userId !== user.id) {
|
||||
return apiError("Medikament nicht gefunden", 404);
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
@@ -171,7 +171,7 @@ export const DELETE = apiHandler(async (request: NextRequest, { params }: RouteP
|
||||
const { id } = await params;
|
||||
const existing = await prisma.medication.findUnique({ where: { id } });
|
||||
if (!existing || existing.userId !== user.id) {
|
||||
return apiError("Medikament nicht gefunden", 404);
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
// Revoke API tokens scoped to this medication
|
||||
|
||||
@@ -32,11 +32,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return apiError("Ungültige JSON-Daten", 422);
|
||||
return apiError("Invalid JSON data", 422);
|
||||
}
|
||||
|
||||
const parsed = clientErrorSchema.safeParse(body);
|
||||
if (!parsed.success) return apiError("Ungültige Fehlerdaten", 422);
|
||||
if (!parsed.success) return apiError("Invalid error data", 422);
|
||||
|
||||
const delivery = await sendGlitchtipEvent({
|
||||
dsn: settings.glitchtipDsn,
|
||||
@@ -54,7 +54,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
if (!delivery.ok) {
|
||||
getEvent()?.addWarning("Glitchtip event rejected: " + delivery.method + " " + delivery.status + " " + delivery.details);
|
||||
return apiError("Glitchtip konnte Fehler nicht annehmen", 502);
|
||||
return apiError("Glitchtip could not accept error", 502);
|
||||
}
|
||||
|
||||
return apiSuccess({ sent: true });
|
||||
|
||||
@@ -34,7 +34,7 @@ export const GET = apiHandler(async (_request: NextRequest, { params }: RoutePar
|
||||
const entry = await prisma.moodEntry.findUnique({ where: { id } });
|
||||
|
||||
if (!entry || entry.userId !== user.id) {
|
||||
return apiError("Stimmungseintrag nicht gefunden", 404);
|
||||
return apiError("Mood entry not found", 404);
|
||||
}
|
||||
|
||||
annotate({ action: { name: "mood-entries.get" }, meta: { moodEntryId: id } });
|
||||
@@ -50,7 +50,7 @@ export const PUT = apiHandler(async (request: NextRequest, { params }: RoutePara
|
||||
const existing = await prisma.moodEntry.findUnique({ where: { id } });
|
||||
|
||||
if (!existing || existing.userId !== user.id) {
|
||||
return apiError("Stimmungseintrag nicht gefunden", 404);
|
||||
return apiError("Mood entry not found", 404);
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
@@ -100,7 +100,7 @@ export const DELETE = apiHandler(async (request: NextRequest, { params }: RouteP
|
||||
const existing = await prisma.moodEntry.findUnique({ where: { id } });
|
||||
|
||||
if (!existing || existing.userId !== user.id) {
|
||||
return apiError("Stimmungseintrag nicht gefunden", 404);
|
||||
return apiError("Mood entry not found", 404);
|
||||
}
|
||||
|
||||
await prisma.moodEntry.delete({ where: { id } });
|
||||
|
||||
@@ -113,7 +113,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
} catch (err) {
|
||||
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
|
||||
return apiError(
|
||||
"Ein Stimmungseintrag mit diesen Daten existiert bereits",
|
||||
"A mood entry with this data already exists",
|
||||
409,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,18 +98,18 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return apiError("Ungültige JSON-Daten", 422);
|
||||
return apiError("Invalid JSON data", 422);
|
||||
}
|
||||
|
||||
const parsed = notificationPreferenceSchema.safeParse(body);
|
||||
if (!parsed.success) return apiError("Ungültige Daten", 422);
|
||||
if (!parsed.success) return apiError("Invalid data", 422);
|
||||
|
||||
const { channelId, eventType, enabled } = parsed.data;
|
||||
|
||||
const channel = await prisma.notificationChannel.findFirst({
|
||||
where: { id: channelId, userId: user.id },
|
||||
});
|
||||
if (!channel) return apiError("Kanal nicht gefunden", 404);
|
||||
if (!channel) return apiError("Channel not found", 404);
|
||||
|
||||
const preference = await prisma.notificationPreference.upsert({
|
||||
where: {
|
||||
|
||||
@@ -30,11 +30,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return apiError("Ungültige JSON-Daten", 422);
|
||||
return apiError("Invalid JSON data", 422);
|
||||
}
|
||||
|
||||
const parsed = subscribeSchema.safeParse(body);
|
||||
if (!parsed.success) return apiError("Ungültige Daten", 422);
|
||||
if (!parsed.success) return apiError("Invalid data", 422);
|
||||
|
||||
const { endpoint, keys } = parsed.data;
|
||||
const userAgent = request.headers.get("user-agent") ?? undefined;
|
||||
@@ -92,11 +92,11 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return apiError("Ungültige JSON-Daten", 422);
|
||||
return apiError("Invalid JSON data", 422);
|
||||
}
|
||||
|
||||
const parsed = unsubscribeSchema.safeParse(body);
|
||||
if (!parsed.success) return apiError("Ungültige Daten", 422);
|
||||
if (!parsed.success) return apiError("Invalid data", 422);
|
||||
|
||||
await prisma.pushSubscription.deleteMany({
|
||||
where: {
|
||||
|
||||
@@ -24,7 +24,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
if (jsonError) return jsonError;
|
||||
const result = z.safeParse(onboardingSchema, body);
|
||||
if (!result.success) {
|
||||
return apiError("Ungültige Eingabe", 422);
|
||||
return apiError("Invalid input", 422);
|
||||
}
|
||||
|
||||
const data: Record<string, unknown> = {
|
||||
|
||||
@@ -20,12 +20,12 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
|
||||
const body = await request.json();
|
||||
confirm = typeof body?.confirm === "string" ? body.confirm : "";
|
||||
} catch {
|
||||
return apiError("Ungueltige Anfrage", 422);
|
||||
return apiError("Invalid request", 422);
|
||||
}
|
||||
|
||||
if (confirm !== "DELETE_ACCOUNT") {
|
||||
return apiError(
|
||||
"Bestaetigung fehlt. Sende { confirm: 'DELETE_ACCOUNT' }",
|
||||
"Confirmation missing. Send { confirm: 'DELETE_ACCOUNT' }",
|
||||
422,
|
||||
);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
|
||||
const adminCount = await prisma.user.count({ where: { role: "ADMIN" } });
|
||||
if (adminCount <= 1) {
|
||||
return apiError(
|
||||
"Der letzte Admin-Account kann nicht geloescht werden",
|
||||
"The last admin account cannot be deleted",
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
|
||||
const body = await request.json();
|
||||
confirm = typeof body?.confirm === "string" ? body.confirm : "";
|
||||
} catch {
|
||||
return apiError("Ungueltige Anfrage", 422);
|
||||
return apiError("Invalid request", 422);
|
||||
}
|
||||
|
||||
if (confirm !== "DELETE") {
|
||||
return apiError("Bestaetigung fehlt", 422);
|
||||
return apiError("Confirmation missing", 422);
|
||||
}
|
||||
|
||||
const userId = user.id;
|
||||
|
||||
@@ -21,7 +21,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
const parsed = moodLogCredentialsSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiError("Ungueltige Eingabe", 422);
|
||||
return apiError("Invalid input", 422);
|
||||
}
|
||||
|
||||
const { url, apiKey } = parsed.data;
|
||||
|
||||
@@ -51,13 +51,13 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = ntfySettingsSchema.safeParse(body);
|
||||
if (!parsed.success) return apiError("Ungueltige Daten", 422);
|
||||
if (!parsed.success) return apiError("Invalid data", 422);
|
||||
|
||||
const { serverUrl, topic, authToken, enabled } = parsed.data;
|
||||
|
||||
if (enabled && (!serverUrl || !topic)) {
|
||||
return apiError(
|
||||
"Server-URL und Topic sind erforderlich, wenn ntfy aktiviert ist",
|
||||
"Server URL and topic are required when ntfy is enabled",
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const POST = apiHandler(async () => {
|
||||
|
||||
const rl = await checkRateLimit(`ntfy-test:${user.id}`, 5, 5 * 60 * 1000);
|
||||
if (!rl.allowed) {
|
||||
return apiError("Maximal 5 Tests in 5 Minuten", 429);
|
||||
return apiError("Maximum 5 tests in 5 minutes", 429);
|
||||
}
|
||||
|
||||
const channel = await prisma.notificationChannel.findUnique({
|
||||
@@ -25,13 +25,13 @@ export const POST = apiHandler(async () => {
|
||||
});
|
||||
|
||||
if (!channel) {
|
||||
return apiError("ntfy ist nicht konfiguriert", 400);
|
||||
return apiError("ntfy is not configured", 400);
|
||||
}
|
||||
|
||||
const config = JSON.parse(decrypt(channel.config)) as NtfyChannelConfig;
|
||||
|
||||
if (!config.serverUrl || !config.topic) {
|
||||
return apiError("Server-URL und Topic sind erforderlich", 400);
|
||||
return apiError("Server URL and topic are required", 400);
|
||||
}
|
||||
|
||||
const success = await sendViaNtfy(config, {
|
||||
@@ -39,11 +39,11 @@ export const POST = apiHandler(async () => {
|
||||
userId: user.id,
|
||||
title: "HealthLog Test",
|
||||
message:
|
||||
"HealthLog: Verbindung erfolgreich! ntfy-Benachrichtigungen sind aktiv.",
|
||||
"HealthLog: Connection successful! ntfy notifications are active.",
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
return apiError("Testnachricht konnte nicht gesendet werden", 500);
|
||||
return apiError("Failed to send test message", 500);
|
||||
}
|
||||
|
||||
annotate({ action: { name: "settings.ntfy.test" }, meta: { success: true } });
|
||||
|
||||
@@ -47,14 +47,14 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
telegramEnabled: true,
|
||||
},
|
||||
});
|
||||
if (!current) return apiError("Benutzer nicht gefunden", 404);
|
||||
if (!current) return apiError("User not found", 404);
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
|
||||
if (jsonError) return jsonError;
|
||||
const result = z.safeParse(telegramSettingsSchema, body);
|
||||
if (!result.success) {
|
||||
return apiError("Ungueltige Eingabe", 422);
|
||||
return apiError("Invalid input", 422);
|
||||
}
|
||||
|
||||
const { botToken, chatId, enabled } = result.data;
|
||||
@@ -68,7 +68,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
if (enabled && (!hasTokenAfter || !hasChatIdAfter)) {
|
||||
return apiError(
|
||||
"Fuer aktiviertes Telegram werden Bot-Token und Chat-ID benoetigt",
|
||||
"Bot token and chat ID are required when Telegram is enabled",
|
||||
422,
|
||||
);
|
||||
}
|
||||
@@ -82,7 +82,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
const appUrl = process.env.APP_URL ?? process.env.NEXT_PUBLIC_APP_URL;
|
||||
if (!appUrl) {
|
||||
return apiError(
|
||||
"Server-Konfiguration fehlerhaft: APP_URL (oder NEXT_PUBLIC_APP_URL) fehlt.",
|
||||
"Server configuration error: APP_URL (or NEXT_PUBLIC_APP_URL) is missing.",
|
||||
500,
|
||||
);
|
||||
}
|
||||
@@ -92,14 +92,14 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
appBaseUrl = new URL(appUrl);
|
||||
} catch {
|
||||
return apiError(
|
||||
"Server-Konfiguration fehlerhaft: APP_URL ist ungueltig.",
|
||||
"Server configuration error: APP_URL is invalid.",
|
||||
500,
|
||||
);
|
||||
}
|
||||
|
||||
if (appBaseUrl.protocol !== "https:") {
|
||||
return apiError(
|
||||
"Telegram-Webhook benoetigt eine oeffentliche HTTPS-URL (kein http/localhost).",
|
||||
"Telegram webhook requires a public HTTPS URL (no http/localhost).",
|
||||
422,
|
||||
);
|
||||
}
|
||||
@@ -107,7 +107,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
const localHosts = new Set(["localhost", "127.0.0.1", "::1"]);
|
||||
if (localHosts.has(appBaseUrl.hostname.toLowerCase())) {
|
||||
return apiError(
|
||||
"Telegram-Webhook benoetigt eine oeffentlich erreichbare Domain (localhost ist nicht erlaubt).",
|
||||
"Telegram webhook requires a publicly reachable domain (localhost is not allowed).",
|
||||
422,
|
||||
);
|
||||
}
|
||||
@@ -117,7 +117,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
!["80", "88", "443", "8443"].includes(appBaseUrl.port)
|
||||
) {
|
||||
return apiError(
|
||||
"Telegram-Webhook erlaubt nur die Ports 80, 88, 443 oder 8443.",
|
||||
"Telegram webhook only allows ports 80, 88, 443, or 8443.",
|
||||
422,
|
||||
);
|
||||
}
|
||||
@@ -131,7 +131,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
);
|
||||
if (!ok) {
|
||||
return apiError(
|
||||
"Telegram-Webhook konnte nicht gesetzt werden. Pruefe Bot-Token und Erreichbarkeit.",
|
||||
"Failed to set Telegram webhook. Check bot token and reachability.",
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export const POST = apiHandler(async () => {
|
||||
|
||||
const rl = await checkRateLimit(`telegram-test:${user.id}`, 5, 5 * 60 * 1000);
|
||||
if (!rl.allowed) {
|
||||
return apiError("Maximal 5 Tests in 5 Minuten", 429);
|
||||
return apiError("Maximum 5 tests in 5 minutes", 429);
|
||||
}
|
||||
|
||||
const dbUser = await prisma.user.findUnique({
|
||||
@@ -24,7 +24,7 @@ export const POST = apiHandler(async () => {
|
||||
|
||||
if (!dbUser?.telegramBotToken || !dbUser?.telegramChatId) {
|
||||
return apiError(
|
||||
"Bot-Token und Chat-ID muessen zuerst gespeichert werden",
|
||||
"Bot token and chat ID must be saved first",
|
||||
422,
|
||||
);
|
||||
}
|
||||
@@ -33,12 +33,12 @@ export const POST = apiHandler(async () => {
|
||||
const ok = await sendTelegramMessage(
|
||||
botToken,
|
||||
dbUser.telegramChatId,
|
||||
"HealthLog: Verbindung erfolgreich! Telegram-Benachrichtigungen sind aktiv.",
|
||||
"HealthLog: Connection successful! Telegram notifications are active.",
|
||||
);
|
||||
|
||||
if (!ok) {
|
||||
return apiError(
|
||||
"Nachricht konnte nicht gesendet werden. Pruefe Bot-Token und Chat-ID.",
|
||||
"Failed to send message. Check bot token and chat ID.",
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ export const DELETE = apiHandler(async (_request: Request, { params }: RoutePara
|
||||
annotate({ action: { name: "tokens.revoke" } });
|
||||
|
||||
if (!(await isApiGloballyEnabled())) {
|
||||
return apiError("API ist global deaktiviert", 403);
|
||||
return apiError("API is globally disabled", 403);
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const token = await prisma.apiToken.findUnique({ where: { id } });
|
||||
|
||||
if (!token || token.userId !== user.id) {
|
||||
return apiError("Token nicht gefunden", 404);
|
||||
return apiError("Token not found", 404);
|
||||
}
|
||||
|
||||
await prisma.apiToken.update({
|
||||
|
||||
@@ -9,7 +9,7 @@ import { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
const createTokenSchema = z.object({
|
||||
name: z.string().min(1, "Name erforderlich").max(100),
|
||||
name: z.string().min(1, "Name required").max(100),
|
||||
expiresInDays: z.number().int().min(1).max(365).optional(),
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ export const GET = apiHandler(async () => {
|
||||
annotate({ action: { name: "tokens.list" } });
|
||||
|
||||
if (!(await isApiGloballyEnabled())) {
|
||||
return apiError("API ist global deaktiviert", 403);
|
||||
return apiError("API is globally disabled", 403);
|
||||
}
|
||||
|
||||
const tokens = await prisma.apiToken.findMany({
|
||||
@@ -43,7 +43,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
annotate({ action: { name: "tokens.create" } });
|
||||
|
||||
if (!(await isApiGloballyEnabled())) {
|
||||
return apiError("API ist global deaktiviert", 403);
|
||||
return apiError("API is globally disabled", 403);
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
|
||||
@@ -41,7 +41,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
if (jsonError) return jsonError;
|
||||
const result = z.safeParse(withingsCredentialsSchema, body);
|
||||
if (!result.success) {
|
||||
return apiError("Client-ID und Client-Secret sind erforderlich", 422);
|
||||
return apiError("Client ID and Client Secret are required", 422);
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
|
||||
@@ -19,7 +19,7 @@ export const POST = apiHandler(async () => {
|
||||
});
|
||||
|
||||
if (!connection) {
|
||||
return apiError("Keine Withings-Verbindung vorhanden", 404);
|
||||
return apiError("No Withings connection", 404);
|
||||
}
|
||||
|
||||
// Try to unsubscribe webhook (best-effort)
|
||||
|
||||
Reference in new issue
Block a user