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.

352 lines (309 loc) 9.71 kB
import { NextApiResponse } from 'next'; import { prisma } from '../../db'; import { AuthenticatedRequest, AuthMiddleware } from './index'; interface AccountLockout { id: string; userId: string; lockedUntil: Date | null; reason: string | null; } interface PrismaExtended { accountLockout: { findUnique: (args: unknown) => Promise<AccountLockout | null>; delete: (args: unknown) => Promise<AccountLockout>; upsert: (args: unknown) => Promise<AccountLockout>; }; loginAttempt: { create: (args: unknown) => Promise<unknown>; count: (args: unknown) => Promise<number>; }; securitySettings: { findUnique: (args: unknown) => Promise<SecuritySettings | null>; }; waitlist: { findFirst: (args: unknown) => Promise<unknown>; }; emailAllowlist: { count: (args: unknown) => Promise<number>; findFirst: (args: unknown) => Promise<unknown>; }; emailBlocklist: { findFirst: (args: unknown) => Promise<unknown>; }; } interface SecuritySettings { enableLockoutPolicy?: boolean; maxLoginAttempts?: number; lockoutDuration?: number; signUpMode?: string; blockDisposableEmails?: boolean; blockEmailSubaddresses?: boolean; enableEnumerationProtection?: boolean; tenantId: string; } // Check if account is locked export const checkAccountLockout: AuthMiddleware = async (req: AuthenticatedRequest, res: NextApiResponse, next) => { try { const email = req.body?.email; if (!email) { return next(); } const user = await prisma.user.findUnique({ where: { email }, }); if (!user) { return next(); } const lockout = await (prisma as unknown as PrismaExtended).accountLockout.findUnique({ where: { userId: user.id }, }); if (!lockout) { return next(); } // Check if lockout has expired if (lockout.lockedUntil && new Date(lockout.lockedUntil) < new Date()) { // Unlock account await (prisma as unknown as PrismaExtended).accountLockout.delete({ where: { id: lockout.id } }); return next(); } // Account is still locked const lockoutMessage = lockout.lockedUntil ? `Account locked until ${new Date(lockout.lockedUntil).toLocaleString()}` : 'Account locked indefinitely. Contact support to unlock.'; return res.status(423).json({ success: false, error: { code: 'ACCOUNT_LOCKED', message: lockoutMessage, lockedUntil: lockout.lockedUntil, }, }); } catch (error) { console.error('Account lockout check error:', error); return next(); } }; // Track failed login attempts and trigger lockout export const trackLoginAttempt = async ( email: string, success: boolean, ipAddress: string, userAgent: string | undefined, tenantId: string | null, failureReason?: string ) => { try { // Log the attempt await (prisma as unknown as PrismaExtended).loginAttempt.create({ data: { email, ipAddress, userAgent, success, failureReason, tenantId, }, }); if (success) { return; } // Get security settings for tenant const securitySettings = tenantId ? await (prisma as unknown as PrismaExtended).securitySettings.findUnique({ where: { tenantId } }) : null; if (!securitySettings?.enableLockoutPolicy) { return; } // Count recent failed attempts (last 24 hours) const recentAttempts = await (prisma as unknown as PrismaExtended).loginAttempt.count({ where: { email, success: false, createdAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000), }, }, }); const maxAttempts = securitySettings.maxLoginAttempts || 100; if (recentAttempts >= maxAttempts) { const user = await prisma.user.findUnique({ where: { email } }); if (user) { const lockedUntil = securitySettings.lockoutDuration ? new Date(Date.now() + securitySettings.lockoutDuration * 60 * 1000) : null; // Create or update lockout await (prisma as unknown as PrismaExtended).accountLockout.upsert({ where: { userId: user.id }, create: { userId: user.id, lockedUntil, reason: `Too many failed login attempts (${recentAttempts})`, }, update: { lockedAt: new Date(), lockedUntil, reason: `Too many failed login attempts (${recentAttempts})`, }, }); } } } catch (error) { console.error('Track login attempt error:', error); } }; // Check sign-up restrictions export const checkSignUpRestrictions: AuthMiddleware = async (req: AuthenticatedRequest, res: NextApiResponse, next) => { try { const email = req.body?.email; const tenantId = req.tenant?.id || req.body?.tenantId; if (!email || !tenantId) { return next(); } const securitySettings = await (prisma as unknown as PrismaExtended).securitySettings.findUnique({ where: { tenantId }, }); if (!securitySettings) { return next(); } // Check sign-up mode if (securitySettings.signUpMode === 'RESTRICTED') { return res.status(403).json({ success: false, error: { code: 'SIGNUPS_RESTRICTED', message: 'Sign-ups are restricted. You must be invited or manually created.', }, }); } if (securitySettings.signUpMode === 'WAITLIST') { // Check if user is on waitlist and approved const waitlistEntry = await (prisma as unknown as PrismaExtended).waitlist.findFirst({ where: { email, status: 'APPROVED', }, }); if (!waitlistEntry) { return res.status(403).json({ success: false, error: { code: 'WAITLIST_REQUIRED', message: 'You must be on the approved waitlist to sign up.', }, }); } } // Check disposable email if (securitySettings.blockDisposableEmails && isDisposableEmail(email)) { return res.status(400).json({ success: false, error: { code: 'DISPOSABLE_EMAIL_BLOCKED', message: 'Disposable email addresses are not allowed.', }, }); } // Check email subaddresses (e.g., user+tag@domain.com) if (securitySettings.blockEmailSubaddresses && email.includes('+')) { return res.status(400).json({ success: false, error: { code: 'EMAIL_SUBADDRESS_BLOCKED', message: 'Email subaddresses are not allowed.', }, }); } // Check allowlist const allowlistCount = await (prisma as unknown as PrismaExtended).emailAllowlist.count({ where: { tenantId }, }); if (allowlistCount > 0) { const domain = email.split('@')[1]; const allowlistMatch = await (prisma as unknown as PrismaExtended).emailAllowlist.findFirst({ where: { tenantId, OR: [ { email }, { email: `@${domain}` }, ], }, }); if (!allowlistMatch) { return res.status(403).json({ success: false, error: { code: 'EMAIL_NOT_ALLOWED', message: 'This email domain is not allowed to sign up.', }, }); } } // Check blocklist const domain = email.split('@')[1]; const blocklistMatch = await (prisma as unknown as PrismaExtended).emailBlocklist.findFirst({ where: { tenantId, OR: [ { email }, { email: `@${domain}` }, ], }, }); if (blocklistMatch) { return res.status(403).json({ success: false, error: { code: 'EMAIL_BLOCKED', message: 'This email or domain has been blocked.', }, }); } next(); } catch (error) { console.error('Sign-up restrictions check error:', error); next(); } }; // Simple disposable email check (can be extended with external service) function isDisposableEmail(email: string): boolean { const disposableDomains = [ 'tempmail.com', 'throwaway.email', 'guerrillamail.com', 'mailinator.com', '10minutemail.com', 'trashmail.com', 'yopmail.com', 'fakeinbox.com', 'temp-mail.org', 'getnada.com', ]; const domain = email.split('@')[1]?.toLowerCase(); return disposableDomains.includes(domain); } // User enumeration protection - return generic error messages export const protectUserEnumeration = ( securitySettings: SecuritySettings | null, error: 'USER_NOT_FOUND' | 'INVALID_PASSWORD' | 'ACCOUNT_NOT_VERIFIED' ): string => { if (securitySettings?.enableEnumerationProtection) { return 'Invalid email or password.'; } switch (error) { case 'USER_NOT_FOUND': return 'No account found with this email address.'; case 'INVALID_PASSWORD': return 'Incorrect password.'; case 'ACCOUNT_NOT_VERIFIED': return 'Please verify your email before logging in.'; default: return 'Authentication failed.'; } }; const securityCheckExports = { checkAccountLockout, trackLoginAttempt, checkSignUpRestrictions, protectUserEnumeration, }; export default securityCheckExports;