Initial FastAPI admin auth scaffold
This commit is contained in:
1
app/common/__init__.py
Normal file
1
app/common/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Common helpers."""
|
||||
3
app/common/context.py
Normal file
3
app/common/context.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from contextvars import ContextVar
|
||||
|
||||
current_admin_id: ContextVar[int] = ContextVar("current_admin_id", default=0)
|
||||
1
app/common/repository/__init__.py
Normal file
1
app/common/repository/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Repository layer."""
|
||||
79
app/common/repository/admin_user_repository.py
Normal file
79
app/common/repository/admin_user_repository.py
Normal 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")
|
||||
6
app/common/repository/base_repository.py
Normal file
6
app/common/repository/base_repository.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from app.core.database import Database
|
||||
|
||||
|
||||
class BaseRepository:
|
||||
def __init__(self, database: Database) -> None:
|
||||
self.database = database
|
||||
1
app/common/security/__init__.py
Normal file
1
app/common/security/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Security helpers."""
|
||||
40
app/common/security/password_hasher.py
Normal file
40
app/common/security/password_hasher.py
Normal 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)
|
||||
Reference in New Issue
Block a user