- Show both Microsoft SSO and local login options on login page
- Add admin user management: create local users with password
- Add admin password reset for local users only
- Add self-service password change on profile page for local users
- Display auth provider (Local/SSO) in admin user table
- New API endpoints: POST /api/admin/users, POST /api/admin/users/{email}/reset-password, POST /api/users/change-password
- SSO and local auth remain separate (no dual-auth)
- Minimum 8 character password requirement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
1340 lines
No EOL
48 KiB
HTML
1340 lines
No EOL
48 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Admin Dashboard - AgentHub{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container my-5">
|
|
<!-- Header -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h2><i class="fas fa-users-cog me-3"></i>Admin Dashboard</h2>
|
|
<p class="text-muted mb-0">Manage users and AI agents across the system</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-outline-primary" id="refreshBtn">
|
|
<i class="fas fa-sync-alt me-2"></i>Refresh
|
|
</button>
|
|
<button class="btn btn-outline-danger" onclick="logout()">
|
|
<i class="fas fa-sign-out-alt me-2"></i>Logout
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="stat-card">
|
|
<div class="stat-icon">
|
|
<i class="fas fa-users"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3 id="totalUsers">0</h3>
|
|
<p>Total Users</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="stat-card admin-card">
|
|
<div class="stat-icon">
|
|
<i class="fas fa-user-shield"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3 id="adminUsers">0</h3>
|
|
<p>Admin Users</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="stat-card agent-card">
|
|
<div class="stat-icon">
|
|
<i class="fas fa-robot"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3 id="totalAgents">0</h3>
|
|
<p>Total Agents</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="stat-card active-card">
|
|
<div class="stat-icon">
|
|
<i class="fas fa-user-check"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3 id="activeUsers">0</h3>
|
|
<p>Active Users</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs Navigation -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-header bg-white">
|
|
<ul class="nav nav-tabs card-header-tabs" id="adminTabs">
|
|
<li class="nav-item">
|
|
<a class="nav-link active" id="users-tab" data-bs-toggle="tab" href="#users">
|
|
<i class="fas fa-users me-2"></i>Users Management
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" id="agents-tab" data-bs-toggle="tab" href="#agents">
|
|
<i class="fas fa-robot me-2"></i>Agents Management
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="tab-content" id="adminTabsContent">
|
|
<!-- Users Management Tab -->
|
|
<div class="tab-pane fade show active" id="users">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h5 class="mb-0">Users Management</h5>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-success" onclick="showCreateUserModal()">
|
|
<i class="fas fa-user-plus me-2"></i>Create User
|
|
</button>
|
|
<div class="input-group" style="width: 300px;">
|
|
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
|
<input type="text" class="form-control" id="userSearch" placeholder="Search users...">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>User</th>
|
|
<th>Email</th>
|
|
<th>Auth</th>
|
|
<th>Type</th>
|
|
<th>Status</th>
|
|
<th>Agents</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="usersTableBody">
|
|
<tr>
|
|
<td colspan="7" class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Agents Management Tab -->
|
|
<div class="tab-pane fade" id="agents">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h5 class="mb-0">Agent Management</h5>
|
|
<div class="d-flex gap-2">
|
|
<select class="form-select" id="agentStatusFilter" style="width: auto;">
|
|
<option value="">All Statuses</option>
|
|
<option value="Active">Active</option>
|
|
<option value="Development">Development</option>
|
|
<option value="Inactive">Inactive</option>
|
|
<option value="Deprecated">Deprecated</option>
|
|
</select>
|
|
<div class="input-group" style="width: 300px;">
|
|
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
|
<input type="text" class="form-control" id="agentSearch" placeholder="Search agents...">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Agent Name</th>
|
|
<th>Owner</th>
|
|
<th>Version</th>
|
|
<th>Created</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="agentsTableBody">
|
|
<tr>
|
|
<td colspan="5" class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit User Modal -->
|
|
<div class="modal fade" id="editUserModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Edit User</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="editUserForm">
|
|
<input type="hidden" id="editUserId">
|
|
<input type="hidden" id="editUserAuthProvider">
|
|
<div class="mb-3">
|
|
<label for="editUserEmail" class="form-label">Email Address</label>
|
|
<input type="email" class="form-control" id="editUserEmail" readonly>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="editUserFullName" class="form-label">Full Name</label>
|
|
<input type="text" class="form-control" id="editUserFullName">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Authentication Method</label>
|
|
<div id="editUserAuthDisplay">
|
|
<span class="badge bg-secondary">Loading...</span>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="editUserIsActive">
|
|
<label class="form-check-label" for="editUserIsActive">
|
|
Account is active
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="editUserIsAdmin">
|
|
<label class="form-check-label" for="editUserIsAdmin">
|
|
User has admin privileges
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<!-- Password Reset Section (only for local users) -->
|
|
<div class="mb-3" id="passwordResetSection" style="display: none;">
|
|
<hr>
|
|
<label class="form-label">Password Management</label>
|
|
<button type="button" class="btn btn-outline-warning btn-sm" onclick="showResetPasswordModal()">
|
|
<i class="fas fa-key me-2"></i>Reset Password
|
|
</button>
|
|
<div class="form-text">Reset the user's password. They will need to use the new password to log in.</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" form="editUserForm" class="btn btn-primary">Save Changes</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create User Modal -->
|
|
<div class="modal fade" id="createUserModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="fas fa-user-plus me-2"></i>Create New User</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-info small">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
This creates a local user account with email/password authentication.
|
|
For Microsoft SSO users, they will be created automatically on first login.
|
|
</div>
|
|
<form id="createUserForm">
|
|
<div class="mb-3">
|
|
<label for="createUserEmail" class="form-label">Email Address <span class="text-danger">*</span></label>
|
|
<input type="email" class="form-control" id="createUserEmail" required placeholder="user@example.com">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="createUserFullName" class="form-label">Full Name</label>
|
|
<input type="text" class="form-control" id="createUserFullName" placeholder="John Doe">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="createUserPassword" class="form-label">Password <span class="text-danger">*</span></label>
|
|
<input type="password" class="form-control" id="createUserPassword" required minlength="8" placeholder="Minimum 8 characters">
|
|
<div class="form-text">Password must be at least 8 characters.</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="createUserPasswordConfirm" class="form-label">Confirm Password <span class="text-danger">*</span></label>
|
|
<input type="password" class="form-control" id="createUserPasswordConfirm" required minlength="8" placeholder="Confirm password">
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="createUserIsAdmin">
|
|
<label class="form-check-label" for="createUserIsAdmin">
|
|
Grant admin privileges
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" form="createUserForm" class="btn btn-success">
|
|
<i class="fas fa-user-plus me-2"></i>Create User
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reset Password Modal -->
|
|
<div class="modal fade" id="resetPasswordModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="fas fa-key me-2"></i>Reset Password</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-warning small">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
This will reset the password for <strong id="resetPasswordUserEmail"></strong>.
|
|
The user will need to use the new password to log in.
|
|
</div>
|
|
<form id="resetPasswordForm">
|
|
<input type="hidden" id="resetPasswordEmail">
|
|
<div class="mb-3">
|
|
<label for="newPassword" class="form-label">New Password <span class="text-danger">*</span></label>
|
|
<input type="password" class="form-control" id="newPassword" required minlength="8" placeholder="Minimum 8 characters">
|
|
<div class="form-text">Password must be at least 8 characters.</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="newPasswordConfirm" class="form-label">Confirm New Password <span class="text-danger">*</span></label>
|
|
<input type="password" class="form-control" id="newPasswordConfirm" required minlength="8" placeholder="Confirm new password">
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" form="resetPasswordForm" class="btn btn-warning">
|
|
<i class="fas fa-key me-2"></i>Reset Password
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Agent Modal -->
|
|
<div class="modal fade" id="editAgentModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Edit Agent</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="editAgentForm">
|
|
<input type="hidden" id="editAgentId">
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentName" class="form-label">Agent Name *</label>
|
|
<input type="text" class="form-control" id="editAgentName" required>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentTool" class="form-label">Tool *</label>
|
|
<input type="text" class="form-control" id="editAgentTool" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentStatus" class="form-label">Status</label>
|
|
<select class="form-select" id="editAgentStatus">
|
|
<option value="Development">Development</option>
|
|
<option value="Active">Active</option>
|
|
<option value="Inactive">Inactive</option>
|
|
<option value="Deprecated">Deprecated</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentVersion" class="form-label">Version</label>
|
|
<input type="text" class="form-control" id="editAgentVersion">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="editAgentDescription" class="form-label">Description</label>
|
|
<textarea class="form-control" id="editAgentDescription" rows="3" maxlength="300"></textarea>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="editAgentPurpose" class="form-label">Purpose</label>
|
|
<input type="text" class="form-control" id="editAgentPurpose" maxlength="200">
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentLocation" class="form-label">Location</label>
|
|
<input type="text" class="form-control" id="editAgentLocation">
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentDepartment" class="form-label">Department</label>
|
|
<input type="text" class="form-control" id="editAgentDepartment">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="editAgentContact" class="form-label">Contact Person</label>
|
|
<input type="text" class="form-control" id="editAgentContact">
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentTags" class="form-label">Tags</label>
|
|
<input type="text" class="form-control" id="editAgentTags" placeholder="Separate with commas">
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentUserbase" class="form-label">Target Userbase</label>
|
|
<input type="text" class="form-control" id="editAgentUserbase" placeholder="Separate with commas">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="editAgentCapabilities" class="form-label">Capabilities</label>
|
|
<input type="text" class="form-control" id="editAgentCapabilities" placeholder="Separate with commas">
|
|
</div>
|
|
|
|
<div class="mb-3" id="editQualityAuditSection">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="editQualityAuditStatus" onchange="toggleAdminRiskFactor()">
|
|
<label class="form-check-label" for="editQualityAuditStatus">
|
|
<i class="fas fa-certificate me-2"></i>Quality Audit
|
|
<span class="badge bg-warning text-dark ms-2">Admin Only</span>
|
|
</label>
|
|
</div>
|
|
<div class="form-text" id="editQualityAuditNote">
|
|
Check this box if the agent has passed quality audit review.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3" id="editRiskFactorSection" style="display: none;">
|
|
<label for="editRiskFactor" class="form-label">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>Risk Factor
|
|
<span class="badge bg-warning text-dark ms-2">Admin Only</span>
|
|
<span class="text-danger">*</span>
|
|
</label>
|
|
<select class="form-select" id="editRiskFactor">
|
|
<option value="">Select Risk Level</option>
|
|
<option value="1">1 - Very Low Risk</option>
|
|
<option value="2">2 - Low Risk</option>
|
|
<option value="3">3 - Medium Risk</option>
|
|
<option value="4">4 - High Risk</option>
|
|
<option value="5">5 - Very High Risk</option>
|
|
</select>
|
|
<div class="form-text" id="editRiskFactorNote">
|
|
Required when Quality Audit is checked. Select the appropriate risk level for this agent.
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" form="editAgentForm" class="btn btn-primary">Save Changes</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.stat-card {
|
|
background: linear-gradient(135deg, #f3ae3e 0%, #f3ae3e 100%);
|
|
border-radius: 16px;
|
|
padding: 1.5rem;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.admin-card {
|
|
background: linear-gradient(135deg, #f3ae3e 0%, #f3ae3e 100%);
|
|
}
|
|
|
|
.agent-card {
|
|
background: linear-gradient(135deg, #f3ae3e 0%, #f3ae3e 100%);
|
|
}
|
|
|
|
.active-card {
|
|
background: linear-gradient(135deg, #f3ae3e 0%, #f3ae3e 100%);
|
|
}
|
|
|
|
.stat-icon {
|
|
font-size: 2.5rem;
|
|
margin-right: 1rem;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.stat-info h3 {
|
|
margin: 0;
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.stat-info p {
|
|
margin: 0;
|
|
opacity: 0.9;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.user-avatar-sm {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #f3ae3e 0%, #f3ae3e 100%);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.table th {
|
|
border-top: none;
|
|
font-weight: 600;
|
|
color: #2d3748;
|
|
background-color: #f7fafc;
|
|
}
|
|
|
|
/* Column width adjustments for better visibility - Override Bootstrap with higher specificity */
|
|
|
|
/* Users table column widths */
|
|
#users .table-responsive .table.table-hover th:nth-child(1),
|
|
#users .table-responsive .table.table-hover td:nth-child(1) {
|
|
width: 22% !important;
|
|
min-width: 140px !important;
|
|
max-width: 180px !important;
|
|
}
|
|
#users .table-responsive .table.table-hover th:nth-child(2),
|
|
#users .table-responsive .table.table-hover td:nth-child(2) {
|
|
width: 25% !important;
|
|
min-width: 160px !important;
|
|
max-width: 220px !important;
|
|
}
|
|
#users .table-responsive .table.table-hover th:nth-child(3),
|
|
#users .table-responsive .table.table-hover td:nth-child(3) {
|
|
width: 10% !important;
|
|
}
|
|
#users .table-responsive .table.table-hover th:nth-child(4),
|
|
#users .table-responsive .table.table-hover td:nth-child(4) {
|
|
width: 10% !important;
|
|
}
|
|
#users .table-responsive .table.table-hover th:nth-child(5),
|
|
#users .table-responsive .table.table-hover td:nth-child(5) {
|
|
width: 8% !important;
|
|
}
|
|
#users .table-responsive .table.table-hover th:nth-child(6),
|
|
#users .table-responsive .table.table-hover td:nth-child(6) {
|
|
width: 12% !important;
|
|
}
|
|
#users .table-responsive .table.table-hover th:nth-child(7),
|
|
#users .table-responsive .table.table-hover td:nth-child(7) {
|
|
width: 15% !important;
|
|
min-width: 120px !important;
|
|
}
|
|
|
|
/* Agents table column widths (5 columns: Agent Name, Owner, Version, Created, Actions) */
|
|
#agents .table-responsive .table.table-hover th:nth-child(1),
|
|
#agents .table-responsive .table.table-hover td:nth-child(1) {
|
|
width: 40% !important;
|
|
min-width: 220px !important;
|
|
max-width: 320px !important;
|
|
}
|
|
#agents .table-responsive .table.table-hover th:nth-child(2),
|
|
#agents .table-responsive .table.table-hover td:nth-child(2) {
|
|
width: 25% !important;
|
|
min-width: 160px !important;
|
|
max-width: 200px !important;
|
|
}
|
|
#agents .table-responsive .table.table-hover th:nth-child(3),
|
|
#agents .table-responsive .table.table-hover td:nth-child(3) {
|
|
width: 12% !important;
|
|
max-width: 90px !important;
|
|
}
|
|
#agents .table-responsive .table.table-hover th:nth-child(4),
|
|
#agents .table-responsive .table.table-hover td:nth-child(4) {
|
|
width: 13% !important;
|
|
}
|
|
#agents .table-responsive .table.table-hover th:nth-child(5),
|
|
#agents .table-responsive .table.table-hover td:nth-child(5) {
|
|
width: 10% !important;
|
|
min-width: 120px !important;
|
|
}
|
|
|
|
/* Ensure table uses full width and layout is fixed */
|
|
#users .table-responsive .table.table-hover,
|
|
#agents .table-responsive .table.table-hover {
|
|
table-layout: fixed !important;
|
|
width: 100% !important;
|
|
}
|
|
|
|
/* Make sure text wraps in narrow columns */
|
|
#users .table-responsive .table.table-hover td,
|
|
#agents .table-responsive .table.table-hover td {
|
|
word-wrap: break-word !important;
|
|
overflow-wrap: break-word !important;
|
|
}
|
|
|
|
.table-hover tbody tr:hover {
|
|
background-color: rgba(243, 174, 62, 0.05);
|
|
}
|
|
|
|
.card {
|
|
border-radius: 16px;
|
|
border: none;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.card-header {
|
|
background: rgba(243, 174, 62, 0.05);
|
|
border-bottom: 1px solid #e2e8f0;
|
|
border-radius: 16px 16px 0 0 !important;
|
|
}
|
|
|
|
.nav-tabs .nav-link {
|
|
border: none;
|
|
color: #64748b;
|
|
padding: 12px 20px;
|
|
}
|
|
|
|
.nav-tabs .nav-link.active {
|
|
background-color: transparent;
|
|
border-bottom: 2px solid #f3ae3e;
|
|
color: #f3ae3e;
|
|
}
|
|
|
|
.status-Active { background-color: #d4edda; color: #155724; }
|
|
.status-Development { background-color: #fff3cd; color: #856404; }
|
|
.status-Inactive { background-color: #f8d7da; color: #721c24; }
|
|
.status-Deprecated { background-color: #e2e3e5; color: #41464b; }
|
|
|
|
.bg-orange {
|
|
background-color: #fd7e14 !important;
|
|
color: white !important;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.stat-card {
|
|
text-align: center;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.stat-icon {
|
|
margin-right: 0;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.table-responsive {
|
|
font-size: 0.875rem;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
let allUsers = [];
|
|
let allAgents = [];
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadAdminData();
|
|
setupEventListeners();
|
|
});
|
|
|
|
function setupEventListeners() {
|
|
document.getElementById('refreshBtn').addEventListener('click', loadAdminData);
|
|
document.getElementById('userSearch').addEventListener('input', filterUsers);
|
|
document.getElementById('agentSearch').addEventListener('input', filterAgents);
|
|
document.getElementById('agentStatusFilter').addEventListener('change', filterAgents);
|
|
document.getElementById('editUserForm').addEventListener('submit', handleEditUserSubmit);
|
|
document.getElementById('editAgentForm').addEventListener('submit', handleEditAgentSubmit);
|
|
document.getElementById('createUserForm').addEventListener('submit', handleCreateUserSubmit);
|
|
document.getElementById('resetPasswordForm').addEventListener('submit', handleResetPasswordSubmit);
|
|
}
|
|
|
|
async function loadAdminData() {
|
|
try {
|
|
// Load users and agents in parallel
|
|
const [usersResponse, agentsResponse] = await Promise.all([
|
|
fetch('{{ base_path }}/api/admin/users', {
|
|
credentials: 'include'
|
|
}),
|
|
fetch('{{ base_path }}/api/admin/agents', {
|
|
credentials: 'include'
|
|
})
|
|
]);
|
|
|
|
if (usersResponse.status === 401 || agentsResponse.status === 401) {
|
|
window.location.href = '{{ base_path }}/login';
|
|
return;
|
|
}
|
|
|
|
if (usersResponse.status === 403 || agentsResponse.status === 403) {
|
|
alert('Admin access required');
|
|
window.location.href = '{{ base_path }}/dashboard';
|
|
return;
|
|
}
|
|
|
|
if (usersResponse.ok && agentsResponse.ok) {
|
|
allUsers = await usersResponse.json();
|
|
allAgents = await agentsResponse.json();
|
|
|
|
updateStatistics();
|
|
displayUsers(allUsers);
|
|
displayAgents(allAgents);
|
|
} else {
|
|
throw new Error('Failed to load admin data');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading admin data:', error);
|
|
showError('Failed to load admin data');
|
|
}
|
|
}
|
|
|
|
function updateStatistics() {
|
|
document.getElementById('totalUsers').textContent = allUsers.length;
|
|
document.getElementById('adminUsers').textContent = allUsers.filter(u => u.is_admin).length;
|
|
document.getElementById('totalAgents').textContent = allAgents.length;
|
|
document.getElementById('activeUsers').textContent = allUsers.filter(u => u.is_active).length;
|
|
}
|
|
|
|
function displayUsers(users) {
|
|
const tbody = document.getElementById('usersTableBody');
|
|
|
|
if (users.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-4">No users found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
const usersHtml = users.map(user => {
|
|
const userAgents = allAgents.filter(agent => agent.created_by === user.email).length;
|
|
const authProvider = user.auth_provider || 'local';
|
|
const isLocalAuth = authProvider === 'local';
|
|
return `
|
|
<tr>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="user-avatar-sm me-2">
|
|
${(user.full_name || user.email)[0].toUpperCase()}
|
|
</div>
|
|
<div>
|
|
<div class="fw-medium">${user.full_name || 'No Name'}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>${user.email}</td>
|
|
<td>
|
|
<span class="badge ${isLocalAuth ? 'bg-secondary' : 'bg-info'}">
|
|
${isLocalAuth ? 'Local' : 'SSO'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge ${user.is_admin ? 'bg-danger' : 'bg-primary'}">
|
|
${user.is_admin ? 'Admin' : 'User'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge ${user.is_active ? 'bg-success' : 'bg-secondary'}">
|
|
${user.is_active ? 'Active' : 'Inactive'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-info">${userAgents}</span>
|
|
</td>
|
|
<td>
|
|
<button class="btn btn-outline-primary btn-sm me-1" onclick="viewUserDetails('${user.email}')" title="View">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button class="btn btn-outline-warning btn-sm me-1" onclick="editUser('${user.email}')" title="Edit">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="toggleUserStatus('${user.email}')" title="${user.is_active ? 'Deactivate' : 'Activate'}">
|
|
<i class="fas fa-${user.is_active ? 'ban' : 'check'}"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
|
|
tbody.innerHTML = usersHtml;
|
|
}
|
|
|
|
function displayAgents(agents) {
|
|
const tbody = document.getElementById('agentsTableBody');
|
|
|
|
if (agents.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-4">No agents found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
const agentsHtml = agents.map(agent => {
|
|
const owner = allUsers.find(u => u.email === agent.created_by);
|
|
const ownerName = owner?.full_name || agent.agent_contact_person || 'Unknown';
|
|
const ownerEmail = owner?.email || agent.created_by;
|
|
const avatarText = (owner?.full_name || agent.agent_contact_person || 'U')[0].toUpperCase();
|
|
|
|
return `
|
|
<tr>
|
|
<td>
|
|
<div class="fw-medium">${agent.agent_name}</div>
|
|
<small class="text-muted">${agent.agent_description || 'No description'}</small>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="user-avatar-sm me-2">
|
|
${avatarText}
|
|
</div>
|
|
<div>
|
|
<div class="fw-medium">${ownerName}</div>
|
|
<small class="text-muted">${ownerEmail}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>${agent.agent_version || 'N/A'}</td>
|
|
<td>
|
|
<small class="text-muted">${formatDate(agent.agent_created_at)}</small>
|
|
</td>
|
|
<td>
|
|
<button class="btn btn-outline-primary btn-sm me-1" onclick="viewAgentDetails('${agent.agent_id}')">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button class="btn btn-outline-warning btn-sm me-1" onclick="editAgentAdmin('${agent.agent_id}')">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn btn-outline-danger btn-sm" onclick="deleteAgentAdmin('${agent.agent_id}')">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
|
|
tbody.innerHTML = agentsHtml;
|
|
}
|
|
|
|
function filterUsers() {
|
|
const searchTerm = document.getElementById('userSearch').value.toLowerCase();
|
|
const filtered = allUsers.filter(user =>
|
|
(user.full_name || '').toLowerCase().includes(searchTerm) ||
|
|
user.email.toLowerCase().includes(searchTerm)
|
|
);
|
|
displayUsers(filtered);
|
|
}
|
|
|
|
function filterAgents() {
|
|
const searchTerm = document.getElementById('agentSearch').value.toLowerCase();
|
|
const statusFilter = document.getElementById('agentStatusFilter').value;
|
|
|
|
let filtered = allAgents.filter(agent => {
|
|
const matchesSearch = agent.agent_name.toLowerCase().includes(searchTerm) ||
|
|
(agent.agent_description || '').toLowerCase().includes(searchTerm);
|
|
const matchesStatus = !statusFilter || agent.agent_status === statusFilter;
|
|
return matchesSearch && matchesStatus;
|
|
});
|
|
|
|
displayAgents(filtered);
|
|
}
|
|
|
|
function viewUserDetails(email) {
|
|
const user = allUsers.find(u => u.email === email);
|
|
const userAgents = allAgents.filter(agent => agent.created_by === email);
|
|
|
|
alert(`User Details:\nName: ${user.full_name || 'No Name'}\nEmail: ${user.email}\nType: ${user.is_admin ? 'Admin' : 'User'}\nStatus: ${user.is_active ? 'Active' : 'Inactive'}\nAgents Created: ${userAgents.length}`);
|
|
}
|
|
|
|
async function viewAgentDetails(agentId) {
|
|
try {
|
|
console.log('DEBUG: Admin fetching fresh agent details for ID:', agentId);
|
|
|
|
// Fetch fresh agent data from server
|
|
const response = await fetch(`{{ base_path }}/api/agents/${agentId}`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
window.location.href = '{{ base_path }}/login';
|
|
return;
|
|
} else if (response.status === 403) {
|
|
showError('Not authorized to view this agent');
|
|
return;
|
|
} else {
|
|
showError('Failed to load agent details');
|
|
return;
|
|
}
|
|
}
|
|
|
|
const agent = await response.json();
|
|
console.log('DEBUG: Admin fresh agent data loaded:', agent);
|
|
|
|
const qualityAuditText = agent.quality_audit_status ? 'Audited' : 'Not Audited';
|
|
const auditTrail = agent.quality_audit_updated_by_name && agent.quality_audit_updated_at ?
|
|
`\nQuality Audit: ${qualityAuditText} by ${agent.quality_audit_updated_by_name} on ${formatDate(agent.quality_audit_updated_at)}` :
|
|
`\nQuality Audit: ${qualityAuditText}`;
|
|
|
|
const lastEditedText = agent.last_edited_by ? `\nLast Edited By: ${agent.last_edited_by}` : '';
|
|
alert(`Agent Details:\nName: ${agent.agent_name}\nStatus: ${agent.agent_status || 'Development'}\nVersion: ${agent.agent_version || 'N/A'}\nDescription: ${agent.agent_description || 'No description'}\nOwner: ${agent.created_by}${lastEditedText}${auditTrail}`);
|
|
|
|
} catch (error) {
|
|
console.error('Error loading agent details:', error);
|
|
showError('Failed to load agent details. Please try again.');
|
|
}
|
|
}
|
|
|
|
function editUser(email) {
|
|
const user = allUsers.find(u => u.email === email);
|
|
if (!user) return;
|
|
|
|
const authProvider = user.auth_provider || 'local';
|
|
const isLocalAuth = authProvider === 'local';
|
|
|
|
// Populate the edit form
|
|
document.getElementById('editUserId').value = user.email;
|
|
document.getElementById('editUserEmail').value = user.email;
|
|
document.getElementById('editUserFullName').value = user.full_name || '';
|
|
document.getElementById('editUserIsActive').checked = user.is_active;
|
|
document.getElementById('editUserIsAdmin').checked = user.is_admin;
|
|
document.getElementById('editUserAuthProvider').value = authProvider;
|
|
|
|
// Update auth display
|
|
const authDisplay = document.getElementById('editUserAuthDisplay');
|
|
if (isLocalAuth) {
|
|
authDisplay.innerHTML = '<span class="badge bg-secondary"><i class="fas fa-key me-1"></i>Local (Email/Password)</span>';
|
|
} else {
|
|
authDisplay.innerHTML = '<span class="badge bg-info"><i class="fab fa-microsoft me-1"></i>Microsoft SSO</span>';
|
|
}
|
|
|
|
// Show/hide password reset section based on auth provider
|
|
const passwordResetSection = document.getElementById('passwordResetSection');
|
|
passwordResetSection.style.display = isLocalAuth ? 'block' : 'none';
|
|
|
|
// Show the modal
|
|
const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
|
|
modal.show();
|
|
}
|
|
|
|
async function toggleUserStatus(email) {
|
|
const user = allUsers.find(u => u.email === email);
|
|
if (!user) return;
|
|
|
|
const newStatus = !user.is_active;
|
|
const action = newStatus ? 'activate' : 'deactivate';
|
|
|
|
if (!confirm(`Are you sure you want to ${action} this user?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`{{ base_path }}/api/admin/users/${encodeURIComponent(email)}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
full_name: user.full_name,
|
|
is_active: newStatus,
|
|
is_admin: user.is_admin
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
await loadAdminData();
|
|
showSuccess(`User ${action}d successfully`);
|
|
} else {
|
|
const error = await response.json();
|
|
showError(error.detail || `Failed to ${action} user`);
|
|
}
|
|
} catch (error) {
|
|
showError(`Failed to ${action} user`);
|
|
}
|
|
}
|
|
|
|
async function deleteAgentAdmin(agentId) {
|
|
if (!confirm('Are you sure you want to delete this agent? This action cannot be undone.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`{{ base_path }}/api/agents/${agentId}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (response.ok) {
|
|
await loadAdminData();
|
|
showSuccess('Agent deleted successfully');
|
|
} else {
|
|
const error = await response.json();
|
|
showError(error.detail || 'Failed to delete agent');
|
|
}
|
|
} catch (error) {
|
|
showError('Failed to delete agent');
|
|
}
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
if (!dateString) return 'N/A';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString();
|
|
}
|
|
|
|
function getRiskFactorLabel(riskFactor) {
|
|
const labels = {
|
|
1: '1 - Very Low Risk',
|
|
2: '2 - Low Risk',
|
|
3: '3 - Medium Risk',
|
|
4: '4 - High Risk',
|
|
5: '5 - Very High Risk'
|
|
};
|
|
return labels[riskFactor] || 'Not Set';
|
|
}
|
|
|
|
function toggleAdminRiskFactor() {
|
|
const qualityAuditStatus = document.getElementById('editQualityAuditStatus');
|
|
const riskFactorSection = document.getElementById('editRiskFactorSection');
|
|
const riskFactor = document.getElementById('editRiskFactor');
|
|
|
|
if (qualityAuditStatus.checked) {
|
|
riskFactorSection.style.display = 'block';
|
|
riskFactor.required = true;
|
|
} else {
|
|
riskFactorSection.style.display = 'none';
|
|
riskFactor.required = false;
|
|
riskFactor.value = '';
|
|
}
|
|
}
|
|
|
|
function showSuccess(message) {
|
|
alert(message);
|
|
}
|
|
|
|
function showError(message) {
|
|
alert('Error: ' + message);
|
|
}
|
|
|
|
async function handleEditUserSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const email = document.getElementById('editUserId').value;
|
|
const fullName = document.getElementById('editUserFullName').value;
|
|
const isActive = document.getElementById('editUserIsActive').checked;
|
|
const isAdmin = document.getElementById('editUserIsAdmin').checked;
|
|
|
|
try {
|
|
const response = await fetch(`{{ base_path }}/api/admin/users/${encodeURIComponent(email)}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
full_name: fullName,
|
|
is_active: isActive,
|
|
is_admin: isAdmin
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Hide the modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('editUserModal'));
|
|
modal.hide();
|
|
|
|
// Reload data and show success
|
|
await loadAdminData();
|
|
showSuccess('User updated successfully');
|
|
} else {
|
|
const error = await response.json();
|
|
showError(error.detail || 'Failed to update user');
|
|
}
|
|
} catch (error) {
|
|
showError('Failed to update user');
|
|
}
|
|
}
|
|
|
|
async function editAgentAdmin(agentId) {
|
|
try {
|
|
console.log('DEBUG: Admin fetching fresh agent data for editing:', agentId);
|
|
|
|
// Fetch fresh agent data from server
|
|
const response = await fetch(`{{ base_path }}/api/agents/${agentId}`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
window.location.href = '{{ base_path }}/login';
|
|
return;
|
|
} else if (response.status === 403) {
|
|
showError('Not authorized to edit this agent');
|
|
return;
|
|
} else {
|
|
showError('Failed to load agent for editing');
|
|
return;
|
|
}
|
|
}
|
|
|
|
const agent = await response.json();
|
|
console.log('DEBUG: Admin fresh agent data for editing:', agent);
|
|
|
|
// Populate edit form
|
|
document.getElementById('editAgentId').value = agent.agent_id;
|
|
document.getElementById('editAgentName').value = agent.agent_name;
|
|
document.getElementById('editAgentTool').value = agent.agent_tool || '';
|
|
document.getElementById('editAgentStatus').value = agent.agent_status || 'Development';
|
|
document.getElementById('editAgentDescription').value = agent.agent_description || '';
|
|
document.getElementById('editAgentPurpose').value = agent.agent_purpose || '';
|
|
document.getElementById('editAgentVersion').value = agent.agent_version || '';
|
|
document.getElementById('editAgentLocation').value = agent.agent_location || '';
|
|
document.getElementById('editAgentDepartment').value = agent.agent_department || '';
|
|
document.getElementById('editAgentContact').value = agent.agent_contact_person || '';
|
|
document.getElementById('editAgentTags').value = agent.agent_tags ? agent.agent_tags.join(', ') : '';
|
|
document.getElementById('editAgentUserbase').value = agent.agent_userbase ? agent.agent_userbase.join(', ') : '';
|
|
document.getElementById('editAgentCapabilities').value = agent.agent_capabilities ? agent.agent_capabilities.join(', ') : '';
|
|
|
|
// Handle Quality Audit field (admin always has access)
|
|
document.getElementById('editQualityAuditStatus').checked = agent.quality_audit_status || false;
|
|
|
|
// Update the note with audit trail information
|
|
let noteHtml = 'Check this box if the agent has passed quality audit review.';
|
|
if (agent.quality_audit_updated_by_name && agent.quality_audit_updated_at) {
|
|
const action = agent.quality_audit_status ? 'checked' : 'unchecked';
|
|
const date = formatDate(agent.quality_audit_updated_at);
|
|
noteHtml += `<br><br><small class="text-info"><i class="fas fa-history me-1"></i><strong>${agent.quality_audit_updated_by_name}</strong> ${action} Quality Audit on ${date}</small>`;
|
|
} else {
|
|
noteHtml += '<br><br><small class="text-muted"><i class="fas fa-info-circle me-1"></i>No quality audit changes recorded yet.</small>';
|
|
}
|
|
document.getElementById('editQualityAuditNote').innerHTML = noteHtml;
|
|
|
|
// Handle Risk Factor field
|
|
const riskFactorSection = document.getElementById('editRiskFactorSection');
|
|
const riskFactor = document.getElementById('editRiskFactor');
|
|
riskFactor.value = agent.risk_factor || '';
|
|
|
|
if (agent.quality_audit_status) {
|
|
riskFactorSection.style.display = 'block';
|
|
riskFactor.required = true;
|
|
} else {
|
|
riskFactorSection.style.display = 'none';
|
|
riskFactor.required = false;
|
|
}
|
|
|
|
// Show edit modal
|
|
const modal = new bootstrap.Modal(document.getElementById('editAgentModal'));
|
|
modal.show();
|
|
|
|
} catch (error) {
|
|
console.error('Error loading agent for editing:', error);
|
|
showError('Failed to load agent for editing. Please try again.');
|
|
}
|
|
}
|
|
|
|
async function handleEditAgentSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const agentId = document.getElementById('editAgentId').value;
|
|
const agentData = {
|
|
agent_name: document.getElementById('editAgentName').value,
|
|
agent_tool: document.getElementById('editAgentTool').value,
|
|
agent_status: document.getElementById('editAgentStatus').value,
|
|
agent_description: document.getElementById('editAgentDescription').value,
|
|
agent_purpose: document.getElementById('editAgentPurpose').value,
|
|
agent_version: document.getElementById('editAgentVersion').value,
|
|
agent_location: document.getElementById('editAgentLocation').value,
|
|
agent_department: document.getElementById('editAgentDepartment').value,
|
|
agent_contact_person: document.getElementById('editAgentContact').value,
|
|
agent_tags: document.getElementById('editAgentTags').value.split(',').map(s => s.trim()).filter(s => s),
|
|
agent_userbase: document.getElementById('editAgentUserbase').value.split(',').map(s => s.trim()).filter(s => s),
|
|
agent_capabilities: document.getElementById('editAgentCapabilities').value.split(',').map(s => s.trim()).filter(s => s),
|
|
quality_audit_status: document.getElementById('editQualityAuditStatus').checked
|
|
};
|
|
|
|
// Add Risk Factor
|
|
const riskFactorValue = document.getElementById('editRiskFactor').value;
|
|
agentData.risk_factor = riskFactorValue ? parseInt(riskFactorValue) : null;
|
|
|
|
// Validate Risk Factor if Quality Audit is checked
|
|
if (agentData.quality_audit_status && (!agentData.risk_factor || agentData.risk_factor < 1 || agentData.risk_factor > 5)) {
|
|
showError('Risk Factor is required when Quality Audit is checked!');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`{{ base_path }}/api/agents/${agentId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(agentData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Hide the modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('editAgentModal'));
|
|
modal.hide();
|
|
|
|
// Reload data and show success
|
|
await loadAdminData();
|
|
showSuccess('Agent updated successfully');
|
|
} else {
|
|
const error = await response.json();
|
|
showError(error.detail || 'Failed to update agent');
|
|
}
|
|
} catch (error) {
|
|
showError('Failed to update agent');
|
|
}
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem('access_token');
|
|
window.location.href = '{{ base_path }}/';
|
|
}
|
|
|
|
// Create User Functions
|
|
function showCreateUserModal() {
|
|
// Clear the form
|
|
document.getElementById('createUserForm').reset();
|
|
|
|
// Show the modal
|
|
const modal = new bootstrap.Modal(document.getElementById('createUserModal'));
|
|
modal.show();
|
|
}
|
|
|
|
async function handleCreateUserSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const email = document.getElementById('createUserEmail').value;
|
|
const fullName = document.getElementById('createUserFullName').value;
|
|
const password = document.getElementById('createUserPassword').value;
|
|
const passwordConfirm = document.getElementById('createUserPasswordConfirm').value;
|
|
const isAdmin = document.getElementById('createUserIsAdmin').checked;
|
|
|
|
// Validate passwords match
|
|
if (password !== passwordConfirm) {
|
|
showError('Passwords do not match');
|
|
return;
|
|
}
|
|
|
|
// Validate password length
|
|
if (password.length < 8) {
|
|
showError('Password must be at least 8 characters');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('{{ base_path }}/api/admin/users', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
email: email,
|
|
full_name: fullName || null,
|
|
password: password,
|
|
is_admin: isAdmin
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Hide the modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('createUserModal'));
|
|
modal.hide();
|
|
|
|
// Reload data and show success
|
|
await loadAdminData();
|
|
showSuccess('User created successfully');
|
|
} else {
|
|
const error = await response.json();
|
|
showError(error.detail || 'Failed to create user');
|
|
}
|
|
} catch (error) {
|
|
showError('Failed to create user');
|
|
}
|
|
}
|
|
|
|
// Reset Password Functions
|
|
function showResetPasswordModal() {
|
|
const email = document.getElementById('editUserId').value;
|
|
|
|
// Set the email in the reset password modal
|
|
document.getElementById('resetPasswordEmail').value = email;
|
|
document.getElementById('resetPasswordUserEmail').textContent = email;
|
|
|
|
// Clear password fields
|
|
document.getElementById('newPassword').value = '';
|
|
document.getElementById('newPasswordConfirm').value = '';
|
|
|
|
// Hide edit modal and show reset password modal
|
|
const editModal = bootstrap.Modal.getInstance(document.getElementById('editUserModal'));
|
|
editModal.hide();
|
|
|
|
const resetModal = new bootstrap.Modal(document.getElementById('resetPasswordModal'));
|
|
resetModal.show();
|
|
}
|
|
|
|
async function handleResetPasswordSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const email = document.getElementById('resetPasswordEmail').value;
|
|
const newPassword = document.getElementById('newPassword').value;
|
|
const newPasswordConfirm = document.getElementById('newPasswordConfirm').value;
|
|
|
|
// Validate passwords match
|
|
if (newPassword !== newPasswordConfirm) {
|
|
showError('Passwords do not match');
|
|
return;
|
|
}
|
|
|
|
// Validate password length
|
|
if (newPassword.length < 8) {
|
|
showError('Password must be at least 8 characters');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`{{ base_path }}/api/admin/users/${encodeURIComponent(email)}/reset-password`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
new_password: newPassword
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Hide the modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('resetPasswordModal'));
|
|
modal.hide();
|
|
|
|
showSuccess('Password reset successfully');
|
|
} else {
|
|
const error = await response.json();
|
|
showError(error.detail || 'Failed to reset password');
|
|
}
|
|
} catch (error) {
|
|
showError('Failed to reset password');
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %} |