uup
This commit is contained in:
220
app/lib/auth-helpers.server.ts
Normal file
220
app/lib/auth-helpers.server.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user