change-password #1

Merged
jamesp merged 2 commits from change-password into main 2025-12-11 12:46:29 -05:00
4 changed files with 161 additions and 1 deletions

View File

@@ -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()
@@ -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,
@@ -91,3 +107,21 @@ async def update_user(
)
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

View File

@@ -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()

View File

@@ -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

View File

@@ -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 @@
</div>
</div>
<!-- Change User Password Modal -->
<div id="changePasswordModal" class="modal">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h2 id="change-password-title">Change User Password</h2>
<button class="close" onclick="closeChangePasswordModal()">&times;</button>
</div>
<div class="modal-body">
<form id="change-password-form">
<div class="form-group full-width">
<label for="change-password-username" style="font-weight: bold;">Username</label>
<input type="text" id="change-password-username" name="username" readonly style="background-color: #f5f5f5; cursor: not-allowed;">
</div>
<div class="form-group full-width">
<label for="change-password-new">New Password *</label>
<input type="password" id="change-password-new" name="new_password" required>
</div>
<div class="form-group full-width">
<label for="change-password-confirm">Confirm New Password *</label>
<input type="password" id="change-password-confirm" name="confirm_password" required>
</div>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="closeChangePasswordModal()">
Cancel
</button>
<button type="submit" class="btn btn-warning">
🔐 Change Password
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Success Notification -->
<div id="notification" class="notification"></div>
@@ -2518,6 +2561,9 @@
<button class="btn btn-warning btn-icon" onclick="event.stopPropagation(); openUserEditModal(${user.id})" title="Edit User">
✏️
</button>
<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); openChangePasswordModal(${user.id}, '${user.username}')" title="Change Password">
🔐
</button>
</td>
`;
@@ -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();