190 lines
6.3 KiB
TypeScript
190 lines
6.3 KiB
TypeScript
import * as plugins from '../../plugins.ts';
|
|
import type { OpsServer } from '../classes.opsserver.ts';
|
|
import * as interfaces from '../../../ts_interfaces/index.ts';
|
|
|
|
export interface IJwtData {
|
|
userId: string;
|
|
role: 'admin';
|
|
status: 'loggedIn' | 'loggedOut';
|
|
expiresAt: number;
|
|
}
|
|
|
|
export class AdminHandler {
|
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
|
|
private revokedTokens = new Set<string>();
|
|
private failedLoginAttempts = new Map<string, { count: number; firstAttemptAt: number }>();
|
|
|
|
constructor(private opsServerRef: OpsServer) {
|
|
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
}
|
|
|
|
public async initialize(): Promise<void> {
|
|
this.smartjwtInstance = new plugins.smartjwt.SmartJwt();
|
|
await this.smartjwtInstance.init();
|
|
await this.smartjwtInstance.createNewKeyPair();
|
|
this.registerHandlers();
|
|
}
|
|
|
|
private registerHandlers(): void {
|
|
// Login
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
|
'adminLoginWithUsernameAndPassword',
|
|
async (dataArg) => {
|
|
this.assertLoginNotRateLimited(dataArg.username);
|
|
const adminPassword = this.opsServerRef.objectStorageRef.config.adminPassword;
|
|
if (dataArg.username !== 'admin' || dataArg.password !== adminPassword) {
|
|
this.recordFailedLogin(dataArg.username);
|
|
await this.opsServerRef.objectStorageRef.auditLogger.log({
|
|
actorUserId: dataArg.username || 'anonymous',
|
|
action: 'admin.login',
|
|
targetType: 'adminSession',
|
|
success: false,
|
|
message: 'Invalid credentials',
|
|
});
|
|
throw new plugins.typedrequest.TypedResponseError('Invalid credentials');
|
|
}
|
|
|
|
this.failedLoginAttempts.delete(dataArg.username);
|
|
const expiresAt = Date.now() + 24 * 3600 * 1000;
|
|
const userId = 'admin';
|
|
const jwt = await this.smartjwtInstance.createJWT({
|
|
userId,
|
|
role: 'admin',
|
|
status: 'loggedIn',
|
|
expiresAt,
|
|
});
|
|
|
|
await this.opsServerRef.objectStorageRef.auditLogger.log({
|
|
actorUserId: userId,
|
|
action: 'admin.login',
|
|
targetType: 'adminSession',
|
|
success: true,
|
|
});
|
|
|
|
console.log('Admin user logged in');
|
|
|
|
return {
|
|
identity: {
|
|
jwt,
|
|
userId,
|
|
username: 'admin',
|
|
expiresAt,
|
|
role: 'admin' as const,
|
|
},
|
|
};
|
|
},
|
|
),
|
|
);
|
|
|
|
// Logout
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLogout>(
|
|
'adminLogout',
|
|
async (dataArg) => {
|
|
if (dataArg.identity?.jwt) {
|
|
this.revokedTokens.add(dataArg.identity.jwt);
|
|
await this.opsServerRef.objectStorageRef.auditLogger.log({
|
|
actorUserId: dataArg.identity.userId,
|
|
action: 'admin.logout',
|
|
targetType: 'adminSession',
|
|
success: true,
|
|
});
|
|
}
|
|
return { ok: true };
|
|
},
|
|
),
|
|
);
|
|
|
|
// Verify Identity
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_VerifyIdentity>(
|
|
'verifyIdentity',
|
|
async (dataArg) => {
|
|
if (!dataArg.identity?.jwt) {
|
|
return { valid: false };
|
|
}
|
|
if (this.revokedTokens.has(dataArg.identity.jwt)) return { valid: false };
|
|
try {
|
|
const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt);
|
|
if (jwtData.expiresAt < Date.now()) return { valid: false };
|
|
if (jwtData.status !== 'loggedIn') return { valid: false };
|
|
if (jwtData.role !== 'admin') return { valid: false };
|
|
return {
|
|
valid: true,
|
|
identity: {
|
|
jwt: dataArg.identity.jwt,
|
|
userId: jwtData.userId,
|
|
username: jwtData.userId,
|
|
expiresAt: jwtData.expiresAt,
|
|
role: jwtData.role,
|
|
},
|
|
};
|
|
} catch {
|
|
return { valid: false };
|
|
}
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// Guard for valid identity
|
|
public validIdentityGuard = new plugins.smartguard.Guard<{
|
|
identity: interfaces.data.IIdentity;
|
|
}>(
|
|
async (dataArg) => {
|
|
if (!dataArg.identity?.jwt) return false;
|
|
if (this.revokedTokens.has(dataArg.identity.jwt)) return false;
|
|
try {
|
|
const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt);
|
|
if (jwtData.expiresAt < Date.now()) return false;
|
|
if (jwtData.status !== 'loggedIn') return false;
|
|
if (jwtData.role !== 'admin') return false;
|
|
if (dataArg.identity.expiresAt !== jwtData.expiresAt) return false;
|
|
if (dataArg.identity.userId !== jwtData.userId) return false;
|
|
if (dataArg.identity.role !== jwtData.role) return false;
|
|
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) => {
|
|
return await this.validIdentityGuard.exec(dataArg);
|
|
},
|
|
{ failedHint: 'user is not admin', name: 'adminIdentityGuard' },
|
|
);
|
|
|
|
private assertLoginNotRateLimited(username: string): void {
|
|
const attempt = this.failedLoginAttempts.get(username);
|
|
if (!attempt) return;
|
|
|
|
const windowMs = 60 * 1000;
|
|
if (Date.now() - attempt.firstAttemptAt > windowMs) {
|
|
this.failedLoginAttempts.delete(username);
|
|
return;
|
|
}
|
|
|
|
if (attempt.count >= 5) {
|
|
throw new plugins.typedrequest.TypedResponseError('Too many failed login attempts');
|
|
}
|
|
}
|
|
|
|
private recordFailedLogin(username: string): void {
|
|
const now = Date.now();
|
|
const attempt = this.failedLoginAttempts.get(username);
|
|
if (!attempt || now - attempt.firstAttemptAt > 60 * 1000) {
|
|
this.failedLoginAttempts.set(username, { count: 1, firstAttemptAt: now });
|
|
return;
|
|
}
|
|
attempt.count++;
|
|
}
|
|
}
|