Files
onebox/ts/opsserver/handlers/admin.handler.ts
T

221 lines
7.0 KiB
TypeScript
Raw Normal View History

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<IJwtData>;
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
}
public async initialize(): Promise<void> {
this.smartjwtInstance = new plugins.smartjwt.SmartJwt<IJwtData>();
await this.smartjwtInstance.init();
this.registerHandlers();
}
private async createIdentityForUser(
user: interfaces.data.IUser & { id?: number },
expiresAt: number,
): Promise<interfaces.data.IIdentity> {
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<interfaces.data.IIdentity> {
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<interfaces.data.IIdentity> {
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<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
'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<interfaces.requests.IReq_AdminLogout>(
'adminLogout',
async (_dataArg) => {
return { ok: true };
},
),
);
// Verify Identity
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_VerifyIdentity>(
'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<interfaces.requests.IReq_ChangePassword>(
'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' },
);
}