import * as plugins from '../../plugins.ts'; import { logger } from '../../logging.ts'; import type { OpsServer } from '../classes.opsserver.ts'; import * as interfaces from '../../../ts_interfaces/index.ts'; import { hashPassword, needsPasswordUpgrade, verifyPassword } from '../../utils/auth.ts'; export interface IJwtData { userId: string; username: string; role: 'admin' | 'user'; status: 'loggedIn' | 'loggedOut'; expiresAt: number; } export class AdminHandler { public typedrouter = new plugins.typedrequest.TypedRouter(); public smartjwtInstance!: plugins.smartjwt.SmartJwt; constructor(private opsServerRef: OpsServer) { this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); } public async initialize(): Promise { this.smartjwtInstance = new plugins.smartjwt.SmartJwt(); await this.smartjwtInstance.init(); this.registerHandlers(); } private async createIdentityForUser( user: interfaces.data.IUser & { id?: number }, expiresAt: number, ): Promise { const userId = String(user.id || user.username); const jwt = await this.smartjwtInstance.createJWT({ userId, username: user.username, role: user.role, status: 'loggedIn', expiresAt, }); return { jwt, userId, username: user.username, expiresAt, role: user.role, }; } public async getVerifiedIdentity( identityArg: interfaces.data.IIdentity | null | undefined, ): Promise { if (!identityArg?.jwt) { throw new plugins.typedrequest.TypedResponseError('No identity provided'); } let jwtData: IJwtData; try { jwtData = await this.smartjwtInstance.verifyJWTAndGetData(identityArg.jwt); } catch { throw new plugins.typedrequest.TypedResponseError('Valid identity required'); } if (jwtData.expiresAt < Date.now() || jwtData.status !== 'loggedIn') { throw new plugins.typedrequest.TypedResponseError('Valid identity required'); } const user = this.opsServerRef.oneboxRef.database.getUserByUsername(jwtData.username); if (!user) { throw new plugins.typedrequest.TypedResponseError('Valid identity required'); } const userId = String(user.id || user.username); if (jwtData.userId !== userId) { throw new plugins.typedrequest.TypedResponseError('Valid identity required'); } return { jwt: identityArg.jwt, userId, username: user.username, expiresAt: jwtData.expiresAt, role: user.role, }; } public async getVerifiedAdminIdentity( identityArg: interfaces.data.IIdentity | null | undefined, ): Promise { const identity = await this.getVerifiedIdentity(identityArg); if (identity.role !== 'admin') { throw new plugins.typedrequest.TypedResponseError('Admin access required'); } return identity; } private registerHandlers(): void { // Login this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'adminLoginWithUsernameAndPassword', async (dataArg) => { try { const user = this.opsServerRef.oneboxRef.database.getUserByUsername(dataArg.username); if (!user) { throw new plugins.typedrequest.TypedResponseError('Invalid credentials'); } const passwordMatches = await verifyPassword(dataArg.password, user.passwordHash); if (!passwordMatches) { throw new plugins.typedrequest.TypedResponseError('Invalid credentials'); } if (needsPasswordUpgrade(user.passwordHash)) { const upgradedHash = await hashPassword(dataArg.password); this.opsServerRef.oneboxRef.database.updateUserPassword(user.username, upgradedHash); } const expiresAt = Date.now() + 24 * 3600 * 1000; const freshUser = this.opsServerRef.oneboxRef.database.getUserByUsername(user.username) || user; const identity = await this.createIdentityForUser(freshUser, expiresAt); logger.info(`User logged in: ${user.username}`); return { identity, }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Login failed'); } }, ), ); // Logout this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'adminLogout', async (_dataArg) => { return { ok: true }; }, ), ); // Verify Identity this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'verifyIdentity', async (dataArg) => { try { const identity = await this.getVerifiedIdentity(dataArg.identity); return { valid: true, identity, }; } catch { return { valid: false }; } }, ), ); // Change Password this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'changePassword', async (dataArg) => { const identity = await this.getVerifiedIdentity(dataArg.identity); const user = this.opsServerRef.oneboxRef.database.getUserByUsername(identity.username); if (!user) { throw new plugins.typedrequest.TypedResponseError('User not found'); } const currentPasswordMatches = await verifyPassword(dataArg.currentPassword, user.passwordHash); if (!currentPasswordMatches) { throw new plugins.typedrequest.TypedResponseError('Current password is incorrect'); } const newHash = await hashPassword(dataArg.newPassword); this.opsServerRef.oneboxRef.database.updateUserPassword(user.username, newHash); logger.info(`Password changed for user: ${user.username}`); return { ok: true }; }, ), ); } // Guard for valid identity public validIdentityGuard = new plugins.smartguard.Guard<{ identity: interfaces.data.IIdentity; }>( async (dataArg) => { try { await this.getVerifiedIdentity(dataArg.identity); return true; } catch { return false; } }, { failedHint: 'identity is not valid', name: 'validIdentityGuard' }, ); // Guard for admin identity public adminIdentityGuard = new plugins.smartguard.Guard<{ identity: interfaces.data.IIdentity; }>( async (dataArg) => { try { const identity = await this.getVerifiedIdentity(dataArg.identity); return identity.role === 'admin'; } catch { return false; } }, { failedHint: 'user is not admin', name: 'adminIdentityGuard' }, ); }