car_mms/app/lib/auth-helpers.server.ts
2025-09-11 14:22:27 +03:00

220 lines
5.8 KiB
TypeScript

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<AuthResult> {
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<AuthResult> {
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<SafeUser> {
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<boolean> {
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<number | null> {
const { getUserId: getSessionUserId } = await import("./auth.server");
return getSessionUserId(request);
}