diff --git a/crud.py b/crud.py
index 04b9b3b..127a69d 100644
--- a/crud.py
+++ b/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})
diff --git a/main.py b/main.py
index c295d34..138d3a5 100644
--- a/main.py
+++ b/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)):
diff --git a/models.py b/models.py
index 1a768ad..bf675eb 100644
--- a/models.py
+++ b/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
diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html
index a72c2ab..3ea73d2 100644
--- a/templates/admin/dashboard.html
+++ b/templates/admin/dashboard.html
@@ -95,10 +95,15 @@
-
Agent Management
-
-
-
+
Users Management
+
+
+
+
+
+
@@ -108,10 +113,10 @@
User
Email
+
Auth
Type
Status
Agents
-
Created
Actions
@@ -188,6 +193,7 @@
+
+
+
+
+
+
Create New User
+
+
+
+
+
+ This creates a local user account with email/password authentication.
+ For Microsoft SSO users, they will be created automatically on first login.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Reset Password
+
+
+
+
+
+ This will reset the password for .
+ The user will need to use the new password to log in.
+
+
+
+
+
+
+
+
@@ -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 = '