feat(auth): implement all 16 homepage/login/register review items
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 33s
CI / tests-integration (push) Successful in 1m42s
CI / tests-unit (push) Successful in 1m47s
CI / tests-ui (push) Successful in 2m30s
CI / preflight (push) Skipped
CI / deploy (push) Failing after 2m56s
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 33s
CI / tests-integration (push) Successful in 1m42s
CI / tests-unit (push) Successful in 1m47s
CI / tests-ui (push) Successful in 2m30s
CI / preflight (push) Skipped
CI / deploy (push) Failing after 2m56s
- add countArticles() (published-only, mirrors news-list) and warm total_articles - localize homepage metadata; bind articleCount to both stats; unique photo alts - drop duplicate news date and the mascot preload priorities - extract shared AuthPageFrame/AuthUsersCards used by /login and /register - login: localized noindex metadata, session redirect via safeRedirectPath, ?from passthrough from proxy, unified auth roster cache keys, registered notice - register: localized metadata, session redirect to /me, unified cache keys - add resend-verification flow on /verify with rate-limited non-enumerable action - add safeRedirectPath() with unit tests - register form: live requirements checklist + password mismatch guard - login form: unverified state with resend-link CTA - honour prefers-reduced-motion in TypewriterText - add 6 translations across all 25 locales
This commit is contained in:
1 parent
8561c3f85e
commit
3933214953
43 files changed
+1037
-490
No files matched your search
@@ -0,0 +1,54 @@
|
||||
"use server";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import { sendVerification } from "@/lib/auth/email-verification";
|
||||
import { db, User } from "@/lib/db";
|
||||
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||
|
||||
export interface ResendVerificationState {
|
||||
ok: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
/**
|
||||
* Re-send a verification e-mail for an address the visitor typed on /verify.
|
||||
*
|
||||
* Deliberately reports success even when no matching unverified account exists:
|
||||
* a distinct failure would let anyone probe which addresses are registered. The
|
||||
* identical-privacy behaviour also applies to the e-mail templates, which are
|
||||
* only sent for real accounts. Rate limiting is the spam defence.
|
||||
*/
|
||||
export async function resendVerification(
|
||||
_prevState: ResendVerificationState,
|
||||
formData: FormData,
|
||||
): Promise<ResendVerificationState> {
|
||||
const email = String(formData.get("email") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!EMAIL_RE.test(email)) {
|
||||
return { ok: false, error: "invalid" };
|
||||
}
|
||||
|
||||
const ip = await clientIp();
|
||||
if (!(await rateLimit(`verify:resend:${ip}`, 3, 10 * 60_000)).ok) {
|
||||
return { ok: false, error: "rateLimited" };
|
||||
}
|
||||
|
||||
try {
|
||||
const [user] = await db
|
||||
.select({ id: User.id, mailVerified: User.mailVerified })
|
||||
.from(User)
|
||||
.where(eq(User.mail, email))
|
||||
.limit(1);
|
||||
if (user && user.mailVerified !== "1") {
|
||||
await sendVerification(email);
|
||||
}
|
||||
} catch {
|
||||
return { ok: false, error: "unavailable" };
|
||||
}
|
||||
|
||||
return { ok: true, error: null };
|
||||
}
|
||||
Reference in new issue
Block a user