UNPKG

codalware-auth

Version:

Complete authentication system with enterprise security, attack protection, team workspaces, waitlist, billing, UI components, 2FA, and account recovery - production-ready in 5 minutes. Enhanced CLI with verification, rollback, and App Router scaffolding.

257 lines (224 loc) 8.04 kB
import { CheckoutStatus, Prisma } from '@prisma/client' import { prisma } from './db' export type CheckoutLineItemInput = { id?: string label: string description?: string amount: number quantity?: number imageUrl?: string metadata?: Record<string, unknown> } export type CreateCheckoutSessionInput = { amount: number currency: string organizationId?: string | null tenantId?: string | null customerEmail?: string | null customerName?: string | null successUrl?: string | null cancelUrl?: string | null createdBy?: string | null metadata?: Record<string, unknown> lineItems?: CheckoutLineItemInput[] provider: string paymentMethod?: string | null } export type ProviderCreateSessionInput = CreateCheckoutSessionInput & { sessionId: string } export type ProviderCreateSessionResult = { providerSessionId?: string | null paymentUrl?: string | null status?: CheckoutStatus expiresAt?: Date | null metadata?: Record<string, unknown> } export type ProviderRetrieveSessionResult = { status: CheckoutStatus providerSessionId?: string | null paymentUrl?: string | null raw?: unknown } export type ProviderCancelSessionResult = { status: CheckoutStatus canceledAt?: Date } export interface PaymentProviderAdapter { id: string label: string description?: string supportsRecurring?: boolean supportsOneTime?: boolean supportedCurrencies?: string[] createSession?: ( input: ProviderCreateSessionInput, context: { prisma: typeof prisma } ) => Promise<ProviderCreateSessionResult> retrieveSession?: ( session: { id: string; providerSessionId?: string | null }, context: { prisma: typeof prisma } ) => Promise<ProviderRetrieveSessionResult> cancelSession?: ( session: { id: string; providerSessionId?: string | null }, context: { prisma: typeof prisma } ) => Promise<ProviderCancelSessionResult> metadata?: Record<string, unknown> } const providerRegistry = new Map<string, PaymentProviderAdapter>() const manualProvider: PaymentProviderAdapter = { id: 'manual', label: 'Manual / Invoiced', description: 'Collect intent details now and complete payment manually or via emailed invoice.', supportsOneTime: true, supportsRecurring: false, createSession: async () => ({ status: CheckoutStatus.PENDING, }), } providerRegistry.set(manualProvider.id, manualProvider) export function registerPaymentProvider(provider: PaymentProviderAdapter) { if (!provider.id) { throw new Error('Payment provider must include an "id" field') } providerRegistry.set(provider.id, provider) } export function unregisterPaymentProvider(id: string) { providerRegistry.delete(id) } export function listPaymentProviders() { return Array.from(providerRegistry.values()).map(provider => ({ id: provider.id, label: provider.label, description: provider.description, supportsRecurring: provider.supportsRecurring ?? false, supportsOneTime: provider.supportsOneTime ?? true, supportedCurrencies: provider.supportedCurrencies, metadata: provider.metadata, })) } export async function createCheckoutSession(input: CreateCheckoutSessionInput) { const amount = Math.round(input.amount) if (!Number.isFinite(amount) || amount <= 0) { throw new Error('Amount must be a positive integer representing the smallest currency unit (e.g. cents)') } if (!input.currency) { throw new Error('Currency is required') } const provider = providerRegistry.get(input.provider) || manualProvider const session = await prisma.checkoutSession.create({ data: { amount, currency: input.currency.toLowerCase(), organizationId: input.organizationId || undefined, tenantId: input.tenantId || undefined, customerEmail: input.customerEmail || undefined, customerName: input.customerName || undefined, successUrl: input.successUrl || undefined, cancelUrl: input.cancelUrl || undefined, metadata: (input.metadata || undefined) as Prisma.JsonObject | undefined, lineItems: (input.lineItems || undefined) as Prisma.JsonArray | undefined, provider: provider.id, paymentMethod: input.paymentMethod || undefined, createdBy: input.createdBy || undefined, }, }) if (provider.createSession) { try { const providerResult = await provider.createSession({ ...input, amount, currency: input.currency.toLowerCase(), sessionId: session.id, }, { prisma }) if (providerResult) { const updated = await prisma.checkoutSession.update({ where: { id: session.id }, data: { providerSessionId: providerResult.providerSessionId || undefined, paymentUrl: providerResult.paymentUrl || undefined, status: providerResult.status || CheckoutStatus.PENDING, expiresAt: providerResult.expiresAt || undefined, metadata: mergeMetadata(session.metadata, providerResult.metadata), }, }) return updated } } catch (error) { await prisma.checkoutSession.update({ where: { id: session.id }, data: { status: CheckoutStatus.FAILED }, }) throw error } } return session } export async function retrieveCheckoutSession(sessionId: string) { const session = await prisma.checkoutSession.findUnique({ where: { id: sessionId } }) if (!session) { return null } const provider = session.provider ? providerRegistry.get(session.provider) : undefined if (provider?.retrieveSession && session.providerSessionId) { try { const upstream = await provider.retrieveSession({ id: session.id, providerSessionId: session.providerSessionId, }, { prisma }) if (upstream && upstream.status !== session.status) { return prisma.checkoutSession.update({ where: { id: session.id }, data: { status: upstream.status, paymentUrl: upstream.paymentUrl || session.paymentUrl || undefined, }, }) } } catch (error) { console.warn('Failed to synchronize checkout session', session.id, error) } } return session } export async function updateCheckoutStatus(sessionId: string, status: CheckoutStatus, data?: { providerSessionId?: string | null paymentUrl?: string | null metadata?: Record<string, unknown> completedAt?: Date | null }) { return prisma.checkoutSession.update({ where: { id: sessionId }, data: { status, providerSessionId: data?.providerSessionId || undefined, paymentUrl: data?.paymentUrl || undefined, metadata: mergeMetadata(undefined, data?.metadata), completedAt: data?.completedAt || undefined, }, }) } export async function cancelCheckoutSession(sessionId: string) { const session = await prisma.checkoutSession.findUnique({ where: { id: sessionId } }) if (!session) { throw new Error('Session not found') } const provider = session.provider ? providerRegistry.get(session.provider) : undefined if (provider?.cancelSession) { await provider.cancelSession({ id: session.id, providerSessionId: session.providerSessionId, }, { prisma }) } return prisma.checkoutSession.update({ where: { id: sessionId }, data: { status: CheckoutStatus.CANCELED }, }) } function mergeMetadata(existing: Prisma.JsonValue | null | undefined, incoming?: Record<string, unknown>) { if (!incoming || Object.keys(incoming).length === 0) { return existing as Prisma.InputJsonValue | undefined } const base = (existing && typeof existing === 'object') ? existing : {} return { ...(base as Record<string, unknown>), ...incoming } as Prisma.InputJsonValue }