220 lines
5.8 KiB
TypeScript
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);
|
|
} |