2026-06-26 10:50:24 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* AuthService – Registration, Login, Session Management
|
|
|
|
|
|
*/
|
|
|
|
|
|
import bcrypt from 'bcrypt';
|
|
|
|
|
|
import { randomUUID } from 'crypto';
|
2026-06-26 14:55:38 +02:00
|
|
|
|
import type { DatabaseInterface, DBUser, DBSession } from '../database/DatabaseInterface.js';
|
2026-06-26 10:50:24 +02:00
|
|
|
|
|
|
|
|
|
|
const SALT_ROUNDS = 10;
|
|
|
|
|
|
const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
|
|
|
|
|
|
|
|
|
|
export interface Session {
|
|
|
|
|
|
token: string;
|
|
|
|
|
|
userId: string;
|
|
|
|
|
|
expiresAt: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface AuthResult {
|
|
|
|
|
|
user: Omit<DBUser, 'password_hash'>;
|
|
|
|
|
|
session: Session;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export class AuthService {
|
|
|
|
|
|
constructor(private db: DatabaseInterface) {}
|
|
|
|
|
|
|
|
|
|
|
|
async register(email: string, password: string, name: string, role: string = 'planer'): Promise<AuthResult> {
|
|
|
|
|
|
const existing = this.db.getUserByEmail(email);
|
|
|
|
|
|
if (existing) {
|
|
|
|
|
|
throw new Error('Email already registered');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const password_hash = await bcrypt.hash(password, SALT_ROUNDS);
|
|
|
|
|
|
const user = this.db.createUser({ email, password_hash, name, role });
|
|
|
|
|
|
const session = this.createSession(user.id);
|
|
|
|
|
|
return { user: this.sanitizeUser(user), session };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async login(email: string, password: string): Promise<AuthResult> {
|
|
|
|
|
|
const user = this.db.getUserByEmail(email);
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
|
throw new Error('Invalid credentials');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const valid = await bcrypt.compare(password, user.password_hash);
|
|
|
|
|
|
if (!valid) {
|
|
|
|
|
|
throw new Error('Invalid credentials');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const session = this.createSession(user.id);
|
|
|
|
|
|
return { user: this.sanitizeUser(user), session };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async changePassword(userId: string, oldPassword: string, newPassword: string): Promise<void> {
|
|
|
|
|
|
const user = this.db.getUser(userId);
|
|
|
|
|
|
if (!user) throw new Error('User not found');
|
|
|
|
|
|
|
|
|
|
|
|
const valid = await bcrypt.compare(oldPassword, user.password_hash);
|
|
|
|
|
|
if (!valid) throw new Error('Invalid current password');
|
|
|
|
|
|
|
|
|
|
|
|
const password_hash = await bcrypt.hash(newPassword, SALT_ROUNDS);
|
|
|
|
|
|
this.db.updateUser(userId, { password_hash });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
createSession(userId: string): Session {
|
|
|
|
|
|
const token = randomUUID();
|
2026-06-26 14:55:38 +02:00
|
|
|
|
const expiresAt = Date.now() + SESSION_TTL_MS;
|
|
|
|
|
|
this.db.createSession({ token, user_id: userId, expires_at: expiresAt });
|
|
|
|
|
|
return { token, userId, expiresAt };
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
validateSession(token: string): Session | null {
|
2026-06-26 14:55:38 +02:00
|
|
|
|
const dbSession = this.db.getSession(token);
|
|
|
|
|
|
if (!dbSession) return null;
|
|
|
|
|
|
if (Date.now() > dbSession.expires_at) {
|
|
|
|
|
|
this.db.deleteSession(token);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2026-06-26 14:55:38 +02:00
|
|
|
|
return {
|
|
|
|
|
|
token: dbSession.token,
|
|
|
|
|
|
userId: dbSession.user_id,
|
|
|
|
|
|
expiresAt: dbSession.expires_at,
|
|
|
|
|
|
};
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
destroySession(token: string): void {
|
2026-06-26 14:55:38 +02:00
|
|
|
|
this.db.deleteSession(token);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getUserFromSession(token: string): DBUser | null {
|
|
|
|
|
|
const session = this.validateSession(token);
|
|
|
|
|
|
if (!session) return null;
|
|
|
|
|
|
return this.db.getUser(session.userId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private sanitizeUser(user: DBUser): Omit<DBUser, 'password_hash'> {
|
|
|
|
|
|
const { password_hash, ...safe } = user;
|
|
|
|
|
|
return safe;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|