Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/db/registration.db.server.ts
T

114 lines
4.7 KiB
TypeScript

import {
createRegistrationCapability,
findRegistrationBillingPreferenceCapability,
findRegistrationByTokenCapability,
findRegistrationSummaryByEmail,
rotateRegistrationVerificationCapability,
updateRegistrationCapability,
} from "@hivegrid/db/pretenant-security.server";
import { deriveVerificationCreationRequestKey } from "~/domain/tenant-bootstrap/signupCreationRequestKey.server";
export const REGISTRATION_VERIFICATION_TTL_MS = 24 * 60 * 60 * 1000;
const REGISTRATION_VERIFICATION_TOKEN_PATTERN = /^[a-f0-9]{40}$/iu;
type RegistrationRecord = NonNullable<Awaited<ReturnType<typeof findRegistrationByTokenCapability>>>;
export type RegistrationVerificationState =
| { status: "invalid" }
| { status: "expired"; registration: RegistrationRecord }
| { status: "used"; registration: RegistrationRecord }
| { status: "pending"; registration: RegistrationRecord };
export class RegistrationVerificationRotationConflictError extends Error {
constructor() {
super("Registration verification rotation capability was rejected");
this.name = "RegistrationVerificationRotationConflictError";
}
}
export async function getRegistrationByEmail(email: string) {
return await findRegistrationSummaryByEmail(email);
}
export async function getRegistrationBillingPreference(tenantId: string, userId: string) {
return await findRegistrationBillingPreferenceCapability(tenantId, userId);
}
export async function getRegistrationByToken(token: string, now = new Date()) {
const state = await getRegistrationVerificationStateByToken(token, now);
return state.status === "pending" || state.status === "used" ? state.registration : null;
}
export async function getRegistrationVerificationStateByToken(token: string, now = new Date()): Promise<RegistrationVerificationState> {
if (!REGISTRATION_VERIFICATION_TOKEN_PATTERN.test(token)) return { status: "invalid" };
const registration = await findRegistrationByTokenCapability(token);
if (!registration) return { status: "invalid" };
if (registration.createdTenantId) return { status: "used", registration };
if (!isRegistrationVerificationFresh(registration.createdAt, now)) return { status: "expired", registration };
return { status: "pending", registration };
}
/** Minimal projection for the public verification page. */
export async function getRegistrationVerificationViewByToken(token: string, now = new Date()) {
const state = await getRegistrationVerificationStateByToken(token, now);
if (state.status !== "pending") return { status: state.status } as const;
return {
status: "pending" as const,
email: state.registration.email,
creationRequestKey: deriveVerificationCreationRequestKey(state.registration.id),
};
}
export function isRegistrationVerificationFresh(createdAt: Date | string, now = new Date()): boolean {
const createdAtMs = new Date(createdAt).getTime();
const nowMs = now.getTime();
return Number.isFinite(createdAtMs) && nowMs >= createdAtMs && nowMs - createdAtMs < REGISTRATION_VERIFICATION_TTL_MS;
}
export async function createRegistration(data: {
email: string;
firstName: string;
lastName: string;
token: string;
company: string | null;
selectedSubscriptionPriceId: string | null;
selectedOfficeSeatQuantity: number | null;
selectedFieldSeatQuantity: number | null;
leadConversionLeadId?: string | null;
leadConversionToken?: string | null;
leadConversionPlanId?: string | null;
ipAddress: string | null;
slug: string | null;
}) {
return await createRegistrationCapability({
email: data.email,
firstName: data.firstName,
lastName: data.lastName,
token: data.token,
company: data.company,
selectedSubscriptionPriceId: data.selectedSubscriptionPriceId,
selectedOfficeSeatQuantity: data.selectedOfficeSeatQuantity,
selectedFieldSeatQuantity: data.selectedFieldSeatQuantity,
leadConversionLeadId: data.leadConversionLeadId ?? null,
leadConversionToken: data.leadConversionToken ?? null,
leadConversionPlanId: data.leadConversionPlanId ?? null,
ipAddress: data.ipAddress,
slug: data.slug,
});
}
export async function rotateRegistrationVerification(id: string, expectedToken: string, token: string) {
const updated = await rotateRegistrationVerificationCapability({ id, expectedToken, token });
if (!updated) {
throw new RegistrationVerificationRotationConflictError();
}
return true;
}
export async function updateRegistration(id: string, capability: { expectedToken: string }, data: { createdTenantId: string }) {
const updated = await updateRegistrationCapability({ id, expectedToken: capability.expectedToken, patch: data });
if (!updated) {
throw new Error("Registration update capability was rejected");
}
return updated;
}