Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions lib/auth/service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { AuthService } from './service';

describe('AuthService', () => {
beforeEach(() => {
// Reset any potential global state between tests
});

describe('User Registration', () => {
it('should register a new user', async () => {
const result = await AuthService.register(
'[email protected]',
'validpassword'
);

expect(result.user.email).toBe('[email protected]');
expect(result.token).toBeTruthy();
});

it('should prevent duplicate user registration', async () => {
await AuthService.register(
'[email protected]',
'validpassword'
);

await expect(
AuthService.register(
'[email protected]',
'anotherpassword'
)
).rejects.toThrow('User already exists');
});

it('should fail registration with invalid email', async () => {
await expect(
AuthService.register(
'invalidemail',
'validpassword'
)
).rejects.toThrow('Invalid email format');
});
});

describe('User Login', () => {
it('should login with correct credentials', async () => {
// First register a user
await AuthService.register(
'[email protected]',
'validpassword'
);

// Then login
const result = await AuthService.login({
email: '[email protected]',
password: 'validpassword'
});

expect(result.user.email).toBe('[email protected]');
expect(result.token).toBeTruthy();
});

it('should fail login with incorrect password', async () => {
// First register a user
await AuthService.register(
'[email protected]',
'validpassword'
);

// Then attempt login with wrong password
await expect(
AuthService.login({
email: '[email protected]',
password: 'wrongpassword'
})
).rejects.toThrow('Invalid email or password');
});
});
});
80 changes: 80 additions & 0 deletions lib/auth/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { v4 as uuidv4 } from 'uuid';
import { User, LoginCredentials, AuthResponse } from './types';
import { AuthUtils } from './utils';

// Mock in-memory user store (replace with database in production)
const USERS: User[] = [];

export class AuthService {
// User registration
static async register(
email: string,
password: string
): Promise<AuthResponse> {
// Validate credentials
const validationErrors = AuthUtils.validateLoginCredentials({ email, password });
if (validationErrors.length > 0) {
throw new Error(validationErrors.join(', '));
}

// Check if user already exists
const existingUser = USERS.find(u => u.email === email);
if (existingUser) {
throw new Error('User already exists');
}

// Hash password
const hashedPassword = await AuthUtils.hashPassword(password);

// Create new user
const newUser: User = {
id: uuidv4(),
email,
password: hashedPassword,
createdAt: new Date()
};

USERS.push(newUser);

// Generate token and return response
const { password: _, ...userWithoutPassword } = newUser;
return {
token: AuthUtils.generateToken(userWithoutPassword),
user: userWithoutPassword
};
}

// User login
static async login(
credentials: LoginCredentials
): Promise<AuthResponse> {
// Validate credentials
const validationErrors = AuthUtils.validateLoginCredentials(credentials);
if (validationErrors.length > 0) {
throw new Error(validationErrors.join(', '));
}

// Find user
const user = USERS.find(u => u.email === credentials.email);
if (!user) {
throw new Error('Invalid email or password');
}

// Verify password
const isPasswordValid = await AuthUtils.comparePassword(
credentials.password,
user.password
);

if (!isPasswordValid) {
throw new Error('Invalid email or password');
}

// Generate token and return response
const { password: _, ...userWithoutPassword } = user;
return {
token: AuthUtils.generateToken(userWithoutPassword),
user: userWithoutPassword
};
}
}
16 changes: 16 additions & 0 deletions lib/auth/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface User {
id: string;
email: string;
password: string;
createdAt: Date;
}

export interface LoginCredentials {
email: string;
password: string;
}

export interface AuthResponse {
token: string;
user: Omit<User, 'password'>;
}
62 changes: 62 additions & 0 deletions lib/auth/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, it, expect } from 'vitest';
import { AuthUtils } from './utils';

describe('AuthUtils', () => {
describe('Password Hashing', () => {
it('should hash a password', async () => {
const password = 'testpassword';
const hashedPassword = await AuthUtils.hashPassword(password);

expect(hashedPassword).not.toBe(password);
expect(hashedPassword.length).toBeGreaterThan(0);
});

it('should compare correct password', async () => {
const password = 'testpassword';
const hashedPassword = await AuthUtils.hashPassword(password);

const result = await AuthUtils.comparePassword(password, hashedPassword);
expect(result).toBe(true);
});

it('should fail when comparing incorrect password', async () => {
const password = 'testpassword';
const hashedPassword = await AuthUtils.hashPassword(password);

const result = await AuthUtils.comparePassword('wrongpassword', hashedPassword);
expect(result).toBe(false);
});
});

describe('Login Credential Validation', () => {
it('should validate correct credentials', () => {
const credentials = {
email: '[email protected]',
password: 'validpassword'
};

const errors = AuthUtils.validateLoginCredentials(credentials);
expect(errors.length).toBe(0);
});

it('should fail with invalid email', () => {
const credentials = {
email: 'invalidemail',
password: 'validpassword'
};

const errors = AuthUtils.validateLoginCredentials(credentials);
expect(errors).toContain('Invalid email format');
});

it('should fail with short password', () => {
const credentials = {
email: '[email protected]',
password: '12345'
};

const errors = AuthUtils.validateLoginCredentials(credentials);
expect(errors).toContain('Password must be at least 6 characters long');
});
});
});
65 changes: 65 additions & 0 deletions lib/auth/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { User, LoginCredentials, AuthResponse } from './types';

const JWT_SECRET = process.env.JWT_SECRET || 'fallback-secret-key-for-development';
const SALT_ROUNDS = 10;

export class AuthUtils {
// Hash password using bcrypt
static async hashPassword(password: string): Promise<string> {
return await bcrypt.hash(password, SALT_ROUNDS);
}

// Compare provided password with stored hash
static async comparePassword(
providedPassword: string,
storedHash: string
): Promise<boolean> {
return await bcrypt.compare(providedPassword, storedHash);
}

// Generate JWT token
static generateToken(user: Omit<User, 'password'>): string {
return jwt.sign(
{
id: user.id,
email: user.email
},
JWT_SECRET,
{ expiresIn: '7d' }
);
}

// Validate JWT token
static verifyToken(token: string): any {
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
return null;
}
}

// Validate login credentials
static validateLoginCredentials(
credentials: LoginCredentials
): string[] {
const errors: string[] = [];

// Email validation
if (!credentials.email) {
errors.push('Email is required');
} else if (!/\S+@\S+\.\S+/.test(credentials.email)) {
errors.push('Invalid email format');
}

// Password validation
if (!credentials.password) {
errors.push('Password is required');
} else if (credentials.password.length < 6) {
errors.push('Password must be at least 6 characters long');
}

return errors;
}
}
Loading