From 9cfd88d8481e30a2f5d2ab4846670292caa705e0 Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Thu, 11 Dec 2025 12:37:11 -0500 Subject: [PATCH 1/2] Initial go --- backend/app/api/endpoints/auth.py | 20 +++++- backend/app/crud/crud_user.py | 9 +++ backend/app/schemas/ppr.py | 5 ++ web/admin.html | 112 ++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 1 deletion(-) diff --git a/backend/app/api/endpoints/auth.py b/backend/app/api/endpoints/auth.py index 12ab494..0cf633d 100644 --- a/backend/app/api/endpoints/auth.py +++ b/backend/app/api/endpoints/auth.py @@ -7,7 +7,7 @@ from app.api.deps import get_db, get_current_admin_user, get_current_read_user from app.core.config import settings from app.core.security import create_access_token from app.crud.crud_user import user as crud_user -from app.schemas.ppr import Token, UserCreate, UserUpdate, User +from app.schemas.ppr import Token, UserCreate, UserUpdate, User, ChangePassword router = APIRouter() @@ -90,4 +90,22 @@ async def update_user( detail="User not found" ) user = crud_user.update(db, db_obj=user, obj_in=user_in) + return user + + +@router.post("/users/{user_id}/change-password", response_model=User) +async def change_user_password( + user_id: int, + password_data: ChangePassword, + db: Session = Depends(get_db), + current_user = Depends(get_current_admin_user) +): + """Change a user's password (admin only)""" + user = crud_user.get(db, user_id=user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + user = crud_user.change_password(db, db_obj=user, new_password=password_data.password) return user \ No newline at end of file diff --git a/backend/app/crud/crud_user.py b/backend/app/crud/crud_user.py index abe59b6..ff7d97e 100644 --- a/backend/app/crud/crud_user.py +++ b/backend/app/crud/crud_user.py @@ -50,5 +50,14 @@ class CRUDUser: # For future use if we add user status return True + def change_password(self, db: Session, db_obj: User, new_password: str) -> User: + """Change a user's password (typically used by admins to reset another user's password)""" + hashed_password = get_password_hash(new_password) + db_obj.password = hashed_password + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + user = CRUDUser() \ No newline at end of file diff --git a/backend/app/schemas/ppr.py b/backend/app/schemas/ppr.py index b699778..1b73bdd 100644 --- a/backend/app/schemas/ppr.py +++ b/backend/app/schemas/ppr.py @@ -135,6 +135,11 @@ class UserUpdate(BaseModel): role: Optional[UserRole] = None +class ChangePassword(BaseModel): + """Schema for admin-initiated password changes""" + password: str + + class UserInDBBase(UserBase): id: int diff --git a/web/admin.html b/web/admin.html index c7695df..54ca3f5 100644 --- a/web/admin.html +++ b/web/admin.html @@ -95,6 +95,15 @@ background-color: #e67e22; } + .btn-info { + background-color: #3498db; + color: white; + } + + .btn-info:hover { + background-color: #2980b9; + } + .btn-danger { background-color: #e74c3c; color: white; @@ -1042,6 +1051,40 @@ + + +
@@ -2518,6 +2561,9 @@ + `; @@ -2591,6 +2637,72 @@ isNewUser = false; } + let currentChangePasswordUserId = null; + + function openChangePasswordModal(userId, username) { + if (!accessToken) return; + + currentChangePasswordUserId = userId; + document.getElementById('change-password-username').value = username; + document.getElementById('change-password-new').value = ''; + document.getElementById('change-password-confirm').value = ''; + document.getElementById('changePasswordModal').style.display = 'block'; + + // Auto-focus on new password field + setTimeout(() => { + document.getElementById('change-password-new').focus(); + }, 100); + } + + function closeChangePasswordModal() { + document.getElementById('changePasswordModal').style.display = 'none'; + currentChangePasswordUserId = null; + } + + // Change password form submission + document.getElementById('change-password-form').addEventListener('submit', async function(e) { + e.preventDefault(); + + if (!accessToken || !currentChangePasswordUserId) return; + + const newPassword = document.getElementById('change-password-new').value.trim(); + const confirmPassword = document.getElementById('change-password-confirm').value.trim(); + + // Validate passwords match + if (newPassword !== confirmPassword) { + showNotification('Passwords do not match!', true); + return; + } + + // Validate password length + if (newPassword.length < 6) { + showNotification('Password must be at least 6 characters long!', true); + return; + } + + try { + const response = await fetch(`/api/v1/auth/users/${currentChangePasswordUserId}/change-password`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}` + }, + body: JSON.stringify({ password: newPassword }) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || 'Failed to change password'); + } + + closeChangePasswordModal(); + showNotification('Password changed successfully!'); + } catch (error) { + console.error('Error changing password:', error); + showNotification(`Error changing password: ${error.message}`, true); + } + }); + // User form submission document.getElementById('user-form').addEventListener('submit', async function(e) { e.preventDefault(); -- 2.49.1 From cc5697eaa04e513d89035cbdcb0e41d42b32fca7 Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Thu, 11 Dec 2025 12:42:27 -0500 Subject: [PATCH 2/2] Add GET for users --- backend/app/api/endpoints/auth.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/backend/app/api/endpoints/auth.py b/backend/app/api/endpoints/auth.py index 0cf633d..b747c5f 100644 --- a/backend/app/api/endpoints/auth.py +++ b/backend/app/api/endpoints/auth.py @@ -58,6 +58,22 @@ async def list_users( return users +@router.get("/users/{user_id}", response_model=User) +async def get_user( + user_id: int, + db: Session = Depends(get_db), + current_user = Depends(get_current_admin_user) +): + """Get a specific user's details (admin only)""" + user = crud_user.get(db, user_id=user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + return user + + @router.post("/users", response_model=User) async def create_user( user_in: UserCreate, -- 2.49.1