init auth

This commit is contained in:
xds
2026-02-08 17:36:40 +03:00
parent 31893414eb
commit b704707abc
34 changed files with 527 additions and 16 deletions

BIN
api/endpoints/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

96
api/endpoints/admin.py Normal file
View File

@@ -0,0 +1,96 @@
from typing import Annotated, List
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel
from repos.user_repo import UsersRepo, UserStatus
from utils.security import verify_password, create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES, ALGORITHM, SECRET_KEY
from jose import JWTError, jwt
from starlette.requests import Request
router = APIRouter(prefix="/api/admin", tags=["admin"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
from api.endpoints.auth import get_users_repo
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)], repo: Annotated[UsersRepo, Depends(get_users_repo)]):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = await repo.get_user_by_username(username)
if user is None:
raise credentials_exception
return user
async def get_current_admin(user: Annotated[dict, Depends(get_current_user)]):
if not user.get("is_admin"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
return user
class UserResponse(BaseModel):
username: str
full_name: str | None = None
status: str
created_at: str | None = None
is_admin: bool
class Config:
from_attributes = True
@router.get("/approvals", response_model=List[UserResponse])
async def list_pending_users(
admin: Annotated[dict, Depends(get_current_admin)],
repo: Annotated[UsersRepo, Depends(get_users_repo)]
):
users = await repo.get_pending_users()
# Pydantic conversion handles the list of dicts
return [
UserResponse(
username=u["username"],
full_name=u.get("full_name"),
status=u["status"],
created_at=str(u.get("created_at")),
is_admin=u.get("is_admin", False)
) for u in users
]
@router.post("/approve/{username}")
async def approve_user(
username: str,
admin: Annotated[dict, Depends(get_current_admin)],
repo: Annotated[UsersRepo, Depends(get_users_repo)]
):
user = await repo.get_user_by_username(username)
if not user:
raise HTTPException(status_code=404, detail="User not found")
await repo.approve_user(username)
return {"message": f"User {username} approved"}
@router.post("/deny/{username}")
async def deny_user(
username: str,
admin: Annotated[dict, Depends(get_current_admin)],
repo: Annotated[UsersRepo, Depends(get_users_repo)]
):
user = await repo.get_user_by_username(username)
if not user:
raise HTTPException(status_code=404, detail="User not found")
await repo.deny_user(username)
return {"message": f"User {username} denied"}

View File

@@ -18,6 +18,8 @@ import logging
logger = logging.getLogger(__name__)
from api.endpoints.auth import get_current_user
router = APIRouter(prefix="/api/assets", tags=["Assets"])
@@ -49,7 +51,7 @@ async def get_asset(
return Response(content=content, media_type=media_type, headers=headers)
@router.delete("/{asset_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete("/{asset_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_user)])
async def delete_asset(
asset_id: str,
dao: DAO = Depends(get_dao)
@@ -65,7 +67,7 @@ async def delete_asset(
return None
@router.get("")
@router.get("", dependencies=[Depends(get_current_user)])
async def get_assets(request: Request, dao: DAO = Depends(get_dao), type: Optional[str] = None, limit: int = 10, offset: int = 0) -> AssetsResponse:
logger.info(f"get_assets called. Limit: {limit}, Offset: {offset}")
assets = await dao.assets.get_assets(type, limit, offset)
@@ -82,7 +84,7 @@ async def get_assets(request: Request, dao: DAO = Depends(get_dao), type: Option
@router.post("/upload", response_model=AssetResponse, status_code=status.HTTP_201_CREATED)
@router.post("/upload", response_model=AssetResponse, status_code=status.HTTP_201_CREATED, dependencies=[Depends(get_current_user)])
async def upload_asset(
file: UploadFile = File(...),
linked_char_id: Optional[str] = Form(None),
@@ -127,7 +129,7 @@ async def upload_asset(
)
@router.post("/regenerate_thumbnails")
@router.post("/regenerate_thumbnails", dependencies=[Depends(get_current_user)])
async def regenerate_thumbnails(dao: DAO = Depends(get_dao)):
"""
Regenerates thumbnails for all existing image assets that don't have one.
@@ -161,7 +163,7 @@ async def regenerate_thumbnails(dao: DAO = Depends(get_dao)):
return {"status": "completed", "processed": count, "updated": updated}
@router.post("/migrate_to_minio")
@router.post("/migrate_to_minio", dependencies=[Depends(get_current_user)])
async def migrate_to_minio(dao: DAO = Depends(get_dao)):
"""
Migrates assets from MongoDB to MinIO.

122
api/endpoints/auth.py Normal file
View File

@@ -0,0 +1,122 @@
from datetime import timedelta
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from jose import JWTError, jwt
from repos.user_repo import UsersRepo, UserStatus
from utils.security import verify_password, create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES, ALGORITHM, SECRET_KEY
from starlette.requests import Request
router = APIRouter(prefix="/api/auth", tags=["auth"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
async def get_users_repo(request: Request) -> UsersRepo:
if not hasattr(request.app.state, "users_repo"):
raise HTTPException(status_code=500, detail="Users repo not initialized")
return request.app.state.users_repo
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)], repo: Annotated[UsersRepo, Depends(get_users_repo)]):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = await repo.get_user_by_username(username)
if user is None:
raise credentials_exception
return user
async def get_current_admin(user: Annotated[dict, Depends(get_current_user)]):
if not user.get("is_admin"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
return user
class UserRegister(BaseModel):
username: str
password: str
full_name: str | None = None
class Token(BaseModel):
access_token: str
token_type: str
class UserResponse(BaseModel):
username: str
full_name: str | None = None
status: str
is_admin: bool = False
@router.get("/me", response_model=UserResponse)
async def read_users_me(current_user: Annotated[dict, Depends(get_current_user)]):
return current_user
@router.post("/register")
async def register(user_data: UserRegister, repo: Annotated[UsersRepo, Depends(get_users_repo)]):
try:
await repo.create_user(
username=user_data.username,
password=user_data.password,
full_name=user_data.full_name
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return {"message": "Registration successful. Please wait for administrator approval."}
@router.post("/token", response_model=Token)
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
repo: Annotated[UsersRepo, Depends(get_users_repo)]
):
user = await repo.get_user_by_username(form_data.username)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Проверяем пароль
if not verify_password(form_data.password, user["hashed_password"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Проверка статуса
if user.get("status") != UserStatus.ALLOWED:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Account is not approved yet. Please contact administrator.",
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user["username"]}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}

View File

@@ -16,7 +16,9 @@ import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/characters", tags=["Characters"])
from api.endpoints.auth import get_current_user
router = APIRouter(prefix="/api/characters", tags=["Characters"], dependencies=[Depends(get_current_user)])
@router.get("/", response_model=List[Character])

View File

@@ -11,11 +11,15 @@ from api.models.GenerationRequest import GenerationResponse, GenerationRequest,
from api.service.generation_service import GenerationService
from models.Generation import Generation
from starlette import status
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix='/api/generations', tags=["Generation"])
from api.endpoints.auth import get_current_user
router = APIRouter(prefix='/api/generations', tags=["Generation"], dependencies=[Depends(get_current_user)])
@router.post("/prompt-assistant", response_model=PromptResponse)
@@ -69,3 +73,12 @@ async def get_generation(generation_id: str,
async def get_running_generations(request: Request,
generation_service: GenerationService = Depends(get_generation_service)):
return await generation_service.get_running_generations()
@router.delete("/{generation_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_user)])
async def delete_generation(generation_id: str, generation_service: GenerationService = Depends(get_generation_service)):
logger.info(f"delete_generation called for ID: {generation_id}")
deleted = await generation_service.delete_generation(generation_id)
if not deleted:
raise HTTPException(status_code=404, detail="Generation not found")
return None