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.

120 lines (99 loc) 4 kB
import { prisma } from './db' import crypto from 'crypto' import dns from 'dns/promises' import { Organization } from '@prisma/client' const DOMAIN_REGEX = /^(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.(?!-)[A-Za-z0-9-]{1,63}(?<!-))*$/ export function normalizeDomain(domain: string) { return domain.toLowerCase().trim() } export function isValidDomain(domain: string) { if (!domain) return false if (domain.includes('://') || domain.includes('/')) return false if (!DOMAIN_REGEX.test(domain)) return false return domain.includes('.') } export async function generateDomainVerificationToken(organizationId: string, domain: string) { // Normalize domain const normalized = normalizeDomain(domain) if (!isValidDomain(normalized)) { throw new Error('Invalid domain') } // Check if domain is already taken by another org // using bracket access to avoid type issues until prisma client is regenerated const existing = await prisma.organizationDomain.findUnique({ where: { domain: normalized } }) if (existing && existing.organizationId !== organizationId) { throw new Error('Domain already claimed') } // Generate token const token = crypto.randomBytes(16).toString('hex') // Upsert the domain row const row = await prisma.organizationDomain.upsert({ where: { domain: normalized }, update: { verificationToken: token, verified: false }, create: { organizationId, domain: normalized, verificationToken: token, verified: false } }) return { token, domain: row.domain } } export async function verifyDomainByDns(organizationId: string, domain: string) { const normalized = normalizeDomain(domain) if (!isValidDomain(normalized)) { throw new Error('Invalid domain') } const row = await prisma.organizationDomain.findUnique({ where: { domain: normalized } }) if (!row) { throw new Error('Domain not requested') } if (row.organizationId !== organizationId) { throw new Error('Domain owned by another organization') } if (!row.verificationToken) { throw new Error('No verification token found') } try { const records = await dns.resolveTxt(normalized) const flat = records.flat().join(' ') if (flat.includes(row.verificationToken)) { const updated = await prisma.organizationDomain.update({ where: { domain: normalized }, data: { verified: true, verifiedAt: new Date() } }) return { verified: true, domain: updated.domain } } return { verified: false } } catch { return { verified: false, error: 'Failed to resolve DNS records' } } } export async function resolveOrgByHost(host: string) { // Strip port const hostname = host.split(':')[0].toLowerCase() // 1) exact match in OrganizationDomain const domainRow = await prisma.organizationDomain.findUnique({ where: { domain: hostname }, include: { organization: true } }) if (domainRow && domainRow.verified) return domainRow.organization // 2) fallback: check Organization.customDomain (legacy) — for backward compatibility // Use a raw query to avoid TypeScript errors if the legacy field doesn't exist try { const legacyRows = await prisma.$queryRaw<Organization[]>` SELECT * FROM organizations WHERE customDomain = ${hostname} LIMIT 1 ` if (legacyRows.length > 0) { return legacyRows[0] } } catch { // legacy field may not exist; ignore } // 3) fallback: subdomain -> slug mapping (e.g., orgslug.yourapp.com) // Try to split hostname and probe first label const parts = hostname.split('.') if (parts.length > 2) { const slug = parts[0] const bySlug = await prisma.organization.findFirst({ where: { slug } }) if (bySlug) return bySlug } return null } const customDomain = { generateDomainVerificationToken, verifyDomainByDns, resolveOrgByHost, normalizeDomain, isValidDomain, } export default customDomain