feat(auth): enterprise authentication and user management
This commit is contained in:
parent
4bc8b20a21
commit
86a32a942c
36 changed files with 2239 additions and 324 deletions
|
|
@ -1,10 +1,14 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, Response
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.security import create_access_token, verify_password
|
||||
from app.core.security import (
|
||||
create_access_token,
|
||||
get_access_token_expire_seconds,
|
||||
verify_password,
|
||||
)
|
||||
from app.db.database import get_db
|
||||
from app.models.user import User
|
||||
from app.schemas.user import LoginRequest
|
||||
from app.schemas.user import LoginRequest, LoginResponse
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/auth",
|
||||
|
|
@ -12,32 +16,30 @@ router = APIRouter(
|
|||
)
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
def login(
|
||||
login: LoginRequest,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
user = db.query(User).filter(User.username == login.username).first()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="Ungültige Anmeldedaten")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Ungültige Anmeldedaten",
|
||||
)
|
||||
|
||||
if not verify_password(login.password, user.password_hash):
|
||||
raise HTTPException(status_code=401, detail="Ungültige Anmeldedaten")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Ungültige Anmeldedaten",
|
||||
)
|
||||
|
||||
token = create_access_token(user.username)
|
||||
|
||||
response.set_cookie(
|
||||
key="access_token",
|
||||
value=token,
|
||||
httponly=True,
|
||||
secure=False,
|
||||
samesite="lax",
|
||||
max_age=3600,
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Login erfolgreich",
|
||||
"user": user.username,
|
||||
}
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": get_access_token_expire_seconds(),
|
||||
"user": user,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,191 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.security import verify_access_token
|
||||
from app.db.database import get_db
|
||||
from app.models.user import User
|
||||
from app.repositories.user_repository import UserRepository
|
||||
from app.schemas.user import UserCreate, UserResponse
|
||||
from app.schemas.user import UserCreate, UserPasswordUpdate, UserResponse, UserUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/users",
|
||||
tags=["Users"]
|
||||
tags=["Users"],
|
||||
)
|
||||
|
||||
bearer_scheme = HTTPBearer(auto_error=False)
|
||||
|
||||
@router.get("/", response_model=list[UserResponse])
|
||||
def get_users(db: Session = Depends(get_db)):
|
||||
|
||||
def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
if credentials is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Nicht authentifiziert",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
username = verify_access_token(credentials.credentials)
|
||||
user = UserRepository.get_by_username(db, username)
|
||||
|
||||
if user is None or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Nicht authentifiziert",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def require_role(*allowed_roles: str):
|
||||
def dependency(current_user: User = Depends(get_current_user)) -> User:
|
||||
if current_user.role not in allowed_roles:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Keine Berechtigung",
|
||||
)
|
||||
return current_user
|
||||
|
||||
return dependency
|
||||
|
||||
|
||||
def get_user_or_404(db: Session, user_id: int) -> User:
|
||||
db_user = UserRepository.get_by_id(db, user_id)
|
||||
|
||||
if db_user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Benutzer nicht gefunden",
|
||||
)
|
||||
|
||||
return db_user
|
||||
|
||||
|
||||
def raise_conflict(field: str) -> None:
|
||||
if field == "username":
|
||||
detail = "Benutzername ist bereits vergeben"
|
||||
else:
|
||||
detail = "E-Mail ist bereits vergeben"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=detail,
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=list[UserResponse])
|
||||
@router.get("/", response_model=list[UserResponse], include_in_schema=False)
|
||||
def get_users(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
logger.info("users.list", extra={"actor_user_id": current_user.id})
|
||||
return UserRepository.get_all(db)
|
||||
|
||||
|
||||
@router.post("/", response_model=UserResponse)
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
def get_user(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
logger.info(
|
||||
"users.detail",
|
||||
extra={"actor_user_id": current_user.id, "target_user_id": user_id},
|
||||
)
|
||||
return get_user_or_404(db, user_id)
|
||||
|
||||
|
||||
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=UserResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
include_in_schema=False,
|
||||
)
|
||||
def create_user(
|
||||
user: UserCreate,
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
return UserRepository.create(db, user)
|
||||
conflict = UserRepository.find_conflict(
|
||||
db,
|
||||
username=user.username,
|
||||
email=str(user.email),
|
||||
)
|
||||
|
||||
if conflict is not None:
|
||||
raise_conflict(conflict[0])
|
||||
|
||||
logger.info("users.create", extra={"actor_user_id": current_user.id})
|
||||
return UserRepository.create(db, user)
|
||||
|
||||
|
||||
@router.put("/{user_id}", response_model=UserResponse)
|
||||
def update_user(
|
||||
user_id: int,
|
||||
user: UserUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
db_user = get_user_or_404(db, user_id)
|
||||
conflict = UserRepository.find_conflict(
|
||||
db,
|
||||
username=user.username,
|
||||
email=str(user.email),
|
||||
exclude_user_id=user_id,
|
||||
)
|
||||
|
||||
if conflict is not None:
|
||||
raise_conflict(conflict[0])
|
||||
|
||||
logger.info(
|
||||
"users.update",
|
||||
extra={"actor_user_id": current_user.id, "target_user_id": user_id},
|
||||
)
|
||||
return UserRepository.update(db, db_user, user)
|
||||
|
||||
|
||||
@router.put("/{user_id}/password", response_model=UserResponse)
|
||||
def update_user_password(
|
||||
user_id: int,
|
||||
password_update: UserPasswordUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
db_user = get_user_or_404(db, user_id)
|
||||
|
||||
logger.info(
|
||||
"users.password_update",
|
||||
extra={"actor_user_id": current_user.id, "target_user_id": user_id},
|
||||
)
|
||||
return UserRepository.update_password(db, db_user, password_update)
|
||||
|
||||
|
||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_user(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
if current_user.id == user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Der aktuell angemeldete Benutzer darf sich nicht selbst löschen",
|
||||
)
|
||||
|
||||
db_user = get_user_or_404(db, user_id)
|
||||
|
||||
logger.info(
|
||||
"users.delete",
|
||||
extra={"actor_user_id": current_user.id, "target_user_id": user_id},
|
||||
)
|
||||
UserRepository.delete(db, db_user)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ class Settings(BaseSettings):
|
|||
|
||||
app_name: str = "Hermes API"
|
||||
app_version: str = "0.1.0"
|
||||
access_token_expire_minutes: int = 60
|
||||
jwt_issuer: str = "hermes"
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from jose import jwt
|
||||
from fastapi import HTTPException, status
|
||||
from jose import JWTError, jwt
|
||||
from pwdlib import PasswordHash
|
||||
|
||||
from app.core.config import settings
|
||||
|
|
@ -8,7 +9,11 @@ from app.core.config import settings
|
|||
password_hash = PasswordHash.recommended()
|
||||
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60
|
||||
ACCESS_TOKEN_TYPE = "access"
|
||||
|
||||
|
||||
def get_access_token_expire_seconds() -> int:
|
||||
return settings.access_token_expire_minutes * 60
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
|
|
@ -20,11 +25,15 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|||
|
||||
|
||||
def create_access_token(subject: str) -> str:
|
||||
expire = datetime.now(UTC) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
issued_at = datetime.now(UTC)
|
||||
expire = issued_at + timedelta(minutes=settings.access_token_expire_minutes)
|
||||
|
||||
payload = {
|
||||
"sub": subject,
|
||||
"iat": issued_at,
|
||||
"exp": expire,
|
||||
"iss": settings.jwt_issuer,
|
||||
"type": ACCESS_TOKEN_TYPE,
|
||||
}
|
||||
|
||||
return jwt.encode(
|
||||
|
|
@ -32,3 +41,31 @@ def create_access_token(subject: str) -> str:
|
|||
settings.secret_key,
|
||||
algorithm=ALGORITHM,
|
||||
)
|
||||
|
||||
|
||||
def verify_access_token(token: str) -> str:
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Nicht authentifiziert",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings.secret_key,
|
||||
algorithms=[ALGORITHM],
|
||||
issuer=settings.jwt_issuer,
|
||||
)
|
||||
subject = payload.get("sub")
|
||||
token_type = payload.get("type")
|
||||
except JWTError as exc:
|
||||
raise credentials_exception from exc
|
||||
|
||||
if not isinstance(subject, str) or not subject:
|
||||
raise credentials_exception
|
||||
|
||||
if token_type != ACCESS_TOKEN_TYPE:
|
||||
raise credentials_exception
|
||||
|
||||
return subject
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import logging
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi import HTTPException
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import JSONResponse
|
||||
from starlette import status
|
||||
from starlette.requests import Request
|
||||
|
||||
from app.api.auth import router as auth_router
|
||||
from app.api.users import router as users_router
|
||||
|
|
@ -11,21 +18,52 @@ app = FastAPI(
|
|||
description="Backend von Olympus",
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
"https://crm.funktechnik-schubert.de",
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(auth_router)
|
||||
app.include_router(users_router)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_exception_handler(request: Request, exc: HTTPException):
|
||||
logger.warning(
|
||||
"http_exception",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"method": request.method,
|
||||
"status_code": exc.status_code,
|
||||
},
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"detail": exc.detail},
|
||||
headers=exc.headers,
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||
logger.warning(
|
||||
"validation_exception",
|
||||
extra={"path": request.url.path, "method": request.method},
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content={"detail": jsonable_encoder(exc.errors())},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def unhandled_exception_handler(request: Request, exc: Exception):
|
||||
logger.exception(
|
||||
"unhandled_exception",
|
||||
extra={"path": request.url.path, "method": request.method},
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content={"detail": "Interner Serverfehler"},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def root():
|
||||
|
|
@ -40,4 +78,4 @@ def health():
|
|||
return {
|
||||
"status": "healthy",
|
||||
"database": check_database(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean
|
||||
from sqlalchemy import DateTime
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Mapped
|
||||
from sqlalchemy.orm import mapped_column
|
||||
|
||||
|
|
@ -10,6 +15,18 @@ class User(Base):
|
|||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
|
||||
first_name: Mapped[str] = mapped_column(
|
||||
String(100),
|
||||
default="",
|
||||
server_default=""
|
||||
)
|
||||
|
||||
last_name: Mapped[str] = mapped_column(
|
||||
String(100),
|
||||
default="",
|
||||
server_default=""
|
||||
)
|
||||
|
||||
username: Mapped[str] = mapped_column(
|
||||
String(50),
|
||||
unique=True,
|
||||
|
|
@ -23,4 +40,27 @@ class User(Base):
|
|||
|
||||
password_hash: Mapped[str] = mapped_column(
|
||||
String(255)
|
||||
)
|
||||
)
|
||||
|
||||
role: Mapped[str] = mapped_column(
|
||||
String(50),
|
||||
default="user",
|
||||
server_default="user"
|
||||
)
|
||||
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean,
|
||||
default=True,
|
||||
server_default="true"
|
||||
)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now()
|
||||
)
|
||||
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,27 +1,98 @@
|
|||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.user import User
|
||||
from app.schemas.user import UserCreate
|
||||
from app.core.security import hash_password
|
||||
from app.models.user import User
|
||||
from app.schemas.user import UserCreate, UserPasswordUpdate, UserUpdate
|
||||
|
||||
|
||||
class UserRepository:
|
||||
@staticmethod
|
||||
def get_all(db: Session) -> list[User]:
|
||||
return list(db.scalars(select(User).order_by(User.created_at.desc())))
|
||||
|
||||
@staticmethod
|
||||
def get_all(db: Session):
|
||||
return db.query(User).all()
|
||||
def get_by_id(db: Session, user_id: int) -> User | None:
|
||||
return db.get(User, user_id)
|
||||
|
||||
@staticmethod
|
||||
def create(db: Session, user: UserCreate):
|
||||
def get_by_username(db: Session, username: str) -> User | None:
|
||||
return db.scalar(select(User).where(User.username == username))
|
||||
|
||||
@staticmethod
|
||||
def find_conflict(
|
||||
db: Session,
|
||||
*,
|
||||
username: str,
|
||||
email: str,
|
||||
exclude_user_id: int | None = None,
|
||||
) -> tuple[str, User] | None:
|
||||
username_query = select(User).where(User.username == username)
|
||||
email_query = select(User).where(User.email == email)
|
||||
|
||||
if exclude_user_id is not None:
|
||||
username_query = username_query.where(User.id != exclude_user_id)
|
||||
email_query = email_query.where(User.id != exclude_user_id)
|
||||
|
||||
username_user = db.scalar(username_query)
|
||||
if username_user is not None:
|
||||
return ("username", username_user)
|
||||
|
||||
email_user = db.scalar(email_query)
|
||||
if email_user is not None:
|
||||
return ("email", email_user)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def create(db: Session, user: UserCreate) -> User:
|
||||
db_user = User(
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
password_hash=hash_password(user.password),
|
||||
)
|
||||
first_name=user.first_name,
|
||||
last_name=user.last_name,
|
||||
username=user.username,
|
||||
email=str(user.email),
|
||||
role=user.role,
|
||||
is_active=user.is_active,
|
||||
password_hash=hash_password(user.password),
|
||||
)
|
||||
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
|
||||
return db_user
|
||||
return db_user
|
||||
|
||||
@staticmethod
|
||||
def update(db: Session, db_user: User, user: UserUpdate) -> User:
|
||||
db_user.first_name = user.first_name
|
||||
db_user.last_name = user.last_name
|
||||
db_user.username = user.username
|
||||
db_user.email = str(user.email)
|
||||
db_user.role = user.role
|
||||
db_user.is_active = user.is_active
|
||||
|
||||
if user.password:
|
||||
db_user.password_hash = hash_password(user.password)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
|
||||
return db_user
|
||||
|
||||
@staticmethod
|
||||
def update_password(
|
||||
db: Session,
|
||||
db_user: User,
|
||||
password_update: UserPasswordUpdate,
|
||||
) -> User:
|
||||
db_user.password_hash = hash_password(password_update.password)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
|
||||
return db_user
|
||||
|
||||
@staticmethod
|
||||
def delete(db: Session, db_user: User) -> None:
|
||||
db.delete(db_user)
|
||||
db.commit()
|
||||
|
|
|
|||
|
|
@ -1,21 +1,79 @@
|
|||
from pydantic import BaseModel, EmailStr
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
|
||||
|
||||
UserRole = Literal["admin", "manager", "user"]
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
username: str
|
||||
class UserBase(BaseModel):
|
||||
first_name: str = Field(default="", max_length=100)
|
||||
last_name: str = Field(default="", max_length=100)
|
||||
username: str = Field(min_length=3, max_length=50)
|
||||
email: EmailStr
|
||||
password: str
|
||||
role: UserRole = "user"
|
||||
is_active: bool = True
|
||||
|
||||
@field_validator("first_name", "last_name", "username", mode="before")
|
||||
@classmethod
|
||||
def normalize_text(cls, value: object) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
return str(value).strip()
|
||||
|
||||
@field_validator("username")
|
||||
@classmethod
|
||||
def validate_username(cls, value: str) -> str:
|
||||
allowed = value.replace(".", "").replace("_", "").replace("-", "")
|
||||
if not allowed.isalnum():
|
||||
raise ValueError(
|
||||
"Benutzername darf nur Buchstaben, Zahlen, Punkt, Unterstrich und Bindestrich enthalten"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str = Field(min_length=8, max_length=128)
|
||||
|
||||
|
||||
class UserUpdate(UserBase):
|
||||
password: str | None = Field(default=None, max_length=128)
|
||||
|
||||
@field_validator("password")
|
||||
@classmethod
|
||||
def validate_optional_password(cls, value: str | None) -> str | None:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
if len(value) < 8:
|
||||
raise ValueError("Passwort muss mindestens 8 Zeichen lang sein")
|
||||
return value
|
||||
|
||||
|
||||
class UserPasswordUpdate(BaseModel):
|
||||
password: str = Field(min_length=8, max_length=128)
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
password: str
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
id: int
|
||||
first_name: str
|
||||
last_name: str
|
||||
username: str
|
||||
email: EmailStr
|
||||
role: UserRole
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = {
|
||||
"from_attributes": True
|
||||
}
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_in: int
|
||||
user: UserResponse
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue