40 lines
1.2 KiB
TypeScript
40 lines
1.2 KiB
TypeScript
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
|
|
export const INVITE_VALID_DAYS = 7;
|
|
|
|
export function createInviteToken(): string {
|
|
return randomBytes(32).toString("base64url");
|
|
}
|
|
|
|
export function hashInviteToken(raw: string): string {
|
|
return createHash("sha256").update(raw, "utf8").digest("hex");
|
|
}
|
|
|
|
export function tokensEqual(a: string, b: string): boolean {
|
|
const ba = Buffer.from(a, "utf8");
|
|
const bb = Buffer.from(b, "utf8");
|
|
if (ba.length !== bb.length) return false;
|
|
return timingSafeEqual(ba, bb);
|
|
}
|
|
|
|
export type InviteLike = { tokenHash: string; expiresAt: Date; acceptedAt: Date | null };
|
|
|
|
/** Prüft Einladung anhand des übergebenen Roh-Tokens und Ablaufdatum. */
|
|
export function isInviteValid(
|
|
invite: InviteLike | null,
|
|
rawToken: string,
|
|
now: Date = new Date()
|
|
): boolean {
|
|
if (!invite || !rawToken) return false;
|
|
if (invite.acceptedAt !== null) return false;
|
|
if (invite.expiresAt.getTime() <= now.getTime()) return false;
|
|
return tokensEqual(invite.tokenHash, hashInviteToken(rawToken));
|
|
}
|
|
|
|
export function normalizeEmail(email: string | null | undefined): string | null {
|
|
if (!email) return null;
|
|
const e = email.trim().toLowerCase();
|
|
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(e)) return null;
|
|
return e;
|
|
}
|