feat(auth): add AuthService.ts
This commit is contained in:
@@ -0,0 +1,201 @@
|
|||||||
|
import argon2 from 'argon2';
|
||||||
|
import jwt from '@fastify/jwt';
|
||||||
|
import { FastifyInstance } from 'fastify';
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
displayName: string;
|
||||||
|
passwordHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Session {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
expiresAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PasswordReset {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
tokenHash: string;
|
||||||
|
expiresAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthService {
|
||||||
|
private users: Map<string, User> = new Map();
|
||||||
|
private sessions: Map<string, Session> = new Map();
|
||||||
|
private passwordResets: Map<string, PasswordReset> = new Map();
|
||||||
|
private fastify: FastifyInstance;
|
||||||
|
|
||||||
|
constructor(fastify: FastifyInstance) {
|
||||||
|
this.fastify = fastify;
|
||||||
|
}
|
||||||
|
|
||||||
|
async register(email: string, password: string, displayName: string): Promise<Omit<User, 'passwordHash'>> {
|
||||||
|
// Check if user already exists
|
||||||
|
for (const user of this.users.values()) {
|
||||||
|
if (user.email === email) {
|
||||||
|
throw new Error('User already exists');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
const passwordHash = await argon2.hash(password);
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
const userId = this.generateId();
|
||||||
|
const user: User = {
|
||||||
|
id: userId,
|
||||||
|
email,
|
||||||
|
displayName,
|
||||||
|
passwordHash
|
||||||
|
};
|
||||||
|
|
||||||
|
this.users.set(userId, user);
|
||||||
|
|
||||||
|
// Return user without password hash
|
||||||
|
const { passwordHash: _, ...userWithoutHash } = user;
|
||||||
|
return userWithoutHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(email: string, password: string): Promise<string> {
|
||||||
|
// Find user
|
||||||
|
let user: User | undefined;
|
||||||
|
for (const u of this.users.values()) {
|
||||||
|
if (u.email === email) {
|
||||||
|
user = u;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify password
|
||||||
|
const isValid = await argon2.verify(user.passwordHash, password);
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error('Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create session
|
||||||
|
const sessionId = this.generateId();
|
||||||
|
const session: Session = {
|
||||||
|
id: sessionId,
|
||||||
|
userId: user.id,
|
||||||
|
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
|
||||||
|
};
|
||||||
|
|
||||||
|
this.sessions.set(sessionId, session);
|
||||||
|
|
||||||
|
// Generate JWT token
|
||||||
|
const token = this.fastify.jwt.sign({
|
||||||
|
userId: user.id,
|
||||||
|
sessionId,
|
||||||
|
expiresAt: session.expiresAt
|
||||||
|
});
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout(token: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const decoded = this.fastify.jwt.verify(token);
|
||||||
|
const sessionId = decoded.sessionId;
|
||||||
|
|
||||||
|
if (this.sessions.has(sessionId)) {
|
||||||
|
this.sessions.delete(sessionId);
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid session');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Invalid token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMe(userId: string): Promise<Omit<User, 'passwordHash'>> {
|
||||||
|
const user = this.users.get(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { passwordHash: _, ...userWithoutHash } = user;
|
||||||
|
return userWithoutHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestPasswordReset(email: string): Promise<void> {
|
||||||
|
// Find user
|
||||||
|
let user: User | undefined;
|
||||||
|
for (const u of this.users.values()) {
|
||||||
|
if (u.email === email) {
|
||||||
|
user = u;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
// Don't reveal if user exists or not
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate reset token
|
||||||
|
const resetToken = this.generateId();
|
||||||
|
const tokenHash = await argon2.hash(resetToken);
|
||||||
|
|
||||||
|
// Store reset token
|
||||||
|
const reset: PasswordReset = {
|
||||||
|
id: this.generateId(),
|
||||||
|
userId: user.id,
|
||||||
|
tokenHash,
|
||||||
|
expiresAt: new Date(Date.now() + 60 * 60 * 1000) // 1 hour
|
||||||
|
};
|
||||||
|
|
||||||
|
this.passwordResets.set(reset.id, reset);
|
||||||
|
|
||||||
|
// In a real implementation, you would send an email here
|
||||||
|
// For now, we'll just log that we would send an email
|
||||||
|
console.log(`Password reset token for ${email}: ${resetToken}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmPasswordReset(token: string, newPassword: string): Promise<void> {
|
||||||
|
// Find reset token
|
||||||
|
let reset: PasswordReset | undefined;
|
||||||
|
let resetId: string | undefined;
|
||||||
|
|
||||||
|
for (const [id, r] of this.passwordResets.entries()) {
|
||||||
|
const isValid = await argon2.verify(r.tokenHash, token);
|
||||||
|
if (isValid && r.expiresAt > new Date()) {
|
||||||
|
reset = r;
|
||||||
|
resetId = id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reset || !resetId) {
|
||||||
|
throw new Error('Invalid or expired reset token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
const user = this.users.get(reset.userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash new password
|
||||||
|
const newPasswordHash = await argon2.hash(newPassword);
|
||||||
|
|
||||||
|
// Update user password
|
||||||
|
user.passwordHash = newPasswordHash;
|
||||||
|
this.users.set(user.id, user);
|
||||||
|
|
||||||
|
// Delete reset token
|
||||||
|
this.passwordResets.delete(resetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateId(): string {
|
||||||
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthService;
|
||||||
Reference in New Issue
Block a user