feat(auth): add AuthService.ts

This commit is contained in:
2026-06-22 05:40:46 +00:00
parent ce1cb72d41
commit 57be3afcf8
+201
View File
@@ -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;