chahinebrini 8e562c982d feat(backend): MDM-Managed Flag — migration + endpoint + guards
- Prisma migration: users.mdm_managed (Boolean DEFAULT false) + users.mdm_detected_at (DateTime?)
- setMdmManaged() helper in server/db/profile.ts
- POST /api/users/me/mdm-status — App reports MDM status to backend
- cooldown/status + cooldown/request — early-return 400 when mdm_managed
- protection/state — response extended with mdmManaged: boolean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 00:46:44 +02:00

74 lines
2.5 KiB
TypeScript

import { requireUser } from "../../utils/auth";
import { getActiveCooldown, createCooldown } from "../../db/cooldown";
import { signCooldownToken, generateJti } from "../../utils/cooldownToken";
import { usePrisma } from "../../utils/prisma";
/** POST /api/cooldown/request — Start a 24h cooldown before protection can be disabled. */
export default defineEventHandler(async (event) => {
const user = await requireUser(event);
const body = await readBody(event).catch(() => ({}));
// ─── MDM-Guard: MDM-User können sich nie selbst deaktivieren.
const db = usePrisma();
const profile = await db.profile.findUnique({
where: { id: user.id },
select: { mdmManaged: true },
});
if (profile?.mdmManaged) {
throw createError({
statusCode: 400,
data: { error: "mdm_managed_cannot_self_deactivate" },
});
}
// Reject if a cooldown is already running (not resolved, not cancelled).
const existing = await getActiveCooldown(user.id);
if (existing) {
const now = new Date();
// If the existing one already expired but wasn't resolved yet, that's fine —
// it means canDisableProtection is already true. Return 409 so the client
// calls /status instead.
throw createError({
statusCode: 409,
data: {
error: "cooldown_already_active",
existingEndsAt: existing.cooldownEndsAt.toISOString(),
},
});
}
// Test-Mode (5min statt 24h) — nur außerhalb von Production aktivierbar.
// Detection via appUrl statt NODE_ENV, da staging.rebreak.org auch mit
// NODE_ENV=production läuft (siehe start-staging.sh).
const config = useRuntimeConfig(event);
const appUrl = (config.public?.appUrl as string) ?? "";
const isProductionUrl =
appUrl.includes("rebreak.org") && !appUrl.includes("staging");
const isTestMode = body?.testMode === true && !isProductionUrl;
const cooldownMs = isTestMode ? 40 * 1000 : 24 * 60 * 60 * 1000;
const now = new Date();
const cooldownEndsAt = new Date(now.getTime() + cooldownMs);
const jti = generateJti();
await createCooldown(user.id, jti, cooldownEndsAt, body?.reason);
const remainingSeconds = Math.max(
0,
Math.floor((cooldownEndsAt.getTime() - Date.now()) / 1000),
);
const token = await signCooldownToken(user.id, jti, cooldownEndsAt);
return {
success: true,
data: {
cooldownStartedAt: now.toISOString(),
cooldownEndsAt: cooldownEndsAt.toISOString(),
remainingSeconds,
token,
testMode: isTestMode,
},
};
});