Files
lib-auth/server/invites.ts
T

110 lines
2.8 KiB
TypeScript

import { createHash, randomBytes } from "node:crypto";
type AccountInviteContext = {
verificationToken: { identifier: string; expires: Date };
user: {
id: string;
email: string | null;
name: string | null;
passwordHash: string | null;
emailVerified: Date | null;
accounts: Array<{ id: string }>;
};
};
type AccountInviteOptions = {
prisma: any;
tokenTtlMs?: number;
identifierPrefix?: string;
};
const DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX = "account-invite:";
const DEFAULT_ACCOUNT_INVITE_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
function hashAccountInviteToken(token: string): string {
return createHash("sha256").update(token).digest("hex");
}
function buildAccountInviteIdentifier(prefix: string, userId: string): string {
return `${prefix}${userId}`;
}
export async function createAccountInviteToken(
options: AccountInviteOptions & { userId: string }
): Promise<{ token: string; expiresAt: Date }> {
const identifierPrefix = options.identifierPrefix ?? DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX;
const identifier = buildAccountInviteIdentifier(identifierPrefix, options.userId);
const token = randomBytes(32).toString("hex");
const expiresAt = new Date(Date.now() + (options.tokenTtlMs ?? DEFAULT_ACCOUNT_INVITE_TOKEN_TTL_MS));
await options.prisma.verificationToken.deleteMany({
where: {
OR: [{ identifier }, { expires: { lt: new Date() } }]
}
});
await options.prisma.verificationToken.create({
data: {
identifier,
token: hashAccountInviteToken(token),
expires: expiresAt
}
});
return { token, expiresAt };
}
export async function getAccountInviteContext(
options: AccountInviteOptions & { token: string }
): Promise<AccountInviteContext | null> {
const identifierPrefix = options.identifierPrefix ?? DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX;
const verificationToken = await options.prisma.verificationToken.findUnique({
where: { token: hashAccountInviteToken(options.token) },
select: { identifier: true, expires: true }
});
if (
!verificationToken ||
verificationToken.expires <= new Date() ||
!verificationToken.identifier.startsWith(identifierPrefix)
) {
return null;
}
const userId = verificationToken.identifier.slice(identifierPrefix.length);
if (!userId) {
return null;
}
const user = await options.prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
name: true,
passwordHash: true,
emailVerified: true,
accounts: {
select: { id: true },
take: 1
}
}
});
if (!user?.email) {
return null;
}
return {
verificationToken,
user
};
}
export {
DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX,
DEFAULT_ACCOUNT_INVITE_TOKEN_TTL_MS,
buildAccountInviteIdentifier,
hashAccountInviteToken
};