2026-03-05 00:04:26 -06:00
let currentUser = null ;
let users = [];
const usersTableBody = document . getElementById ( "users-table-body" );
const addUserBtn = document . getElementById ( "add-user-btn" );
const userModal = document . getElementById ( "user-modal" );
const modalTitle = document . getElementById ( "modal-title" );
const userForm = document . getElementById ( "user-form" );
const closeModal = document . getElementById ( "close-modal" );
const cancelBtn = document . getElementById ( "cancel-btn" );
const errorMessage = document . getElementById ( "error-message" );
const errorText = document . getElementById ( "error-text" );
const logoutBtn = document . getElementById ( "logout-btn" );
const currentUserEl = document . getElementById ( "username-display" );
const currentRoleEl = document . getElementById ( "role-badge" );
async function getCurrentUser () {
try {
const res = await fetch ( "/api/auth/me" );
if ( res . ok ) {
currentUser = await res . json ();
currentUserEl . textContent = currentUser . username ;
const roleBadges = {
admin : '<i class="fa-solid fa-crown mr-1"></i>Admin' ,
operator : '<i class="fa-solid fa-sliders mr-1"></i>Operator' ,
viewer : '<i class="fa-solid fa-eye mr-1"></i>Viewer' ,
};
if ( roleBadges [ currentUser . role ]) {
currentRoleEl . innerHTML = roleBadges [ currentUser . role ];
} else {
currentRoleEl . textContent = currentUser . role || "" ;
}
if ( currentUser . role === "admin" ) {
currentRoleEl . classList . add (
"bg-purple-900/50" ,
"text-purple-300" ,
"border" ,
"border-purple-800" ,
);
} else if ( currentUser . role === "operator" ) {
currentRoleEl . classList . add (
"bg-blue-900/50" ,
"text-blue-300" ,
"border" ,
"border-blue-800" ,
);
} else {
currentRoleEl . classList . add (
"bg-slate-700" ,
"text-slate-300" ,
"border" ,
"border-slate-600" ,
);
}
if ( currentUser . role === "admin" ) {
document . getElementById ( "metrics-link" ) ? . classList . remove ( "hidden" );
document . getElementById ( "scripts-link" ) ? . classList . remove ( "hidden" );
document . getElementById ( "build-link" ) ? . classList . remove ( "hidden" );
document . getElementById ( "users-link" ) ? . classList . remove ( "hidden" );
document . getElementById ( "plugins-link" ) ? . classList . remove ( "hidden" );
document . getElementById ( "deploy-link" ) ? . classList . remove ( "hidden" );
} else if ( currentUser . role === "operator" ) {
document . getElementById ( "metrics-link" ) ? . classList . remove ( "hidden" );
document . getElementById ( "scripts-link" ) ? . classList . remove ( "hidden" );
document . getElementById ( "build-link" ) ? . classList . remove ( "hidden" );
}
2026-03-12 21:26:53 -05:00
if ( currentUser . canBuild ) {
document . getElementById ( "build-link" ) ? . classList . remove ( "hidden" );
}
2026-03-05 00:04:26 -06:00
if ( currentUser . role !== "admin" ) {
alert ( "Access denied. Admin role required." );
window . location . href = "/" ;
}
} else {
window . location . href = "/login.html" ;
}
} catch ( err ) {
console . error ( "Failed to get current user:" , err );
window . location . href = "/login.html" ;
}
}
if ( logoutBtn && ! logoutBtn . dataset . boundLogout ) {
logoutBtn . dataset . boundLogout = "true" ;
logoutBtn . addEventListener ( "click" , async () => {
if ( ! confirm ( "Are you sure you want to logout?" )) return ;
try {
const res = await fetch ( "/api/logout" , { method : "POST" });
if ( res . ok ) {
window . location . href = "/" ;
} else {
alert ( "Logout failed. Please try again." );
}
} catch ( err ) {
console . error ( "Logout error:" , err );
alert ( "Logout failed. Please try again." );
}
});
}
async function loadUsers () {
try {
const res = await fetch ( "/api/users" );
if ( ! res . ok ) {
throw new Error ( "Failed to load users" );
}
const data = await res . json ();
users = data . users || [];
renderUsers ();
} catch ( err ) {
console . error ( "Load users error:" , err );
usersTableBody . innerHTML = `
<tr>
<td colspan="6" class="px-6 py-12 text-center text-red-400">
<i class="fa-solid fa-exclamation-triangle mr-2"></i>
Failed to load users
</td>
</tr>
` ;
}
}
function renderUsers () {
if ( users . length === 0 ) {
usersTableBody . innerHTML = `
<tr>
<td colspan="6" class="px-6 py-12 text-center text-slate-400">
<i class="fa-solid fa-users mr-2"></i>
No users found
</td>
</tr>
` ;
return ;
}
usersTableBody . innerHTML = users
. map (
( user ) => `
<tr class="hover:bg-slate-800/30 transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-2">
<i class="fa-solid fa-user text-slate-400"></i>
<span class="font-medium text-slate-200"> ${ escapeHtml ( user . username ) } </span>
${ user . id === currentUser ? . userId ? '<span class="text-xs text-blue-400">(You)</span>' : "" }
</div>
</td>
<td class="px-6 py-4">
${ getRoleBadge ( user . role ) }
2026-03-12 21:26:53 -05:00
${ user . role !== "admin" ? `
<span class="ml-1 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium cursor-pointer transition-colors ${ user . can_build ? 'bg-green-900/30 text-green-300 border border-green-800 hover:bg-green-900/50' : 'bg-slate-700/30 text-slate-500 border border-slate-700 hover:bg-slate-700/50' } "
data-action="toggle-build" data-user-id=" ${ user . id } " data-username=" ${ escapeHtml ( user . username ) } " data-can-build=" ${ user . can_build ? 1 : 0 } "
title=" ${ user . can_build ? 'Can build (click to revoke)' : 'Cannot build (click to grant)' } ">
<i class="fa-solid fa-hammer mr-1"></i> ${ user . can_build ? 'Build' : 'No Build' }
</span>
2026-04-08 10:33:22 -05:00
<span class="ml-1 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium cursor-pointer transition-colors ${ user . can_upload_files ? 'bg-teal-900/30 text-teal-300 border border-teal-800 hover:bg-teal-900/50' : 'bg-slate-700/30 text-slate-500 border border-slate-700 hover:bg-slate-700/50' } "
data-action="toggle-upload" data-user-id=" ${ user . id } " data-username=" ${ escapeHtml ( user . username ) } " data-can-upload=" ${ user . can_upload_files ? 1 : 0 } "
title=" ${ user . can_upload_files ? 'Can upload files (click to revoke)' : 'Cannot upload files (click to grant)' } ">
<i class="fa-solid fa-cloud-arrow-up mr-1"></i> ${ user . can_upload_files ? 'Upload' : 'No Upload' }
</span>
2026-03-12 21:26:53 -05:00
` : '' }
2026-03-05 00:04:26 -06:00
</td>
<td class="px-6 py-4 text-sm text-slate-400">
${ formatDate ( user . created_at ) }
</td>
<td class="px-6 py-4 text-sm text-slate-400">
${ user . last_login ? formatDate ( user . last_login ) : '<span class="text-slate-500">Never</span>' }
</td>
<td class="px-6 py-4 text-sm text-slate-400">
${ escapeHtml ( user . created_by || "System" ) }
</td>
<td class="px-6 py-4">
<div class="flex items-center justify-end gap-2">
${
user . id !== currentUser ? . userId
? `
<button
class="user-action-btn px-3 py-1.5 text-sm bg-slate-800 hover:bg-slate-700 text-slate-200 rounded border border-slate-700 transition-colors"
data-action="change-password"
data-user-id=" ${ user . id } "
data-username=" ${ escapeHtml ( user . username ) } "
title="Change Password"
>
<i class="fa-solid fa-key"></i>
</button>
<button
class="user-action-btn px-3 py-1.5 text-sm bg-slate-800 hover:bg-slate-700 text-slate-200 rounded border border-slate-700 transition-colors"
data-action="change-role"
data-user-id=" ${ user . id } "
data-username=" ${ escapeHtml ( user . username ) } "
data-role=" ${ escapeHtml ( user . role ) } "
title="Change Role"
>
<i class="fa-solid fa-user-tag"></i>
</button>
2026-03-06 20:50:02 -06:00
<button
class="user-action-btn px-3 py-1.5 text-sm bg-indigo-900/30 hover:bg-indigo-900/50 text-indigo-300 rounded border border-indigo-800 transition-colors"
data-action="client-access"
data-user-id=" ${ user . id } "
data-username=" ${ escapeHtml ( user . username ) } "
data-role=" ${ escapeHtml ( user . role ) } "
title="Client Visibility"
>
<i class="fa-solid fa-user-shield"></i>
</button>
2026-03-05 00:04:26 -06:00
<button
class="user-action-btn px-3 py-1.5 text-sm bg-red-900/20 hover:bg-red-900/40 text-red-400 rounded border border-red-800 transition-colors"
data-action="delete"
data-user-id=" ${ user . id } "
data-username=" ${ escapeHtml ( user . username ) } "
title="Delete User"
>
<i class="fa-solid fa-trash"></i>
</button>
`
: '<span class="text-slate-500 text-sm italic">Cannot edit yourself</span>'
}
</div>
</td>
</tr>
` ,
)
. join ( "" );
attachActionListeners ();
}
function getRoleBadge ( role ) {
const badges = {
admin :
'<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-900/30 text-purple-300 border border-purple-800"><i class="fa-solid fa-crown mr-1"></i>Admin</span>' ,
operator :
'<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-900/30 text-blue-300 border border-blue-800"><i class="fa-solid fa-sliders mr-1"></i>Operator</span>' ,
viewer :
'<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-slate-700/50 text-slate-300 border border-slate-600"><i class="fa-solid fa-eye mr-1"></i>Viewer</span>' ,
};
return badges [ role ] || escapeHtml ( role || "" );
}
function attachActionListeners () {
const oldListener = usersTableBody . _actionListener ;
if ( oldListener ) {
usersTableBody . removeEventListener ( "click" , oldListener );
}
const listener = ( e ) => {
2026-03-12 21:26:53 -05:00
const toggleBuild = e . target . closest ( "[data-action='toggle-build']" );
if ( toggleBuild ) {
const userId = parseInt ( toggleBuild . dataset . userId );
const username = toggleBuild . dataset . username ;
const canBuild = toggleBuild . dataset . canBuild === "1" ;
toggleBuildPermission ( userId , username , canBuild );
return ;
}
2026-04-08 10:33:22 -05:00
const toggleUpload = e . target . closest ( "[data-action='toggle-upload']" );
if ( toggleUpload ) {
const userId = parseInt ( toggleUpload . dataset . userId );
const username = toggleUpload . dataset . username ;
const canUpload = toggleUpload . dataset . canUpload === "1" ;
toggleUploadPermission ( userId , username , canUpload );
return ;
}
2026-03-05 00:04:26 -06:00
const btn = e . target . closest ( ".user-action-btn" );
if ( ! btn ) return ;
const action = btn . dataset . action ;
const userId = parseInt ( btn . dataset . userId );
const username = btn . dataset . username ;
const role = btn . dataset . role ;
switch ( action ) {
case "change-password" :
changePassword ( userId , username );
break ;
case "change-role" :
changeRole ( userId , username , role );
break ;
case "delete" :
deleteUser ( userId , username );
break ;
2026-03-06 20:50:02 -06:00
case "client-access" :
configureClientAccess ( userId , username , role );
break ;
2026-03-05 00:04:26 -06:00
}
};
usersTableBody . addEventListener ( "click" , listener );
usersTableBody . _actionListener = listener ;
}
function formatDate ( timestamp ) {
const date = new Date ( timestamp );
return date . toLocaleString ();
}
function escapeHtml ( text ) {
const div = document . createElement ( "div" );
div . textContent = text ;
return div . innerHTML ;
}
function showModal ( title ) {
modalTitle . textContent = title ;
userModal . classList . remove ( "hidden" );
errorMessage . classList . add ( "hidden" );
userForm . reset ();
}
function hideModal () {
userModal . classList . add ( "hidden" );
userForm . reset ();
}
function showError ( message ) {
errorText . textContent = message ;
errorMessage . classList . remove ( "hidden" );
}
addUserBtn . addEventListener ( "click" , () => {
showModal ( "Add User" );
document . getElementById ( "password-field" ). classList . remove ( "hidden" );
document . getElementById ( "password" ). required = true ;
});
closeModal . addEventListener ( "click" , hideModal );
cancelBtn . addEventListener ( "click" , hideModal );
userForm . addEventListener ( "submit" , async ( e ) => {
e . preventDefault ();
const formData = new FormData ( userForm );
const username = formData . get ( "username" );
const password = formData . get ( "password" );
const role = formData . get ( "role" );
try {
const res = await fetch ( "/api/users" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" },
body : JSON . stringify ({ username , password , role }),
});
const data = await res . json ();
if ( res . ok ) {
hideModal ();
await loadUsers ();
} else {
showError ( data . error || "Failed to create user" );
}
} catch ( err ) {
console . error ( "Create user error:" , err );
showError ( "Network error. Please try again." );
}
});
window . changePassword = async function ( userId , username ) {
const password = prompt ( `Enter new password for ${ username } :` );
if ( ! password ) return ;
if ( password . length < 6 ) {
alert ( "Password must be at least 6 characters" );
return ;
}
try {
const res = await fetch ( `/api/users/ ${ userId } /password` , {
method : "PUT" ,
headers : { "Content-Type" : "application/json" },
body : JSON . stringify ({ password }),
});
const data = await res . json ();
if ( res . ok ) {
alert ( "Password updated successfully" );
} else {
alert ( data . error || "Failed to update password" );
}
} catch ( err ) {
console . error ( "Update password error:" , err );
alert ( "Network error. Please try again." );
}
};
window . changeRole = async function ( userId , username , currentRole ) {
const roles = [ "viewer" , "operator" , "admin" ];
const roleNames = { viewer : "Viewer" , operator : "Operator" , admin : "Admin" };
const message = `Select new role for ${ username } :\n\n1. Viewer (Read-only)\n2. Operator (Control clients)\n3. Admin (Full access)\n\nCurrent: ${ roleNames [ currentRole ] } ` ;
const choice = prompt ( message );
if ( ! choice || ! [ "1" , "2" , "3" ]. includes ( choice )) return ;
const newRole = roles [ parseInt ( choice ) - 1 ];
if ( newRole === currentRole ) {
alert ( "No change made" );
return ;
}
if ( ! confirm ( `Change ${ username } 's role to ${ roleNames [ newRole ] } ?` )) return ;
try {
const res = await fetch ( `/api/users/ ${ userId } /role` , {
method : "PUT" ,
headers : { "Content-Type" : "application/json" },
body : JSON . stringify ({ role : newRole }),
});
const data = await res . json ();
if ( res . ok ) {
alert ( "Role updated successfully" );
await loadUsers ();
} else {
alert ( data . error || "Failed to update role" );
}
} catch ( err ) {
console . error ( "Update role error:" , err );
alert ( "Network error. Please try again." );
}
};
window . deleteUser = async function ( userId , username ) {
if (
! confirm (
`Are you sure you want to delete user " ${ username } "? This action cannot be undone.` ,
)
) {
return ;
}
try {
const res = await fetch ( `/api/users/ ${ userId } ` , {
method : "DELETE" ,
});
const data = await res . json ();
if ( res . ok ) {
alert ( "User deleted successfully" );
await loadUsers ();
} else {
alert ( data . error || "Failed to delete user" );
}
} catch ( err ) {
console . error ( "Delete user error:" , err );
alert ( "Network error. Please try again." );
}
};
2026-03-06 20:50:02 -06:00
window . configureClientAccess = async function ( userId , username , role ) {
const params = new URLSearchParams ({ userId : String ( userId ) });
window . location . href = `/user-client-access? ${ params . toString () } ` ;
};
2026-03-12 21:26:53 -05:00
window . toggleBuildPermission = async function ( userId , username , currentCanBuild ) {
const newVal = ! currentCanBuild ;
if ( ! confirm ( ` ${ newVal ? 'Grant' : 'Revoke' } build permission for ${ username } ?` )) return ;
try {
const res = await fetch ( `/api/users/ ${ userId } /can-build` , {
method : "PUT" ,
headers : { "Content-Type" : "application/json" },
body : JSON . stringify ({ canBuild : newVal }),
});
const data = await res . json ();
if ( res . ok ) {
await loadUsers ();
} else {
alert ( data . error || "Failed to update build permission" );
}
} catch ( err ) {
console . error ( "Toggle build permission error:" , err );
alert ( "Network error. Please try again." );
}
};
2026-04-08 10:33:22 -05:00
window . toggleUploadPermission = async function ( userId , username , currentCanUpload ) {
const newVal = ! currentCanUpload ;
if ( ! confirm ( ` ${ newVal ? 'Grant' : 'Revoke' } file upload permission for ${ username } ?` )) return ;
try {
const res = await fetch ( `/api/users/ ${ userId } /can-upload-files` , {
method : "PUT" ,
headers : { "Content-Type" : "application/json" },
body : JSON . stringify ({ canUploadFiles : newVal }),
});
const data = await res . json ();
if ( res . ok ) {
await loadUsers ();
} else {
alert ( data . error || "Failed to update upload permission" );
}
} catch ( err ) {
console . error ( "Toggle upload permission error:" , err );
alert ( "Network error. Please try again." );
}
};
2026-03-05 00:04:26 -06:00
getCurrentUser ();
loadUsers ();