feat(opsserver,web): replace the Angular UI and REST management layer with a TypedRequest-based ops server and bundled web frontend
This commit is contained in:
263
ts/opsserver/handlers/user.handler.ts
Normal file
263
ts/opsserver/handlers/user.handler.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import * as plugins from '../../plugins.ts';
|
||||
import * as interfaces from '../../../ts_interfaces/index.ts';
|
||||
import type { OpsServer } from '../classes.opsserver.ts';
|
||||
import { requireValidIdentity, requireAdminIdentity } from '../helpers/guards.ts';
|
||||
import { User } from '../../models/user.ts';
|
||||
import { Session } from '../../models/session.ts';
|
||||
import { AuthService } from '../../services/auth.service.ts';
|
||||
|
||||
export class UserHandler {
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
private authService = new AuthService();
|
||||
|
||||
constructor(private opsServerRef: OpsServer) {
|
||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
this.registerHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to format user to IUser interface
|
||||
*/
|
||||
private formatUser(user: User): interfaces.data.IUser {
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
avatarUrl: user.avatarUrl,
|
||||
isSystemAdmin: user.isSystemAdmin,
|
||||
isActive: user.isActive,
|
||||
createdAt: user.createdAt instanceof Date ? user.createdAt.toISOString() : String(user.createdAt),
|
||||
lastLoginAt: user.lastLoginAt instanceof Date ? user.lastLoginAt.toISOString() : user.lastLoginAt ? String(user.lastLoginAt) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private registerHandlers(): void {
|
||||
// Get Users (admin only)
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetUsers>(
|
||||
'getUsers',
|
||||
async (dataArg) => {
|
||||
await requireAdminIdentity(this.opsServerRef.authHandler, dataArg);
|
||||
|
||||
try {
|
||||
const users = await User.getInstances({});
|
||||
return {
|
||||
users: users.map((u) => this.formatUser(u)),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to list users');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Get User
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetUser>(
|
||||
'getUser',
|
||||
async (dataArg) => {
|
||||
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||||
|
||||
try {
|
||||
const { userId } = dataArg;
|
||||
|
||||
// Users can view their own profile, admins can view any
|
||||
if (userId !== dataArg.identity.userId && !dataArg.identity.isSystemAdmin) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Access denied');
|
||||
}
|
||||
|
||||
const user = await User.findById(userId);
|
||||
if (!user) {
|
||||
throw new plugins.typedrequest.TypedResponseError('User not found');
|
||||
}
|
||||
|
||||
return { user: this.formatUser(user) };
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to get user');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Update User
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateUser>(
|
||||
'updateUser',
|
||||
async (dataArg) => {
|
||||
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||||
|
||||
try {
|
||||
const { userId, displayName, avatarUrl, password, isActive, isSystemAdmin } = dataArg;
|
||||
|
||||
// Users can update their own profile, admins can update any
|
||||
if (userId !== dataArg.identity.userId && !dataArg.identity.isSystemAdmin) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Access denied');
|
||||
}
|
||||
|
||||
const user = await User.findById(userId);
|
||||
if (!user) {
|
||||
throw new plugins.typedrequest.TypedResponseError('User not found');
|
||||
}
|
||||
|
||||
if (displayName !== undefined) user.displayName = displayName;
|
||||
if (avatarUrl !== undefined) user.avatarUrl = avatarUrl;
|
||||
|
||||
// Only admins can change these
|
||||
if (dataArg.identity.isSystemAdmin) {
|
||||
if (isActive !== undefined) user.status = isActive ? 'active' : 'suspended';
|
||||
if (isSystemAdmin !== undefined) user.isPlatformAdmin = isSystemAdmin;
|
||||
}
|
||||
|
||||
// Password change
|
||||
if (password) {
|
||||
user.passwordHash = await AuthService.hashPassword(password);
|
||||
}
|
||||
|
||||
await user.save();
|
||||
|
||||
return { user: this.formatUser(user) };
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to update user');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Get User Sessions
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetUserSessions>(
|
||||
'getUserSessions',
|
||||
async (dataArg) => {
|
||||
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||||
|
||||
try {
|
||||
const sessions = await Session.getUserSessions(dataArg.identity.userId);
|
||||
|
||||
return {
|
||||
sessions: sessions.map((s) => ({
|
||||
id: s.id,
|
||||
userId: s.userId,
|
||||
userAgent: s.userAgent,
|
||||
ipAddress: s.ipAddress,
|
||||
isValid: s.isValid,
|
||||
lastActivityAt: s.lastActivityAt instanceof Date ? s.lastActivityAt.toISOString() : String(s.lastActivityAt),
|
||||
createdAt: s.createdAt instanceof Date ? s.createdAt.toISOString() : String(s.createdAt),
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to get sessions');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Revoke Session
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeSession>(
|
||||
'revokeSession',
|
||||
async (dataArg) => {
|
||||
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||||
|
||||
try {
|
||||
await this.authService.logout(dataArg.sessionId, {
|
||||
userId: dataArg.identity.userId,
|
||||
});
|
||||
|
||||
return { message: 'Session revoked successfully' };
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to revoke session');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Change Password
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ChangePassword>(
|
||||
'changePassword',
|
||||
async (dataArg) => {
|
||||
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||||
|
||||
try {
|
||||
const { currentPassword, newPassword } = dataArg;
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'Current password and new password are required',
|
||||
);
|
||||
}
|
||||
|
||||
const user = await User.findById(dataArg.identity.userId);
|
||||
if (!user) {
|
||||
throw new plugins.typedrequest.TypedResponseError('User not found');
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
const isValid = await user.verifyPassword(currentPassword);
|
||||
if (!isValid) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Current password is incorrect');
|
||||
}
|
||||
|
||||
// Hash and set new password
|
||||
user.passwordHash = await AuthService.hashPassword(newPassword);
|
||||
await user.save();
|
||||
|
||||
return { message: 'Password changed successfully' };
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to change password');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Delete Account
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteAccount>(
|
||||
'deleteAccount',
|
||||
async (dataArg) => {
|
||||
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||||
|
||||
try {
|
||||
const { password } = dataArg;
|
||||
|
||||
if (!password) {
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'Password is required to delete account',
|
||||
);
|
||||
}
|
||||
|
||||
const user = await User.findById(dataArg.identity.userId);
|
||||
if (!user) {
|
||||
throw new plugins.typedrequest.TypedResponseError('User not found');
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isValid = await user.verifyPassword(password);
|
||||
if (!isValid) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Password is incorrect');
|
||||
}
|
||||
|
||||
// Soft delete - deactivate instead of removing
|
||||
user.status = 'suspended';
|
||||
await user.save();
|
||||
|
||||
// Invalidate all sessions
|
||||
await Session.invalidateAllUserSessions(user.id, 'account_deleted');
|
||||
|
||||
return { message: 'Account deactivated successfully' };
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to delete account');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user