mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
Initial release v1.0.0
HealthLog — personal health-tracking PWA with weight, blood pressure, pulse, mood tracking, medication compliance, Withings integration, doctor report PDF export, and Dracula-themed UI.
This commit is contained in:
commit
38309932f3
301 files changed
+60041
No files matched your search
@@ -0,0 +1,65 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAdmin } from "@/lib/api-handler";
|
||||
import { hashPassword, checkPasswordStrength } from "@/lib/auth/password";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { apiSuccess, apiError, getClientIp, safeJson } from "@/lib/api-response";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
|
||||
import { destroyAllSessions } from "@/lib/auth/session";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export const POST = apiHandler(async (
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) => {
|
||||
const ip = getClientIp(request);
|
||||
const rl = await checkRateLimit(`admin-reset-pw:${ip}`, 10, 60 * 1000);
|
||||
if (!rl.allowed) {
|
||||
return NextResponse.json(
|
||||
{ data: null, error: "Rate limit exceeded" },
|
||||
{ status: 429, headers: rateLimitHeaders(rl) },
|
||||
);
|
||||
}
|
||||
|
||||
const { user } = await requireAdmin();
|
||||
|
||||
const { id } = await params;
|
||||
annotate({ action: { name: "admin.users.reset-password", entity_type: "user", entity_id: id } });
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
|
||||
if (jsonError) return jsonError;
|
||||
const { password } = body as { password?: string };
|
||||
|
||||
if (!password || typeof password !== "string") {
|
||||
return apiError("Passwort erforderlich", 422);
|
||||
}
|
||||
|
||||
const target = await prisma.user.findUnique({ where: { id } });
|
||||
if (!target) return apiError("Benutzer nicht gefunden", 404);
|
||||
|
||||
const strength = checkPasswordStrength(password, [target.username]);
|
||||
if (!strength.isAcceptable) {
|
||||
return apiError(
|
||||
strength.feedback[0] || "Passwort zu schwach (Score < 3)",
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
const passwordHash = await hashPassword(password);
|
||||
await prisma.user.update({
|
||||
where: { id },
|
||||
data: { passwordHash },
|
||||
});
|
||||
|
||||
// Invalidate all sessions of the target user
|
||||
await destroyAllSessions(id);
|
||||
|
||||
await auditLog("admin.user.reset-password", {
|
||||
userId: user.id,
|
||||
ipAddress: getClientIp(request),
|
||||
details: { targetUserId: id },
|
||||
});
|
||||
|
||||
return apiSuccess({ success: true });
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
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 { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
const updateUserSchema = z.object({
|
||||
username: z.string().min(3).max(30).optional(),
|
||||
email: z.string().email().nullable().optional(),
|
||||
role: z.enum(["ADMIN", "USER"]).optional(),
|
||||
});
|
||||
|
||||
export const PUT = apiHandler(async (
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) => {
|
||||
const { user } = await requireAdmin();
|
||||
|
||||
const { id } = await params;
|
||||
annotate({ action: { name: "admin.users.update", entity_type: "user", entity_id: id } });
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = updateUserSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiError(parsed.error.issues[0].message, 422);
|
||||
}
|
||||
|
||||
const target = await prisma.user.findUnique({ where: { id } });
|
||||
if (!target) return apiError("Benutzer nicht gefunden", 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);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(parsed.data.username !== undefined && {
|
||||
username: parsed.data.username,
|
||||
}),
|
||||
...(parsed.data.email !== undefined && { email: parsed.data.email }),
|
||||
...(parsed.data.role !== undefined && { role: parsed.data.role }),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
role: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
await auditLog("admin.user.update", {
|
||||
userId: user.id,
|
||||
ipAddress: getClientIp(request),
|
||||
details: { targetUserId: id, changes: parsed.data },
|
||||
});
|
||||
|
||||
return apiSuccess(updatedUser);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAdmin } from "@/lib/api-handler";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const GET = apiHandler(async () => {
|
||||
await requireAdmin();
|
||||
annotate({ action: { name: "admin.users.list" } });
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
role: true,
|
||||
createdAt: true,
|
||||
_count: { select: { passkeys: true } },
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
});
|
||||
|
||||
return apiSuccess(
|
||||
users.map((u) => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
role: u.role,
|
||||
createdAt: u.createdAt,
|
||||
passkeyCount: u._count.passkeys,
|
||||
})),
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user