Chore: moving changes - migrating Desktop from nobara 42 to windows(WSL)

This commit is contained in:
2025-11-05 22:29:28 +02:00
parent c086f64363
commit 934d8fc35f
8 changed files with 826 additions and 63 deletions
+135 -20
View File
@@ -1,6 +1,7 @@
# backend/app/api/v1/users.py
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer
from sqlmodel import Session, select
from app.core.db import get_session
@@ -12,7 +13,9 @@ from app.core.auth import (
get_token_expiration_minutes,
require_admin,
require_write_access,
require_any_access
require_any_access,
verify_password,
send_password_reset_email
)
from app.schemas.models import User
from app.schemas.schemas import (
@@ -20,7 +23,10 @@ from app.schemas.schemas import (
UserUpdate,
UserLogin,
Token,
UserResponse
UserResponse,
UserApprovalUpdate,
PasswordChangeRequest,
EmailVerificationRequest
)
router = APIRouter(prefix="/users", tags=["users"])
@@ -29,11 +35,23 @@ router = APIRouter(prefix="/users", tags=["users"])
@router.post("/login", response_model=Token)
def login(user_credentials: UserLogin, session: Session = Depends(get_session)):
"""Authenticate user and return JWT token with role-based expiration."""
user = authenticate_user(session, user_credentials.username, user_credentials.password)
user, error_message = authenticate_user(session, user_credentials.username, user_credentials.password)
if not user:
error_details = {
"user_not_found": "Username not found",
"invalid_password": "Incorrect password",
"account_pending_approval": "Account pending admin approval. Please contact an administrator."
}
# Use different status codes for different error types
status_code = status.HTTP_401_UNAUTHORIZED
if error_message == "account_pending_approval":
status_code = status.HTTP_403_FORBIDDEN
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
status_code=status_code,
detail=error_details.get(error_message, "Authentication failed"),
headers={"WWW-Authenticate": "Bearer"},
)
@@ -58,7 +76,8 @@ def login(user_credentials: UserLogin, session: Session = Depends(get_session)):
user=UserResponse(
id=user.id,
username=user.username,
role=user.role
role=user.role,
is_approved=user.is_approved
)
)
@@ -69,22 +88,23 @@ def get_current_user_info(current_user: User = Depends(get_current_active_user))
return UserResponse(
id=current_user.id,
username=current_user.username,
role=current_user.role
role=current_user.role,
is_approved=current_user.is_approved
)
@router.get("/", response_model=list[UserResponse])
def get_all_users(
session: Session = Depends(get_session),
current_user: User = Depends(require_any_access),
current_user: User = Depends(require_admin),
skip: int = 0,
limit: int = 100
):
"""Get all users (requires any authenticated role)."""
"""Get all users (requires admin authenticated role)."""
statement = select(User).offset(skip).limit(limit)
users = session.exec(statement).all()
return [
UserResponse(id=user.id, username=user.username, role=user.role)
UserResponse(id=user.id, username=user.username, role=user.role, is_approved=user.is_approved)
for user in users
]
@@ -92,10 +112,9 @@ def get_all_users(
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
def create_user(
user: UserCreate,
session: Session = Depends(get_session),
current_user: User = Depends(require_admin)
session: Session = Depends(get_session)
):
"""Create a new user (admin only)."""
"""Create a new user (public registration - requires admin approval to login)."""
# Check if username already exists
statement = select(User).where(User.username == user.username)
existing_user = session.exec(statement).first()
@@ -105,12 +124,13 @@ def create_user(
detail="Username already registered"
)
# Create new user with hashed password
# Create new user with hashed password (not approved by default)
hashed_password = get_password_hash(user.password)
db_user = User(
username=user.username,
password_hash=hashed_password,
role=user.role
role=user.role,
is_approved=False # Requires admin approval
)
session.add(db_user)
@@ -120,7 +140,8 @@ def create_user(
return UserResponse(
id=db_user.id,
username=db_user.username,
role=db_user.role
role=db_user.role,
is_approved=db_user.is_approved
)
@@ -141,7 +162,8 @@ def get_user(
return UserResponse(
id=user.id,
username=user.username,
role=user.role
role=user.role,
is_approved=user.is_approved
)
@@ -161,8 +183,26 @@ def update_user(
detail="User not found"
)
# Update only provided fields
# Get update data
update_data = user_update.model_dump(exclude_unset=True)
# Check for duplicate username if username is being updated
if "username" in update_data and update_data["username"] != user.username:
statement = select(User).where(User.username == update_data["username"])
existing_user = session.exec(statement).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already exists"
)
# Handle password hashing if password is being updated
if "password" in update_data:
hashed_password = get_password_hash(update_data["password"])
update_data["password_hash"] = hashed_password
del update_data["password"] # Remove plain text password
# Update only provided fields
for key, value in update_data.items():
setattr(user, key, value)
@@ -173,7 +213,38 @@ def update_user(
return UserResponse(
id=user.id,
username=user.username,
role=user.role
role=user.role,
is_approved=user.is_approved
)
@router.put("/{user_id}/approval", response_model=UserResponse)
def update_user_approval(
user_id: int,
approval_update: UserApprovalUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(require_admin)
):
"""Approve or reject a user account (admin only)."""
user = session.get(User, user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# Update approval status
user.is_approved = approval_update.is_approved
session.add(user)
session.commit()
session.refresh(user)
return UserResponse(
id=user.id,
username=user.username,
role=user.role,
is_approved=user.is_approved
)
@@ -201,3 +272,47 @@ def delete_user(
session.delete(user)
session.commit()
return None
@router.put("/me/change-password")
def change_password(
password_change: PasswordChangeRequest,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_active_user)
):
"""Change password (user must know current password)."""
# Verify current password
if not verify_password(password_change.current_password, current_user.password_hash):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Current password is incorrect"
)
# Update to new password
hashed_password = get_password_hash(password_change.new_password)
current_user.password_hash = hashed_password
session.add(current_user)
session.commit()
return {"message": "Password changed successfully"}
@router.post("/request-password-reset")
def request_password_reset(
reset_request: EmailVerificationRequest,
session: Session = Depends(get_session)
):
"""Request password reset via email verification (no database needed)."""
# Find user by username
statement = select(User).where(User.username == reset_request.username)
user = session.exec(statement).first()
# Always return success to prevent username enumeration
if user and user.is_approved:
# Send email with instructions (mock implementation)
send_password_reset_email(reset_request.username, reset_request.email)
return {
"message": "If your username and email are correct, you will receive instructions to reset your password."
}