Merge pull request 'main' (#2) from main into local-flights
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
@@ -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,
|
||||
@@ -90,4 +106,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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
|
||||
112
web/admin.html
112
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 @@
|
||||
</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()">×</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();
|
||||
|
||||
Reference in New Issue
Block a user