110 lines
2.8 KiB
TypeScript
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
|
|
};
|