Improved user management, added no-registration mode

This commit is contained in:
Xoconoch
2025-08-04 08:32:50 -06:00
parent 6ab603d90a
commit 1da75a3fbc
11 changed files with 660 additions and 28 deletions

View File

@@ -11,6 +11,7 @@ logger = logging.getLogger(__name__)
# Configuration
AUTH_ENABLED = os.getenv("ENABLE_AUTH", "false").lower() in ("true", "1", "yes", "on")
DISABLE_REGISTRATION = os.getenv("DISABLE_REGISTRATION", "false").lower() in ("true", "1", "yes", "on")
JWT_SECRET = os.getenv("JWT_SECRET", "your-super-secret-jwt-key-change-in-production")
JWT_ALGORITHM = "HS256"
JWT_EXPIRATION_HOURS = int(os.getenv("JWT_EXPIRATION_HOURS", "24"))

View File

@@ -4,7 +4,7 @@ from pydantic import BaseModel
from typing import Optional, List
import logging
from . import AUTH_ENABLED, user_manager, token_manager, User
from . import AUTH_ENABLED, DISABLE_REGISTRATION, user_manager, token_manager, User
logger = logging.getLogger(__name__)
@@ -24,6 +24,19 @@ class RegisterRequest(BaseModel):
email: Optional[str] = None
class CreateUserRequest(BaseModel):
"""Admin-only request to create users when registration is disabled"""
username: str
password: str
email: Optional[str] = None
role: str = "user"
class RoleUpdateRequest(BaseModel):
"""Request to update user role"""
role: str
class UserResponse(BaseModel):
username: str
email: Optional[str]
@@ -46,6 +59,7 @@ class AuthStatusResponse(BaseModel):
auth_enabled: bool
authenticated: bool = False
user: Optional[UserResponse] = None
registration_enabled: bool = True
# Dependency to get current user
@@ -101,7 +115,8 @@ async def auth_status(current_user: Optional[User] = Depends(get_current_user)):
return AuthStatusResponse(
auth_enabled=AUTH_ENABLED,
authenticated=current_user is not None,
user=UserResponse(**current_user.to_public_dict()) if current_user else None
user=UserResponse(**current_user.to_public_dict()) if current_user else None,
registration_enabled=AUTH_ENABLED and not DISABLE_REGISTRATION
)
@@ -138,6 +153,12 @@ async def register(request: RegisterRequest):
detail="Authentication is disabled"
)
if DISABLE_REGISTRATION:
raise HTTPException(
status_code=403,
detail="Public registration is disabled. Contact an administrator to create an account."
)
# Check if this is the first user (should be admin)
existing_users = user_manager.list_users()
role = "admin" if len(existing_users) == 0 else "user"
@@ -188,11 +209,11 @@ async def delete_user(username: str, current_user: User = Depends(require_admin)
@router.put("/users/{username}/role", response_model=MessageResponse)
async def update_user_role(
username: str,
role: str,
request: RoleUpdateRequest,
current_user: User = Depends(require_admin)
):
"""Update user role (admin only)"""
if role not in ["user", "admin"]:
if request.role not in ["user", "admin"]:
raise HTTPException(
status_code=400,
detail="Role must be 'user' or 'admin'"
@@ -204,13 +225,42 @@ async def update_user_role(
detail="Cannot change your own role"
)
success, message = user_manager.update_user_role(username, role)
success, message = user_manager.update_user_role(username, request.role)
if not success:
raise HTTPException(status_code=404, detail=message)
return MessageResponse(message=message)
@router.post("/users/create", response_model=MessageResponse)
async def create_user_admin(request: CreateUserRequest, current_user: User = Depends(require_admin)):
"""Create a new user (admin only) - for use when registration is disabled"""
if not AUTH_ENABLED:
raise HTTPException(
status_code=400,
detail="Authentication is disabled"
)
# Validate role
if request.role not in ["user", "admin"]:
raise HTTPException(
status_code=400,
detail="Role must be 'user' or 'admin'"
)
success, message = user_manager.create_user(
username=request.username,
password=request.password,
email=request.email,
role=request.role
)
if not success:
raise HTTPException(status_code=400, detail=message)
return MessageResponse(message=message)
# Profile endpoints
@router.get("/profile", response_model=UserResponse)
async def get_profile(current_user: User = Depends(require_auth)):

View File

@@ -3,8 +3,9 @@ from fastapi.responses import JSONResponse
import json
import logging
import os
from typing import Any
from typing import Any, Optional, List
from pathlib import Path
from pydantic import BaseModel
# Import the centralized config getters that handle file creation and defaults
from routes.utils.celery_config import (
@@ -20,12 +21,36 @@ from routes.utils.watch.manager import (
# Import authentication dependencies
from routes.auth.middleware import require_admin_from_state, User
from routes.auth import user_manager, AUTH_ENABLED, DISABLE_REGISTRATION
logger = logging.getLogger(__name__)
router = APIRouter()
# User management models for config interface
class CreateUserConfigRequest(BaseModel):
"""User creation request for config interface"""
username: str
password: str
email: Optional[str] = None
role: str = "user"
class UserConfigResponse(BaseModel):
"""User response for config interface"""
username: str
email: Optional[str]
role: str
created_at: str
last_login: Optional[str]
class MessageConfigResponse(BaseModel):
"""Message response for config interface"""
message: str
# Flag for config change notifications
config_changed = False
last_config: dict[str, Any] = {}
@@ -390,3 +415,131 @@ async def update_watch_config(request: Request, current_user: User = Depends(req
status_code=500,
detail={"error": "Failed to update watch configuration", "details": str(e)}
)
# User management endpoints for config interface
@router.get("/auth/status")
async def get_auth_status_config(current_user: User = Depends(require_admin_from_state)):
"""Get authentication system status for config interface"""
return {
"auth_enabled": AUTH_ENABLED,
"registration_disabled": DISABLE_REGISTRATION,
"current_user": {
"username": current_user.username,
"role": current_user.role
} if current_user else None
}
@router.get("/users", response_model=List[UserConfigResponse])
async def list_users_config(current_user: User = Depends(require_admin_from_state)):
"""List all users for config interface"""
if not AUTH_ENABLED:
raise HTTPException(
status_code=400,
detail={"error": "Authentication is disabled"}
)
users = user_manager.list_users()
return [UserConfigResponse(**user.to_public_dict()) for user in users]
@router.post("/users", response_model=MessageConfigResponse)
async def create_user_config(request: CreateUserConfigRequest, current_user: User = Depends(require_admin_from_state)):
"""Create a new user through config interface"""
if not AUTH_ENABLED:
raise HTTPException(
status_code=400,
detail={"error": "Authentication is disabled"}
)
# Validate role
if request.role not in ["user", "admin"]:
raise HTTPException(
status_code=400,
detail={"error": "Role must be 'user' or 'admin'"}
)
success, message = user_manager.create_user(
username=request.username,
password=request.password,
email=request.email,
role=request.role
)
if not success:
raise HTTPException(
status_code=400,
detail={"error": message}
)
return MessageConfigResponse(message=message)
@router.delete("/users/{username}", response_model=MessageConfigResponse)
async def delete_user_config(username: str, current_user: User = Depends(require_admin_from_state)):
"""Delete a user through config interface"""
if not AUTH_ENABLED:
raise HTTPException(
status_code=400,
detail={"error": "Authentication is disabled"}
)
if username == current_user.username:
raise HTTPException(
status_code=400,
detail={"error": "Cannot delete your own account"}
)
success, message = user_manager.delete_user(username)
if not success:
raise HTTPException(
status_code=404,
detail={"error": message}
)
return MessageConfigResponse(message=message)
@router.put("/users/{username}/role", response_model=MessageConfigResponse)
async def update_user_role_config(
username: str,
request: Request,
current_user: User = Depends(require_admin_from_state)
):
"""Update user role through config interface"""
if not AUTH_ENABLED:
raise HTTPException(
status_code=400,
detail={"error": "Authentication is disabled"}
)
try:
data = await request.json()
role = data.get("role")
except:
raise HTTPException(
status_code=400,
detail={"error": "Invalid request body"}
)
if role not in ["user", "admin"]:
raise HTTPException(
status_code=400,
detail={"error": "Role must be 'user' or 'admin'"}
)
if username == current_user.username:
raise HTTPException(
status_code=400,
detail={"error": "Cannot change your own role"}
)
success, message = user_manager.update_user_role(username, role)
if not success:
raise HTTPException(
status_code=404,
detail={"error": message}
)
return MessageConfigResponse(message=message)