Initial FastAPI admin auth scaffold

This commit is contained in:
2026-06-05 17:10:30 +08:00
commit 5635da9ea5
65 changed files with 1407 additions and 0 deletions

1
app/core/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Core application wiring."""

39
app/core/config.py Normal file
View File

@@ -0,0 +1,39 @@
from functools import lru_cache
from pathlib import Path
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
populate_by_name=True,
)
app_name: str = Field(default="py_server", alias="APP_NAME")
app_env: str = Field(default="local", alias="APP_ENV")
database_path: Path = Field(default=Path("storage/app.db"), alias="DATABASE_PATH")
admin_jwt_secret: str = Field(
default="dev_admin_secret_change_me",
alias="JWT_ADMIN_SECRET",
)
admin_jwt_ttl: int = Field(default=3600, alias="ADMIN_JWT_TTL")
admin_jwt_refresh_ttl: int = Field(default=7200, alias="ADMIN_JWT_REFRESH_TTL")
jwt_blacklist_ttl: int = Field(default=7201, alias="JWT_BLACKLIST_TTL")
api_jwt_secret: str = Field(default="dev_api_secret_change_me", alias="JWT_SECRET")
api_jwt_ttl: int = Field(default=3600, alias="JWT_TTL")
api_jwt_refresh_ttl: int = Field(default=7200, alias="JWT_REFRESH_TTL")
admin_seed_username: str = Field(default="admin", alias="ADMIN_SEED_USERNAME")
admin_seed_password: str = Field(default="admin", alias="ADMIN_SEED_PASSWORD")
cors_allow_origins: list[str] = Field(default=["*"], alias="CORS_ALLOW_ORIGINS")
@lru_cache
def get_settings() -> Settings:
return Settings()

87
app/core/database.py Normal file
View File

@@ -0,0 +1,87 @@
import asyncio
import sqlite3
from pathlib import Path
from typing import Any
class Database:
def __init__(self, path: Path) -> None:
self.path = path
async def initialize(self) -> None:
await asyncio.to_thread(self._initialize)
async def execute(self, sql: str, params: tuple[Any, ...] = ()) -> int:
return await asyncio.to_thread(self._execute, sql, params)
async def fetchone(
self,
sql: str,
params: tuple[Any, ...] = (),
) -> dict[str, Any] | None:
return await asyncio.to_thread(self._fetchone, sql, params)
async def fetchall(
self,
sql: str,
params: tuple[Any, ...] = (),
) -> list[dict[str, Any]]:
return await asyncio.to_thread(self._fetchall, sql, params)
def _connect(self) -> sqlite3.Connection:
self.path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(self.path)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
return conn
def _initialize(self) -> None:
conn = self._connect()
try:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS admin_user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
user_type TEXT NOT NULL DEFAULT 'admin',
nickname TEXT NOT NULL DEFAULT '',
phone TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL DEFAULT '',
status INTEGER NOT NULL DEFAULT 1,
login_ip TEXT NOT NULL DEFAULT '',
login_time TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
remark TEXT NOT NULL DEFAULT ''
)
"""
)
conn.commit()
finally:
conn.close()
def _execute(self, sql: str, params: tuple[Any, ...]) -> int:
conn = self._connect()
try:
cursor = conn.execute(sql, params)
conn.commit()
return int(cursor.lastrowid or 0)
finally:
conn.close()
def _fetchone(self, sql: str, params: tuple[Any, ...]) -> dict[str, Any] | None:
conn = self._connect()
try:
row = conn.execute(sql, params).fetchone()
return dict(row) if row else None
finally:
conn.close()
def _fetchall(self, sql: str, params: tuple[Any, ...]) -> list[dict[str, Any]]:
conn = self._connect()
try:
rows = conn.execute(sql, params).fetchall()
return [dict(row) for row in rows]
finally:
conn.close()

70
app/core/dependencies.py Normal file
View File

@@ -0,0 +1,70 @@
from functools import lru_cache
from app.common.repository.admin_user_repository import AdminUserRepository
from app.core.config import Settings, get_settings
from app.core.database import Database
from app.lib.jwt.blacklist import InMemoryTokenBlacklist
from app.lib.jwt.factory import JwtFactory
from app.lib.response.admin_return import AdminReturn
from app.service.admin.login.login_service import LoginService
from app.service.admin.login.refresh_service import RefreshService
from app.service.admin.profile.current_user_service import CurrentUserService
from app.service.base_token_service import BaseTokenService
# lru_cache 会缓存函数第一次创建出来的对象。
# 这里用它把 Database、JwtFactory、TokenService 等依赖做成应用级单例,
# 类似 Hyperf 从容器里反复 get 同一个共享服务。
@lru_cache
def get_database() -> Database:
return Database(get_settings().database_path)
@lru_cache
def get_token_blacklist() -> InMemoryTokenBlacklist:
return InMemoryTokenBlacklist()
@lru_cache
def get_jwt_factory() -> JwtFactory:
return JwtFactory(get_settings(), get_token_blacklist())
@lru_cache
def get_token_service() -> BaseTokenService:
return BaseTokenService(get_jwt_factory())
@lru_cache
def get_admin_return() -> AdminReturn:
return AdminReturn()
@lru_cache
def get_admin_user_repository() -> AdminUserRepository:
return AdminUserRepository(get_database())
def get_login_service() -> LoginService:
return LoginService(
get_admin_user_repository(),
get_token_service(),
get_admin_return(),
)
def get_refresh_service() -> RefreshService:
return RefreshService(get_token_service(), get_admin_return())
def get_current_user_service() -> CurrentUserService:
return CurrentUserService(get_admin_user_repository(), get_admin_return())
async def bootstrap_database(settings: Settings | None = None) -> None:
settings = settings or get_settings()
await get_database().initialize()
await get_admin_user_repository().ensure_seed_admin(
settings.admin_seed_username,
settings.admin_seed_password,
)