import { redirect } from "@remix-run/node"; import { prisma } from "./db.server"; import { verifyPassword, hashPassword } from "./auth.server"; import type { SignInFormData, SignUpFormData, AuthResult, AuthLevel, SafeUser, RouteProtectionOptions } from "~/types/auth"; import { AUTH_LEVELS, USER_STATUS } from "~/types/auth"; import { AUTH_ERRORS, AUTH_CONFIG, VALIDATION_PATTERNS } from "./auth-constants"; // Authentication validation functions export async function validateSignIn(formData: SignInFormData): Promise { const { usernameOrEmail, password } = formData; // Find user by username or email const user = await prisma.user.findFirst({ where: { OR: [ { username: usernameOrEmail }, { email: usernameOrEmail }, ], }, }); if (!user) { return { success: false, errors: [{ message: AUTH_ERRORS.INVALID_CREDENTIALS }], }; } // Check if user is active if (user.status !== USER_STATUS.ACTIVE) { return { success: false, errors: [{ message: AUTH_ERRORS.ACCOUNT_INACTIVE }], }; } // Verify password const isValidPassword = await verifyPassword(password, user.password); if (!isValidPassword) { return { success: false, errors: [{ message: AUTH_ERRORS.INVALID_CREDENTIALS }], }; } // Return success with safe user data const { password: _, ...safeUser } = user; return { success: true, user: safeUser, }; } export async function validateSignUp(formData: SignUpFormData): Promise { const { name, username, email, password, confirmPassword } = formData; const errors: { field?: string; message: string }[] = []; // Validate required fields if (!name.trim()) { errors.push({ field: "name", message: AUTH_ERRORS.NAME_REQUIRED }); } if (!username.trim()) { errors.push({ field: "username", message: AUTH_ERRORS.USERNAME_REQUIRED }); } if (!email.trim()) { errors.push({ field: "email", message: AUTH_ERRORS.EMAIL_REQUIRED }); } if (!password) { errors.push({ field: "password", message: AUTH_ERRORS.PASSWORD_REQUIRED }); } if (password !== confirmPassword) { errors.push({ field: "confirmPassword", message: AUTH_ERRORS.PASSWORD_MISMATCH }); } // Validate password strength if (password && password.length < AUTH_CONFIG.MIN_PASSWORD_LENGTH) { errors.push({ field: "password", message: AUTH_ERRORS.PASSWORD_TOO_SHORT }); } // Validate email format if (email && !VALIDATION_PATTERNS.EMAIL.test(email)) { errors.push({ field: "email", message: AUTH_ERRORS.INVALID_EMAIL }); } // Check for existing username if (username) { const existingUsername = await prisma.user.findUnique({ where: { username }, }); if (existingUsername) { errors.push({ field: "username", message: AUTH_ERRORS.USERNAME_EXISTS }); } } // Check for existing email if (email) { const existingEmail = await prisma.user.findUnique({ where: { email }, }); if (existingEmail) { errors.push({ field: "email", message: AUTH_ERRORS.EMAIL_EXISTS }); } } if (errors.length > 0) { return { success: false, errors, }; } return { success: true }; } // User creation function export async function createUser(formData: SignUpFormData): Promise { const { name, username, email, password } = formData; const hashedPassword = await hashPassword(password); const user = await prisma.user.create({ data: { name: name.trim(), username: username.trim(), email: email.trim(), password: hashedPassword, status: USER_STATUS.ACTIVE, authLevel: AUTH_LEVELS.ADMIN, // First user becomes admin createdDate: new Date(), editDate: new Date(), }, }); const { password: _, ...safeUser } = user; return safeUser; } // Authorization helper functions export function hasPermission(userAuthLevel: AuthLevel, requiredAuthLevel: AuthLevel): boolean { return userAuthLevel <= requiredAuthLevel; } export function canAccessUserManagement(userAuthLevel: AuthLevel): boolean { return userAuthLevel <= AUTH_LEVELS.ADMIN; } export function canViewAllUsers(userAuthLevel: AuthLevel): boolean { return userAuthLevel === AUTH_LEVELS.SUPERADMIN; } export function canCreateUsers(userAuthLevel: AuthLevel): boolean { return userAuthLevel <= AUTH_LEVELS.ADMIN; } // Route protection middleware export async function requireAuthLevel( request: Request, requiredAuthLevel: AuthLevel, options: RouteProtectionOptions = {} ) { const { allowInactive = false, redirectTo = "/signin" } = options; // Get user from session const userId = await getUserId(request); if (!userId) { throw redirect(redirectTo); } const user = await prisma.user.findUnique({ where: { id: userId }, }); if (!user) { throw redirect(redirectTo); } // Check if user is active (unless explicitly allowed) if (!allowInactive && user.status !== USER_STATUS.ACTIVE) { throw redirect("/signin?error=account_inactive"); } // Check authorization level if (!hasPermission(user.authLevel as AuthLevel, requiredAuthLevel)) { throw redirect("/dashboard?error=insufficient_permissions"); } const { password: _, ...safeUser } = user; return safeUser; } // Check if signup should be allowed (only when no admin users exist) export async function isSignupAllowed(): Promise { const adminCount = await prisma.user.count({ where: { authLevel: { in: [AUTH_LEVELS.SUPERADMIN, AUTH_LEVELS.ADMIN], }, status: USER_STATUS.ACTIVE, }, }); return adminCount === 0; } // Import getUserId function async function getUserId(request: Request): Promise { const { getUserId: getSessionUserId } = await import("./auth.server"); return getSessionUserId(request); }