feat(auth): add DB-backed login, token management, and idempotent auth migration
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
"""Authentication request and response schemas."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from app.schemas.user import UserResponse
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str = Field(min_length=1)
|
||||
password: str = Field(min_length=1)
|
||||
|
||||
@field_validator("username")
|
||||
@classmethod
|
||||
def normalize_username(cls, value: str) -> str:
|
||||
normalized = value.strip()
|
||||
if not normalized:
|
||||
raise ValueError("username은 비어 있을 수 없습니다")
|
||||
return normalized
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: Literal["bearer"] = "bearer"
|
||||
expires_at: datetime | None = None
|
||||
permission: Literal["read", "trade-write", "admin"]
|
||||
user: UserResponse
|
||||
|
||||
|
||||
class CreateClientTokenRequest(BaseModel):
|
||||
name: str = Field(min_length=1)
|
||||
permission: Literal["read", "trade-write", "admin"] | None = None
|
||||
expires_in_days: int | None = Field(default=None, ge=1, le=3650)
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def normalize_name(cls, value: str) -> str:
|
||||
normalized = value.strip()
|
||||
if not normalized:
|
||||
raise ValueError("name은 비어 있을 수 없습니다")
|
||||
return normalized
|
||||
|
||||
|
||||
class AuthTokenResponse(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
name: str
|
||||
token_prefix: str
|
||||
permission: Literal["read", "trade-write", "admin"]
|
||||
token_type: Literal["session", "client"]
|
||||
expires_at: datetime | None = None
|
||||
last_used_at: datetime | None = None
|
||||
revoked_at: datetime | None = None
|
||||
created_at: datetime | None = None
|
||||
|
||||
|
||||
class IssuedClientTokenResponse(BaseModel):
|
||||
token: str
|
||||
token_type: Literal["bearer"] = "bearer"
|
||||
auth_token: AuthTokenResponse
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
@@ -11,13 +12,19 @@ class UserResponse(BaseModel):
|
||||
id: str
|
||||
username: str
|
||||
email: str | None = None
|
||||
permission: Literal["read", "trade-write", "admin"] = "read"
|
||||
is_active: bool = True
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
last_login_at: datetime | None = None
|
||||
|
||||
|
||||
class CreateUserRequest(BaseModel):
|
||||
username: str = Field(min_length=1)
|
||||
email: str | None = None
|
||||
password: str = Field(min_length=8)
|
||||
permission: Literal["read", "trade-write", "admin"] = "read"
|
||||
is_active: bool = True
|
||||
|
||||
@field_validator("username")
|
||||
@classmethod
|
||||
@@ -39,6 +46,9 @@ class CreateUserRequest(BaseModel):
|
||||
class UpdateUserRequest(BaseModel):
|
||||
username: str | None = Field(default=None, min_length=1)
|
||||
email: str | None = None
|
||||
password: str | None = Field(default=None, min_length=8)
|
||||
permission: Literal["read", "trade-write", "admin"] | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
@field_validator("username")
|
||||
@classmethod
|
||||
|
||||
Reference in New Issue
Block a user