AutoTechJobs implements a secure authentication system that manages user registration, login, and session management. The system supports multiple user roles with different permissions and access levels.
New users can register through the signup form, which collects essential information and creates a user account:
// In routes/signup.tsx
import type { ActionFunctionArgs } from '@remix-run/cloudflare';
import { json, redirect } from '@remix-run/cloudflare';
import { Form, useActionData } from '@remix-run/react';
export const action = async ({ request, context }: ActionFunctionArgs) => {
const formData = await request.formData();
const email = formData.get('email')?.toString();
const password = formData.get('password')?.toString();
const roleId = formData.get('roleId')?.toString();
// Validation
const fieldErrors = {
email: !email ? 'Email is required' : null,
password: !password ? 'Password is required' :
password.length < 8 ? 'Password must be at least 8 characters' : null,
roleId: !roleId ? 'Role is required' : null,
};
const hasErrors = Object.values(fieldErrors).some(error => error !== null);
if (hasErrors) {
return json({ success: false, fieldErrors }, { status: 400 });
}
try {
// Create user account
const authService = new AuthService(context.cloudflare.env.DB);
await authService.registerUser({
email,
password,
roleId: parseInt(roleId),
});
// Redirect to login
return redirect('/login');
} catch (error) {
return json({
success: false,
error: error instanceof Error ? error.message : 'An error occurred during registration'
}, { status: 500 });
}
};The login process authenticates users and creates a session:
// In routes/login.tsx
export const action = async ({ request, context }: ActionFunctionArgs) => {
const formData = await request.formData();
const email = formData.get('email')?.toString();
const password = formData.get('password')?.toString();
// Validation
const fieldErrors = {
email: !email ? 'Email is required' : null,
password: !password ? 'Password is required' : null,
};
const hasErrors = Object.values(fieldErrors).some(error => error !== null);
if (hasErrors) {
return json({ success: false, fieldErrors }, { status: 400 });
}
try {
// Authenticate user
const authService = new AuthService(context.cloudflare.env.DB);
const user = await authService.validateCredentials(email, password);
// Create session
const sessionStorage = createSessionStorage(context.cloudflare.env);
const session = await sessionStorage.getSession(request.headers.get('Cookie'));
session.set('user', { id: user.id.toString(), email: user.email, roleId: user.roleId });
// Redirect based on role
const redirectTo = user.roleId === 1 ? '/admin/dashboard' :
user.roleId === 2 ? '/employers/dashboard' :
'/candidates/dashboard';
return redirect(redirectTo, {
headers: {
'Set-Cookie': await sessionStorage.commitSession(session),
},
});
} catch (error) {
return json({
success: false,
error: 'Invalid email or password'
}, { status: 401 });
}
};Sessions are managed using Cloudflare KV for storage:
// In lib/session.ts
import { createCookieSessionStorage } from '@remix-run/cloudflare';
export function createSessionStorage(env: Env) {
return createCookieSessionStorage({
cookie: {
name: 'autotechjobs_session',
httpOnly: true,
path: '/',
sameSite: 'lax',
secrets: [env.SESSION_SECRET],
secure: process.env.NODE_ENV === 'production',
},
});
}
export async function getUserFromSession(request: Request, env: Env) {
const sessionStorage = createSessionStorage(env);
const session = await sessionStorage.getSession(request.headers.get('Cookie'));
const userId = session.get('user')?.id;
if (!userId) {
return null;
}
// Get user from database
const userRepository = new UserRepository(env.DB);
return userRepository.findById(userId);
}Passwords are securely hashed using bcrypt before storage:
// In lib/utils/auth.ts
import * as bcrypt from 'bcryptjs';
export async function hashPassword(password: string): Promise<string> {
const salt = await bcrypt.genSalt(10);
return bcrypt.hash(password, salt);
}
export async function comparePasswords(password: string, hashedPassword: string): Promise<boolean> {
return bcrypt.compare(password, hashedPassword);
}The application implements role-based access control to restrict access to certain routes:
// In routes/employers.dashboard.tsx
export const loader = async ({ request, context }: LoaderFunctionArgs) => {
const user = await getUserFromSession(request, context.cloudflare.env);
// Check if user is authenticated
if (!user) {
return redirect('/login');
}
// Check if user has employer role
if (user.roleId !== 2) {
return redirect('/unauthorized');
}
// Load employer-specific data
const employerRepository = new EmployerRepository(context.cloudflare.env.DB);
const profile = await employerRepository.findByUserId(user.id);
return json({ user, profile });
};The AuthService class encapsulates authentication logic:
// In lib/services/auth.service.ts
export class AuthService {
constructor(
private userRepository: UserRepository,
) {}
async registerUser(userData: UserRegistrationData): Promise<User> {
// Check if email already exists
const existingUser = await this.userRepository.findByEmail(userData.email);
if (existingUser) {
throw new Error('Email already in use');
}
// Hash password
const hashedPassword = await hashPassword(userData.password);
// Create user
return this.userRepository.create({
email: userData.email,
passwordHash: hashedPassword,
roleId: userData.roleId,
});
}
async validateCredentials(email: string, password: string): Promise<User> {
const user = await this.userRepository.findByEmail(email);
if (!user) {
throw new Error('Invalid credentials');
}
const passwordHash = await this.userRepository.getPasswordHash(user.id);
const isValid = await comparePasswords(password, passwordHash);
if (!isValid) {
throw new Error('Invalid credentials');
}
return user;
}
}