Move admin menu wiring server-side and filter ban/unban lists

This commit is contained in:
Jage9
2026-02-27 03:49:28 -05:00
parent 0edc9b9a3f
commit aba319751b
8 changed files with 103 additions and 50 deletions

View File

@@ -186,10 +186,8 @@ type AuthPolicy = {
passwordMaxLength: number;
};
type AdminMenuActionId = 'manage_roles' | 'change_user_role' | 'ban_user' | 'unban_user';
type AdminMenuAction = {
id: AdminMenuActionId;
id: string;
label: string;
};
@@ -300,6 +298,7 @@ let itemPropertiesShowAll = false;
let activeTeleportLoopStop: (() => void) | null = null;
let activeTeleportLoopToken = 0;
const adminMenuActions: AdminMenuAction[] = [];
let serverAdminMenuActions: AdminMenuAction[] = [];
let adminMenuIndex = 0;
let adminRoles: AdminRoleSummary[] = [];
let adminRoleIndex = 0;
@@ -595,6 +594,16 @@ function applyAuthPermissions(role: string | null | undefined, permissions: stri
applyVoiceSendPermission();
}
/** Applies server-authored admin menu actions for current session. */
function applyServerAdminMenuActions(actions: Array<{ id: string; label: string }> | null | undefined): void {
serverAdminMenuActions = (actions || [])
.map((entry) => ({
id: String(entry.id || '').trim(),
label: String(entry.label || '').trim(),
}))
.filter((entry) => entry.id.length > 0 && entry.label.length > 0);
}
/** Applies server-authoritative voice.send permission immediately to local outbound track state. */
function applyVoiceSendPermission(): void {
voiceSendAllowed = hasPermission('voice.send');
@@ -1485,6 +1494,7 @@ function sendAuthRequest(): void {
function handleAuthRequired(message: Extract<IncomingMessage, { type: 'auth_required' }>): void {
applyAuthPolicy(message.authPolicy);
applyAuthPermissions('user', []);
applyServerAdminMenuActions([]);
setConnectionStatus('Authentication required.');
updateStatus(message.message);
}
@@ -1502,6 +1512,7 @@ async function handleAuthResult(message: Extract<IncomingMessage, { type: 'auth_
settings.saveAuthSessionToken('');
}
applyAuthPermissions('user', []);
applyServerAdminMenuActions([]);
setConnectionStatus(message.message);
mediaSession.setConnecting(false);
updateConnectAvailability();
@@ -1526,6 +1537,7 @@ async function handleAuthResult(message: Extract<IncomingMessage, { type: 'auth_
}
}
applyAuthPermissions(message.role, message.permissions);
applyServerAdminMenuActions(message.adminMenuActions);
dom.authPassword.value = '';
dom.registerPassword.value = '';
dom.registerPasswordConfirm.value = '';
@@ -1539,6 +1551,7 @@ function logOutAccount(): void {
settings.saveAuthSessionToken('');
settings.saveAuthUsername('');
applyAuthPermissions('user', []);
applyServerAdminMenuActions([]);
if (state.running) {
signaling.send({ type: 'auth_logout' });
disconnect();
@@ -1552,6 +1565,7 @@ function logOutAccount(): void {
function handleAuthPermissions(message: Extract<IncomingMessage, { type: 'auth_permissions' }>): void {
const hadVoiceSend = voiceSendAllowed;
applyAuthPermissions(message.role, message.permissions);
applyServerAdminMenuActions(message.adminMenuActions);
if (hadVoiceSend && !voiceSendAllowed) {
updateStatus('Voice send permission revoked.');
}
@@ -1562,18 +1576,7 @@ function handleAuthPermissions(message: Extract<IncomingMessage, { type: 'auth_p
/** Returns available admin-menu root actions based on current permission set. */
function getAvailableAdminActions(): AdminMenuAction[] {
const actions: AdminMenuAction[] = [];
if (hasPermission('role.manage')) {
actions.push({ id: 'manage_roles', label: 'Role management' });
}
if (hasPermission('user.change_role')) {
actions.push({ id: 'change_user_role', label: 'Change user role' });
}
if (hasPermission('user.ban_unban')) {
actions.push({ id: 'ban_user', label: 'Ban user' });
actions.push({ id: 'unban_user', label: 'Unban user' });
}
return actions;
return [...serverAdminMenuActions];
}
/** Handles server role-list response for admin menu flows. */
@@ -1806,6 +1809,10 @@ async function onSignalingMessage(message: IncomingMessage): Promise<void> {
if (message.type === 'welcome') {
applyAuthPolicy(message.auth?.policy);
applyAuthPermissions(message.auth?.role, message.auth?.permissions);
const uiAdminActions =
(message.uiDefinitions as { adminMenu?: { actions?: Array<{ id: string; label: string }> } } | undefined)?.adminMenu?.actions ??
message.auth?.adminMenuActions;
applyServerAdminMenuActions(uiAdminActions);
const incomingInstanceId = String(message.serverInfo?.instanceId ?? '').trim() || null;
const incomingVersion = String(message.serverInfo?.version ?? '').trim() || 'unknown';
connectedAnnouncement = reconnectInFlight
@@ -2637,19 +2644,19 @@ function handleAdminMenuModeInput(code: string, key: string): void {
}
if (selected.id === 'change_user_role') {
adminPendingUserAction = 'set_role';
signaling.send({ type: 'admin_users_list' });
signaling.send({ type: 'admin_users_list', action: 'set_role' });
updateStatus('Loading users...');
return;
}
if (selected.id === 'ban_user') {
adminPendingUserAction = 'ban';
signaling.send({ type: 'admin_users_list' });
signaling.send({ type: 'admin_users_list', action: 'ban' });
updateStatus('Loading users...');
return;
}
if (selected.id === 'unban_user') {
adminPendingUserAction = 'unban';
signaling.send({ type: 'admin_users_list' });
signaling.send({ type: 'admin_users_list', action: 'unban' });
updateStatus('Loading users...');
}
return;
@@ -2818,17 +2825,31 @@ function handleAdminUserListModeInput(code: string, key: string): void {
return;
}
if (adminPendingUserAction === 'ban') {
adminUsers.splice(adminUserIndex, 1);
if (adminUsers.length > 0) {
adminUserIndex = Math.min(adminUserIndex, adminUsers.length - 1);
const next = adminUsers[adminUserIndex];
updateStatus(`${next.username}, ${next.role}, ${next.status}.`);
} else {
state.mode = 'adminMenu';
updateStatus('No users to ban.');
}
signaling.send({ type: 'admin_user_ban', username: selected.username });
state.mode = 'normal';
adminPendingUserAction = null;
updateStatus(`Banning ${selected.username}...`);
adminPendingUserAction = 'ban';
return;
}
if (adminPendingUserAction === 'unban') {
adminUsers.splice(adminUserIndex, 1);
if (adminUsers.length > 0) {
adminUserIndex = Math.min(adminUserIndex, adminUsers.length - 1);
const next = adminUsers[adminUserIndex];
updateStatus(`${next.username}, ${next.role}, ${next.status}.`);
} else {
state.mode = 'adminMenu';
updateStatus('No users to unban.');
}
signaling.send({ type: 'admin_user_unban', username: selected.username });
state.mode = 'normal';
adminPendingUserAction = null;
updateStatus(`Unbanning ${selected.username}...`);
adminPendingUserAction = 'unban';
return;
}
return;
@@ -2858,9 +2879,19 @@ function handleAdminUserRoleSelectModeInput(code: string, key: string): void {
if (control.type === 'select') {
const selectedRole = adminRoles[adminRoleIndex];
signaling.send({ type: 'admin_user_set_role', username: adminSelectedUsername, role: selectedRole.name });
state.mode = 'normal';
for (const user of adminUsers) {
if (user.username === adminSelectedUsername) {
user.role = selectedRole.name;
}
}
state.mode = 'adminUserList';
adminPendingUserAction = null;
updateStatus(`Setting ${adminSelectedUsername} to ${selectedRole.name}...`);
const selectedUser = adminUsers.find((user) => user.username === adminSelectedUsername);
if (selectedUser) {
updateStatus(`${selectedUser.username}, ${selectedUser.role}, ${selectedUser.status}.`);
} else {
updateStatus('Select user.');
}
return;
}
if (control.type === 'cancel') {

View File

@@ -57,6 +57,7 @@ export const welcomeMessageSchema = z.object({
username: z.string().nullable().optional(),
role: z.string().nullable().optional(),
permissions: z.array(z.string()).optional(),
adminMenuActions: z.array(z.object({ id: z.string(), label: z.string() })).optional(),
policy: z
.object({
usernameMinLength: z.number().int().positive(),
@@ -100,6 +101,11 @@ export const welcomeMessageSchema = z.object({
globalProperties: z.record(z.string(), z.unknown()).optional(),
}),
),
adminMenu: z
.object({
actions: z.array(z.object({ id: z.string(), label: z.string() })),
})
.optional(),
})
.optional(),
});
@@ -125,6 +131,7 @@ export const authResultSchema = z.object({
username: z.string().optional(),
role: z.string().optional(),
permissions: z.array(z.string()).optional(),
adminMenuActions: z.array(z.object({ id: z.string(), label: z.string() })).optional(),
nickname: z.string().optional(),
authPolicy: z
.object({
@@ -267,6 +274,7 @@ export const authPermissionsSchema = z.object({
type: z.literal('auth_permissions'),
role: z.string(),
permissions: z.array(z.string()),
adminMenuActions: z.array(z.object({ id: z.string(), label: z.string() })).optional(),
});
const adminRoleSummarySchema = z.object({
@@ -345,7 +353,7 @@ export type OutgoingMessage =
| { type: 'admin_role_create'; name: string }
| { type: 'admin_role_update_permissions'; role: string; permissions: string[] }
| { type: 'admin_role_delete'; role: string; replacementRole: string }
| { type: 'admin_users_list' }
| { type: 'admin_users_list'; action?: 'set_role' | 'ban' | 'unban' }
| { type: 'admin_user_set_role'; username: string; role: string }
| { type: 'admin_user_ban'; username: string }
| { type: 'admin_user_unban'; username: string }