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
text/typescript
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;