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/common/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Common helpers."""

3
app/common/context.py Normal file
View File

@@ -0,0 +1,3 @@
from contextvars import ContextVar
current_admin_id: ContextVar[int] = ContextVar("current_admin_id", default=0)

View File

@@ -0,0 +1 @@
"""Repository layer."""

View File

@@ -0,0 +1,79 @@
from datetime import UTC, datetime
from app.common.repository.base_repository import BaseRepository
from app.common.security.password_hasher import hash_password
from app.constants.model.admin_user.admin_user_status_code import AdminUserStatusCode
from app.model.admin_user import AdminUser
class AdminUserRepository(BaseRepository):
async def find_by_username(self, username: str) -> AdminUser | None:
row = await self.database.fetchone(
"""
SELECT id, username, password, user_type, nickname, phone, email,
status, login_ip, login_time, remark
FROM admin_user
WHERE username = ?
LIMIT 1
""",
(username,),
)
return AdminUser.from_row(row) if row else None
async def find_by_id(self, admin_id: int) -> AdminUser | None:
row = await self.database.fetchone(
"""
SELECT id, username, password, user_type, nickname, phone, email,
status, login_ip, login_time, remark
FROM admin_user
WHERE id = ?
LIMIT 1
""",
(admin_id,),
)
return AdminUser.from_row(row) if row else None
async def record_login(self, admin_id: int, login_ip: str) -> None:
now = self._now()
await self.database.execute(
"""
UPDATE admin_user
SET login_ip = ?, login_time = ?, updated_at = ?
WHERE id = ?
""",
(login_ip, now, now, admin_id),
)
async def ensure_seed_admin(self, username: str, password: str) -> None:
exists = await self.find_by_username(username)
if exists is not None:
return
now = self._now()
await self.database.execute(
"""
INSERT INTO admin_user (
username, password, user_type, nickname, phone, email, status,
login_ip, login_time, created_at, updated_at, remark
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
username,
hash_password(password),
"SuperAdmin",
"Super Admin",
"",
"",
int(AdminUserStatusCode.NORMAL),
"",
"",
now,
now,
"seeded by application startup",
),
)
@staticmethod
def _now() -> str:
return datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S")

View File

@@ -0,0 +1,6 @@
from app.core.database import Database
class BaseRepository:
def __init__(self, database: Database) -> None:
self.database = database

View File

@@ -0,0 +1 @@
"""Security helpers."""

View File

@@ -0,0 +1,40 @@
import base64
import hashlib
import hmac
import secrets
ALGORITHM = "pbkdf2_sha256"
ITERATIONS = 390_000
def _b64encode(raw: bytes) -> str:
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
def _b64decode(value: str) -> bytes:
padding = "=" * (-len(value) % 4)
return base64.urlsafe_b64decode(value + padding)
def hash_password(password: str, iterations: int = ITERATIONS) -> str:
salt = secrets.token_bytes(16)
digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations)
return f"{ALGORITHM}${iterations}${_b64encode(salt)}${_b64encode(digest)}"
def verify_password(password: str, stored_hash: str) -> bool:
try:
algorithm, iterations, salt, expected = stored_hash.split("$", 3)
except ValueError:
return hmac.compare_digest(password, stored_hash)
if algorithm != ALGORITHM:
return False
digest = hashlib.pbkdf2_hmac(
"sha256",
password.encode("utf-8"),
_b64decode(salt),
int(iterations),
)
return hmac.compare_digest(_b64encode(digest), expected)