Add local user login alongside Microsoft SSO

- 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>
This commit is contained in:
michael 2025-12-05 06:58:45 -06:00
parent 3c32064c23
commit 48db28b8fb
6 changed files with 618 additions and 43 deletions

64
crud.py
View file

@ -329,6 +329,70 @@ async def delete_user(user_id: str):
except:
return False
async def admin_create_local_user(email: str, password: str, full_name: str = None, is_admin: bool = False):
"""Create a new local user (admin only)"""
# Check if user already exists
existing = await get_user_by_email(email)
if existing:
raise ValueError("User with this email already exists")
# Validate password length
if len(password) < 8:
raise ValueError("Password must be at least 8 characters")
return await create_user(
email=email,
password=password,
full_name=full_name,
is_admin=is_admin,
auth_provider="local"
)
async def admin_reset_user_password(user_id: str, new_password: str):
"""Reset password for a local user (admin only)"""
user = await get_user_by_id(user_id)
if not user:
raise ValueError("User not found")
if user.get("auth_provider") != "local":
raise ValueError("Cannot reset password for SSO users")
if len(new_password) < 8:
raise ValueError("Password must be at least 8 characters")
hashed = hash_password(new_password)
result = await users_collection.update_one(
{"_id": ObjectId(user_id)},
{"$set": {"hashed_password": hashed, "updated_at": datetime.utcnow()}}
)
return result.modified_count > 0
async def change_user_password(user_id: str, current_password: str, new_password: str):
"""Change password for current user (requires current password verification)"""
user = await get_user_by_id(user_id)
if not user:
raise ValueError("User not found")
if user.get("auth_provider") != "local":
raise ValueError("Cannot change password for SSO users")
if not user.get("hashed_password"):
raise ValueError("User has no password set")
# Verify current password
if not verify_password(current_password, user["hashed_password"]):
raise ValueError("Current password is incorrect")
if len(new_password) < 8:
raise ValueError("New password must be at least 8 characters")
hashed = hash_password(new_password)
result = await users_collection.update_one(
{"_id": ObjectId(user_id)},
{"$set": {"hashed_password": hashed, "updated_at": datetime.utcnow()}}
)
return result.modified_count > 0
async def get_agent_by_name(agent_name: str):
"""Get agent by exact name match globally"""
return await agents_collection.find_one({"agent_name": agent_name})

80
main.py
View file

@ -933,14 +933,15 @@ async def toggle_admin_view(request: Request, current_user: dict = Depends(get_c
# Admin endpoints
@app.get("/api/admin/users", response_model=List[models.UserResponse])
async def get_all_users(current_user: dict = Depends(require_admin)):
users = await crud.get_all_users()
return [
models.UserResponse(
email=user["email"],
full_name=user.get("full_name"),
is_active=user["is_active"],
is_admin=user["is_admin"]
is_admin=user["is_admin"],
auth_provider=user.get("auth_provider", "local")
) for user in users
]
@ -962,9 +963,82 @@ async def update_user(email: str, user_update: models.UserUpdate, current_user:
email=updated_user["email"],
full_name=updated_user.get("full_name"),
is_active=updated_user["is_active"],
is_admin=updated_user["is_admin"]
is_admin=updated_user["is_admin"],
auth_provider=updated_user.get("auth_provider", "local")
)
@app.post("/api/admin/users", response_model=models.UserResponse)
async def admin_create_user(
user_data: models.AdminUserCreate,
current_user: dict = Depends(require_admin)
):
"""Create a new local user (admin only)"""
try:
created_user = await crud.admin_create_local_user(
email=user_data.email,
password=user_data.password,
full_name=user_data.full_name,
is_admin=user_data.is_admin
)
return models.UserResponse(
email=created_user["email"],
full_name=created_user.get("full_name"),
is_active=created_user["is_active"],
is_admin=created_user["is_admin"],
auth_provider=created_user.get("auth_provider", "local")
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/api/admin/users/{email}/reset-password")
async def admin_reset_password(
email: str,
password_data: models.AdminPasswordReset,
current_user: dict = Depends(require_admin)
):
"""Reset password for a local user (admin only)"""
user = await crud.get_user_by_email(email)
if not user:
raise HTTPException(status_code=404, detail="User not found")
if user.get("auth_provider") != "local":
raise HTTPException(
status_code=400,
detail="Cannot reset password for SSO users. This user authenticates via Microsoft."
)
try:
success = await crud.admin_reset_user_password(str(user["_id"]), password_data.new_password)
if success:
return {"message": "Password reset successfully"}
raise HTTPException(status_code=500, detail="Failed to reset password")
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/api/users/change-password")
async def change_password(
password_data: models.PasswordChange,
current_user: dict = Depends(get_current_user_from_cookie)
):
"""Change password for current logged-in user"""
if current_user.get("auth_provider") != "local":
raise HTTPException(
status_code=400,
detail="Cannot change password for SSO users. Your account uses Microsoft authentication."
)
try:
success = await crud.change_user_password(
str(current_user["_id"]),
password_data.current_password,
password_data.new_password
)
if success:
return {"message": "Password changed successfully"}
raise HTTPException(status_code=500, detail="Failed to change password")
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/api/admin/agents", response_model=List[models.AiAgentResponse])
async def get_all_agents_admin(current_user: dict = Depends(require_admin)):

View file

@ -50,6 +50,7 @@ class UserResponse(BaseModel):
full_name: Optional[str] = None
is_active: bool
is_admin: bool
auth_provider: Optional[str] = "local"
class UserUpdate(BaseModel):
full_name: Optional[str] = None
@ -60,6 +61,20 @@ class Token(BaseModel):
access_token: str
token_type: str = "bearer"
# Admin user management models
class AdminUserCreate(BaseModel):
email: EmailStr
full_name: Optional[str] = None
password: str = Field(..., min_length=8)
is_admin: bool = False
class AdminPasswordReset(BaseModel):
new_password: str = Field(..., min_length=8)
class PasswordChange(BaseModel):
current_password: str
new_password: str = Field(..., min_length=8)
# Agent models for creation and response
class AiAgentCreate(BaseModel):
agent_name: str

View file

@ -95,10 +95,15 @@
<!-- 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">Agent Management</h5>
<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...">
<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>
@ -108,10 +113,10 @@
<tr>
<th>User</th>
<th>Email</th>
<th>Auth</th>
<th>Type</th>
<th>Status</th>
<th>Agents</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
@ -188,6 +193,7 @@
<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>
@ -196,6 +202,12 @@
<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">
@ -212,6 +224,15 @@
</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">
@ -222,6 +243,95 @@
</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">
@ -555,6 +665,8 @@ function setupEventListeners() {
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() {
@ -605,14 +717,16 @@ function updateStatistics() {
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>
@ -622,11 +736,15 @@ function displayUsers(users) {
</div>
<div>
<div class="fw-medium">${user.full_name || 'No Name'}</div>
<small class="text-muted">ID: ${user.email}</small>
</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'}
@ -641,23 +759,20 @@ function displayUsers(users) {
<span class="badge bg-info">${userAgents}</span>
</td>
<td>
<small class="text-muted">Recently</small>
</td>
<td>
<button class="btn btn-outline-primary btn-sm me-1" onclick="viewUserDetails('${user.email}')">
<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}')">
<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 me-1" onclick="toggleUserStatus('${user.email}')">
<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;
}
@ -786,14 +901,30 @@ async function viewAgentDetails(agentId) {
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();
@ -1076,5 +1207,134 @@ 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 %}

View file

@ -16,6 +16,20 @@
<p class="text-muted">Sign in to your account</p>
</div>
{% if error %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-circle me-2"></i>{{ error }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% if success %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle me-2"></i>{{ success }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% if msal_enabled %}
<!-- Microsoft Sign-in Button -->
<div class="d-grid mb-4">
@ -27,45 +41,44 @@
</button>
</div>
{% if show_local_login %}
<!-- Divider -->
<div class="auth-divider mb-4">
<span class="auth-divider-text">or continue with email</span>
<span class="auth-divider-text">or sign in with email</span>
</div>
{% endif %}
{% endif %}
{% if show_local_login %}
<!-- Local Authentication Section -->
<div class="local-auth-section">
{% if msal_enabled %}
<p class="text-muted small mb-3 text-center">
<i class="fas fa-shield-alt me-1"></i>
Administrator & Development Access
<i class="fas fa-user me-1"></i>
For partner organizations without Microsoft SSO
</p>
{% endif %}
<form method="POST" id="loginForm">
<div class="mb-4">
<label for="userEmail" class="form-label">
<i class="fas fa-envelope me-2"></i>Email Address
</label>
<input type="email"
name="email"
class="form-control form-control-lg"
id="userEmail"
<input type="email"
name="email"
class="form-control form-control-lg"
id="userEmail"
placeholder="Enter your email"
required>
</div>
<div class="mb-4">
<label for="userPassword" class="form-label">
<i class="fas fa-lock me-2"></i>Password
</label>
<div class="password-input-group">
<input type="password"
name="password"
class="form-control form-control-lg"
<input type="password"
name="password"
class="form-control form-control-lg"
id="userPassword"
placeholder="Enter your password"
placeholder="Enter your password"
required>
<button type="button" class="password-toggle" onclick="togglePassword('userPassword')">
<i class="fas fa-eye" id="passwordIcon"></i>
@ -87,15 +100,8 @@
<i class="fas fa-sign-in-alt me-2"></i>Sign In
</button>
</div>
<div class="text-center">
<p class="text-muted small">
<em>Local login is for administrators and development use only</em>
</p>
</div>
</form>
</div> <!-- End local-auth-section -->
{% endif %}
</div>
</div>
</div>
@ -157,6 +163,38 @@
border-color: var(--primary-color);
}
.auth-divider {
display: flex;
align-items: center;
text-align: center;
}
.auth-divider::before,
.auth-divider::after {
content: '';
flex: 1;
border-bottom: 1px solid #dee2e6;
}
.auth-divider-text {
padding: 0 1rem;
color: #6c757d;
font-size: 0.875rem;
}
.btn-microsoft {
background-color: #fff;
border: 1px solid #dee2e6;
color: #333;
display: flex;
align-items: center;
justify-content: center;
}
.btn-microsoft:hover {
background-color: #f8f9fa;
border-color: #c1c1c1;
}
@media (max-width: 576px) {
.card-body {

View file

@ -61,6 +61,18 @@
<span class="info-value">{{ current_user.created_at.strftime('%B %Y') if current_user.created_at else 'Unknown' }}</span>
</div>
</div>
<div class="col-md-6">
<div class="info-item">
<span class="info-label">Authentication Method</span>
<span class="info-value">
{% if current_user.auth_provider == 'azure_ad' %}
<span class="badge bg-info"><i class="fab fa-microsoft me-1"></i>Microsoft SSO</span>
{% else %}
<span class="badge bg-secondary"><i class="fas fa-key me-1"></i>Local (Email/Password)</span>
{% endif %}
</span>
</div>
</div>
</div>
</div>
</div>
@ -69,6 +81,55 @@
</div>
</div>
<!-- Password Change Section (only for local users) -->
{% if current_user.auth_provider != 'azure_ad' %}
<div class="row mb-4">
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="fas fa-key me-2"></i>Change Password</h5>
</div>
<div class="card-body">
<form id="changePasswordForm">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="currentPassword" class="form-label">Current Password <span class="text-danger">*</span></label>
<input type="password" class="form-control" id="currentPassword" required placeholder="Enter your current password">
</div>
<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="confirmNewPassword" class="form-label">Confirm New Password <span class="text-danger">*</span></label>
<input type="password" class="form-control" id="confirmNewPassword" required minlength="8" placeholder="Confirm your new password">
</div>
<div id="passwordChangeAlert" class="alert d-none mb-3" role="alert"></div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Update Password
</button>
</div>
<div class="col-md-6">
<div class="alert alert-info">
<h6><i class="fas fa-info-circle me-2"></i>Password Tips</h6>
<ul class="mb-0 small">
<li>Use at least 8 characters</li>
<li>Include a mix of letters, numbers, and symbols</li>
<li>Avoid using personal information</li>
<li>Don't reuse passwords from other sites</li>
</ul>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Account Overview -->
<div class="row mb-4">
<div class="col-12">
@ -230,8 +291,71 @@ let userAgents = [];
document.addEventListener('DOMContentLoaded', function() {
loadUserStats();
loadRecentActivity();
// Set up password change form handler if it exists
const changePasswordForm = document.getElementById('changePasswordForm');
if (changePasswordForm) {
changePasswordForm.addEventListener('submit', handlePasswordChange);
}
});
async function handlePasswordChange(e) {
e.preventDefault();
const currentPassword = document.getElementById('currentPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmNewPassword = document.getElementById('confirmNewPassword').value;
const alertDiv = document.getElementById('passwordChangeAlert');
// Reset alert
alertDiv.classList.add('d-none');
alertDiv.classList.remove('alert-success', 'alert-danger');
// Validate passwords match
if (newPassword !== confirmNewPassword) {
showPasswordAlert('New passwords do not match', 'danger');
return;
}
// Validate password length
if (newPassword.length < 8) {
showPasswordAlert('New password must be at least 8 characters', 'danger');
return;
}
try {
const response = await fetch('{{ base_path }}/api/users/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword
})
});
if (response.ok) {
showPasswordAlert('Password changed successfully!', 'success');
// Clear the form
document.getElementById('changePasswordForm').reset();
} else {
const error = await response.json();
showPasswordAlert(error.detail || 'Failed to change password', 'danger');
}
} catch (error) {
showPasswordAlert('Failed to change password. Please try again.', 'danger');
}
}
function showPasswordAlert(message, type) {
const alertDiv = document.getElementById('passwordChangeAlert');
alertDiv.textContent = message;
alertDiv.classList.remove('d-none', 'alert-success', 'alert-danger');
alertDiv.classList.add(`alert-${type}`);
}
async function loadUserStats() {
try {
const response = await fetch('{{ base_path }}/api/agents', {