mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
Long-overdue prettier sweep; prettier had drifted vs. checked-in code across ~130 files (mostly long-argument line-break wrapping in TS and table-padding in markdown). Pure formatting — no semantic changes — and required to clear the v1.4.6 release gate's `pnpm format:check`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
235 lines
7.7 KiB
TypeScript
235 lines
7.7 KiB
TypeScript
import { prisma } from "@/lib/db";
|
|
import { apiHandler, requireAdmin } from "@/lib/api-handler";
|
|
import { auditLog } from "@/lib/auth/audit";
|
|
import {
|
|
apiSuccess,
|
|
apiError,
|
|
getClientIp,
|
|
safeJson,
|
|
} from "@/lib/api-response";
|
|
import { annotate } from "@/lib/logging/context";
|
|
import { encrypt } from "@/lib/crypto";
|
|
import { adminSettingsSchema } from "@/lib/validations/admin";
|
|
import { NextRequest } from "next/server";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export const GET = apiHandler(async () => {
|
|
await requireAdmin();
|
|
annotate({ action: { name: "admin.settings.get" } });
|
|
|
|
const settings = await prisma.appSettings.findUnique({
|
|
where: { id: "singleton" },
|
|
});
|
|
|
|
return apiSuccess({
|
|
registrationEnabled: settings?.registrationEnabled ?? true,
|
|
defaultLocale: settings?.defaultLocale ?? "de",
|
|
telegramGlobal: settings?.telegramGlobal ?? true,
|
|
ntfyGlobal: settings?.ntfyGlobal ?? true,
|
|
webPushGlobal: settings?.webPushGlobal ?? true,
|
|
webPushVapidPublicKey: settings?.webPushVapidPublicKey ?? null,
|
|
webPushVapidSubject: settings?.webPushVapidSubject ?? null,
|
|
webPushVapidConfigured: Boolean(
|
|
settings?.webPushVapidPublicKey &&
|
|
settings?.webPushVapidPrivateKeyEncrypted &&
|
|
settings?.webPushVapidSubject,
|
|
),
|
|
apiGlobal: settings?.apiGlobal ?? true,
|
|
umamiEnabled: settings?.umamiEnabled ?? false,
|
|
umamiScriptUrl: settings?.umamiScriptUrl ?? null,
|
|
umamiWebsiteId: settings?.umamiWebsiteId ?? null,
|
|
glitchtipEnabled: settings?.glitchtipEnabled ?? false,
|
|
glitchtipDsn: settings?.glitchtipDsn ?? null,
|
|
glitchtipEnvironment: settings?.glitchtipEnvironment ?? "production",
|
|
bugReportRepo: settings?.githubIssueRepo ?? null,
|
|
bugReportConfigured: Boolean(
|
|
settings?.githubIssueRepo && settings?.githubIssueTokenEncrypted,
|
|
),
|
|
bugReportEnabled: settings?.bugReportEnabled ?? true,
|
|
reminderLateMinutes: settings?.reminderLateMinutes ?? 120,
|
|
reminderMissedMinutes: settings?.reminderMissedMinutes ?? 240,
|
|
moodLogGlobal: settings?.moodLogGlobal ?? true,
|
|
});
|
|
});
|
|
|
|
export const PUT = apiHandler(async (request: NextRequest) => {
|
|
const { user } = await requireAdmin();
|
|
annotate({ action: { name: "admin.settings.update" } });
|
|
|
|
const { data: body, error: jsonError } = await safeJson(request);
|
|
|
|
if (jsonError) return jsonError;
|
|
const parsed = adminSettingsSchema.safeParse(body);
|
|
if (!parsed.success) {
|
|
return apiError(parsed.error.issues[0].message, 422);
|
|
}
|
|
|
|
const data = parsed.data;
|
|
const updates: Record<string, unknown> = {};
|
|
const auditDetails: Record<string, unknown> = {};
|
|
|
|
// Boolean fields — direct mapping
|
|
const booleanFields = [
|
|
"registrationEnabled",
|
|
"telegramGlobal",
|
|
"ntfyGlobal",
|
|
"webPushGlobal",
|
|
"apiGlobal",
|
|
"umamiEnabled",
|
|
"glitchtipEnabled",
|
|
"moodLogGlobal",
|
|
"bugReportEnabled",
|
|
] as const;
|
|
for (const field of booleanFields) {
|
|
if (data[field] !== undefined) {
|
|
updates[field] = data[field];
|
|
auditDetails[field] = data[field];
|
|
}
|
|
}
|
|
|
|
if (data.defaultLocale !== undefined) {
|
|
updates.defaultLocale = data.defaultLocale;
|
|
auditDetails.defaultLocale = data.defaultLocale;
|
|
}
|
|
|
|
// String fields that map directly (with empty → null)
|
|
if (data.webPushVapidPublicKey !== undefined) {
|
|
const value = data.webPushVapidPublicKey.trim();
|
|
updates.webPushVapidPublicKey = value || null;
|
|
auditDetails.webPushVapidPublicKey = value ? "configured" : null;
|
|
}
|
|
|
|
if (data.webPushVapidSubject !== undefined) {
|
|
const value = data.webPushVapidSubject.trim();
|
|
updates.webPushVapidSubject = value || null;
|
|
auditDetails.webPushVapidSubject = value || null;
|
|
}
|
|
|
|
// Encrypted fields
|
|
if (data.webPushVapidPrivateKey !== undefined) {
|
|
const value = data.webPushVapidPrivateKey.trim();
|
|
if (value) {
|
|
updates.webPushVapidPrivateKeyEncrypted = encrypt(value);
|
|
auditDetails.webPushVapidPrivateKeyUpdated = true;
|
|
}
|
|
}
|
|
if (data.clearWebPushVapidPrivateKey === true) {
|
|
updates.webPushVapidPrivateKeyEncrypted = null;
|
|
auditDetails.webPushVapidPrivateKeyUpdated = false;
|
|
}
|
|
|
|
// URL fields with normalization
|
|
if (data.umamiScriptUrl !== undefined) {
|
|
const value = data.umamiScriptUrl.trim();
|
|
if (!value) {
|
|
updates.umamiScriptUrl = null;
|
|
auditDetails.umamiScriptUrl = null;
|
|
} else {
|
|
const parsed = new URL(value);
|
|
if (parsed.pathname === "/" || parsed.pathname === "") {
|
|
parsed.pathname = "/script.js";
|
|
}
|
|
updates.umamiScriptUrl = parsed.toString();
|
|
auditDetails.umamiScriptUrl = parsed.toString();
|
|
}
|
|
}
|
|
|
|
if (data.umamiWebsiteId !== undefined) {
|
|
const value = data.umamiWebsiteId.trim();
|
|
updates.umamiWebsiteId = value || null;
|
|
auditDetails.umamiWebsiteId = value || null;
|
|
}
|
|
|
|
if (data.glitchtipDsn !== undefined) {
|
|
const value = data.glitchtipDsn.trim();
|
|
if (!value) {
|
|
updates.glitchtipDsn = null;
|
|
auditDetails.glitchtipDsn = null;
|
|
} else {
|
|
updates.glitchtipDsn = new URL(value).toString();
|
|
auditDetails.glitchtipDsn = "configured";
|
|
}
|
|
}
|
|
|
|
if (data.glitchtipEnvironment !== undefined) {
|
|
const value = data.glitchtipEnvironment.trim();
|
|
updates.glitchtipEnvironment = value || null;
|
|
auditDetails.glitchtipEnvironment = value || null;
|
|
}
|
|
|
|
// Bug report
|
|
if (data.bugReportRepo !== undefined) {
|
|
const repo = data.bugReportRepo.trim();
|
|
updates.githubIssueRepo = repo || null;
|
|
auditDetails.bugReportRepo = repo || null;
|
|
}
|
|
if (data.bugReportToken !== undefined) {
|
|
const token = data.bugReportToken.trim();
|
|
if (token) {
|
|
updates.githubIssueTokenEncrypted = encrypt(token);
|
|
auditDetails.bugReportTokenUpdated = true;
|
|
}
|
|
}
|
|
if (data.clearBugReportToken === true) {
|
|
updates.githubIssueTokenEncrypted = null;
|
|
auditDetails.bugReportTokenUpdated = false;
|
|
}
|
|
|
|
// Numeric thresholds
|
|
if (data.reminderLateMinutes !== undefined) {
|
|
updates.reminderLateMinutes = data.reminderLateMinutes;
|
|
auditDetails.reminderLateMinutes = data.reminderLateMinutes;
|
|
}
|
|
if (data.reminderMissedMinutes !== undefined) {
|
|
updates.reminderMissedMinutes = data.reminderMissedMinutes;
|
|
auditDetails.reminderMissedMinutes = data.reminderMissedMinutes;
|
|
}
|
|
|
|
if (Object.keys(updates).length === 0) {
|
|
return apiError("No valid fields", 422);
|
|
}
|
|
|
|
const settings = await prisma.appSettings.upsert({
|
|
where: { id: "singleton" },
|
|
update: updates,
|
|
create: { id: "singleton", ...updates },
|
|
});
|
|
|
|
await auditLog("admin.settings.update", {
|
|
userId: user.id,
|
|
ipAddress: getClientIp(request),
|
|
details: auditDetails,
|
|
});
|
|
|
|
return apiSuccess({
|
|
registrationEnabled: settings.registrationEnabled,
|
|
defaultLocale: settings.defaultLocale,
|
|
telegramGlobal: settings.telegramGlobal,
|
|
ntfyGlobal: settings.ntfyGlobal,
|
|
webPushGlobal: settings.webPushGlobal,
|
|
webPushVapidPublicKey: settings.webPushVapidPublicKey,
|
|
webPushVapidSubject: settings.webPushVapidSubject,
|
|
webPushVapidConfigured: Boolean(
|
|
settings.webPushVapidPublicKey &&
|
|
settings.webPushVapidPrivateKeyEncrypted &&
|
|
settings.webPushVapidSubject,
|
|
),
|
|
apiGlobal: settings.apiGlobal,
|
|
umamiEnabled: settings.umamiEnabled,
|
|
umamiScriptUrl: settings.umamiScriptUrl,
|
|
umamiWebsiteId: settings.umamiWebsiteId,
|
|
glitchtipEnabled: settings.glitchtipEnabled,
|
|
glitchtipDsn: settings.glitchtipDsn,
|
|
glitchtipEnvironment: settings.glitchtipEnvironment ?? "production",
|
|
bugReportRepo: settings.githubIssueRepo,
|
|
bugReportConfigured: Boolean(
|
|
settings.githubIssueRepo && settings.githubIssueTokenEncrypted,
|
|
),
|
|
bugReportEnabled: settings.bugReportEnabled,
|
|
reminderLateMinutes: settings.reminderLateMinutes,
|
|
reminderMissedMinutes: settings.reminderMissedMinutes,
|
|
moodLogGlobal: settings.moodLogGlobal,
|
|
});
|
|
});
|