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:
parent
3c32064c23
commit
48db28b8fb
6 changed files with 618 additions and 43 deletions
64
crud.py
64
crud.py
|
|
@ -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
80
main.py
|
|
@ -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)):
|
||||
|
||||
|
|
|
|||
15
models.py
15
models.py
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue