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.config import settings
|
||||||
from app.core.security import create_access_token
|
from app.core.security import create_access_token
|
||||||
from app.crud.crud_user import user as crud_user
|
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()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -58,6 +58,22 @@ async def list_users(
|
|||||||
return 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)
|
@router.post("/users", response_model=User)
|
||||||
async def create_user(
|
async def create_user(
|
||||||
user_in: UserCreate,
|
user_in: UserCreate,
|
||||||
@@ -91,3 +107,21 @@ async def update_user(
|
|||||||
)
|
)
|
||||||
user = crud_user.update(db, db_obj=user, obj_in=user_in)
|
user = crud_user.update(db, db_obj=user, obj_in=user_in)
|
||||||
return user
|
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
|
# For future use if we add user status
|
||||||
return True
|
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()
|
user = CRUDUser()
|
||||||
@@ -135,6 +135,11 @@ class UserUpdate(BaseModel):
|
|||||||
role: Optional[UserRole] = None
|
role: Optional[UserRole] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePassword(BaseModel):
|
||||||
|
"""Schema for admin-initiated password changes"""
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
class UserInDBBase(UserBase):
|
class UserInDBBase(UserBase):
|
||||||
id: int
|
id: int
|
||||||
|
|
||||||
|
|||||||
112
web/admin.html
112
web/admin.html
@@ -95,6 +95,15 @@
|
|||||||
background-color: #e67e22;
|
background-color: #e67e22;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-info {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info:hover {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
background-color: #e74c3c;
|
background-color: #e74c3c;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -1042,6 +1051,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Success Notification -->
|
||||||
<div id="notification" class="notification"></div>
|
<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 class="btn btn-warning btn-icon" onclick="event.stopPropagation(); openUserEditModal(${user.id})" title="Edit User">
|
||||||
✏️
|
✏️
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-info btn-icon" onclick="event.stopPropagation(); openChangePasswordModal(${user.id}, '${user.username}')" title="Change Password">
|
||||||
|
🔐
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -2591,6 +2637,72 @@
|
|||||||
isNewUser = false;
|
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
|
// User form submission
|
||||||
document.getElementById('user-form').addEventListener('submit', async function(e) {
|
document.getElementById('user-form').addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
Reference in New Issue
Block a user