feat: public events/polls, friends graph, captcha, SSE hardening, and admin UX
Ship product gaps: register/vote pages, friend add/accept/decline/remove, email verify TTL, captcha on login/forgot, soft-fail user actions, SSE abort/shared client, Commando Centrum error toasts, admin delete for events/polls, and IT/NL i18n fills. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
2ff08e5127
commit
ed7db6e048
76 files changed
+4834
-1376
No files matched your search
@@ -4,16 +4,25 @@ import { env } from "@/env";
|
||||
import { checkLogin } from "@/lib/auth/password";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||
import { captchaConfig, verifyCaptcha } from "@/lib/services/captcha";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
export type PrecheckResult = "ok" | "invalid" | "twofactor";
|
||||
export type PrecheckResult =
|
||||
| "ok"
|
||||
| "invalid"
|
||||
| "twofactor"
|
||||
| "unverified"
|
||||
| "captcha";
|
||||
|
||||
/**
|
||||
* Validates username+password WITHOUT creating a session, and reports whether a
|
||||
* TOTP code is still required. Lets the login form do the two-step 2FA flow.
|
||||
* Also enforces captcha + optional email-verification when configured.
|
||||
*/
|
||||
export async function precheckLogin(
|
||||
username: string,
|
||||
password: string,
|
||||
captchaToken?: string | null,
|
||||
): Promise<PrecheckResult> {
|
||||
const u = String(username ?? "")
|
||||
.normalize("NFC")
|
||||
@@ -21,14 +30,29 @@ export async function precheckLogin(
|
||||
const p = String(password ?? "");
|
||||
if (!u || !p) return "invalid";
|
||||
|
||||
if (!(await rateLimit(`precheck:${await clientIp()}`, 10, 5 * 60_000)).ok)
|
||||
return "invalid";
|
||||
const ip = await clientIp();
|
||||
if (!(await rateLimit(`precheck:${ip}`, 10, 5 * 60_000)).ok) return "invalid";
|
||||
|
||||
let user: { password: string; twoFactorConfirmedAt: Date | null } | null;
|
||||
const cfg = await captchaConfig();
|
||||
if (cfg.provider !== "none") {
|
||||
if (!(await verifyCaptcha(captchaToken ?? null, ip))) return "captcha";
|
||||
}
|
||||
|
||||
let user: {
|
||||
password: string;
|
||||
twoFactorConfirmedAt: Date | null;
|
||||
mail: string | null;
|
||||
mailVerified: string;
|
||||
} | null;
|
||||
try {
|
||||
user = await prisma.user.findUnique({
|
||||
where: { username: u },
|
||||
select: { password: true, twoFactorConfirmedAt: true },
|
||||
select: {
|
||||
password: true,
|
||||
twoFactorConfirmedAt: true,
|
||||
mail: true,
|
||||
mailVerified: true,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return "invalid";
|
||||
@@ -50,5 +74,13 @@ export async function precheckLogin(
|
||||
});
|
||||
if (!res.valid) return "invalid";
|
||||
|
||||
if (
|
||||
(await siteSettings.getBool("require_email_verification", false)) &&
|
||||
user.mail &&
|
||||
user.mailVerified !== "1"
|
||||
) {
|
||||
return "unverified";
|
||||
}
|
||||
|
||||
return user.twoFactorConfirmedAt ? "twofactor" : "ok";
|
||||
}
|
||||
Reference in new issue
Block a user