chore: initialize insar management system v2

This commit is contained in:
2026-04-14 13:16:01 +08:00
commit ecc72ec9cd
361 changed files with 2142522 additions and 0 deletions
+239
View File
@@ -0,0 +1,239 @@
import os
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from .auth_utils import (
generate_session_token,
hash_password,
hash_session_token,
normalize_username,
validate_password,
validate_username,
verify_password,
)
from .config import read_bool_env, read_int_env, settings
from .models import AuthAuditLogORM, AuthSessionORM, AuthUserORM
ROLE_ADMIN = "admin"
ROLE_VIEWER = "viewer"
VALID_ROLES = {ROLE_ADMIN, ROLE_VIEWER}
SESSION_COOKIE_NAME = settings.AUTH_SESSION_COOKIE_NAME
SESSION_TTL_HOURS = read_int_env("SESSION_TTL_HOURS", 12)
COOKIE_SECURE = read_bool_env("AUTH_COOKIE_SECURE", True)
COOKIE_SAMESITE = (settings.AUTH_COOKIE_SAMESITE or "lax").strip().lower()
if COOKIE_SAMESITE not in {"lax", "strict", "none"}:
COOKIE_SAMESITE = "lax"
def get_cookie_options() -> Dict[str, Any]:
max_age = SESSION_TTL_HOURS * 3600
return {
"key": SESSION_COOKIE_NAME,
"httponly": True,
"secure": COOKIE_SECURE,
"samesite": COOKIE_SAMESITE,
"max_age": max_age,
"path": "/",
}
def _normalize_role(role: Optional[str]) -> str:
value = (role or ROLE_VIEWER).strip().lower()
if value not in VALID_ROLES:
raise ValueError(f"Invalid role: {role}")
return value
async def get_user_by_username(db: AsyncSession, username: str) -> Optional[AuthUserORM]:
normalized = normalize_username(username)
if not normalized:
return None
result = await db.execute(
select(AuthUserORM).where(AuthUserORM.username == normalized)
)
return result.scalar_one_or_none()
async def list_users(db: AsyncSession) -> List[AuthUserORM]:
result = await db.execute(
select(AuthUserORM).order_by(AuthUserORM.created_at.desc(), AuthUserORM.id.desc())
)
return result.scalars().all()
async def create_user(
db: AsyncSession,
username: str,
password: str,
role: str = ROLE_VIEWER,
is_active: bool = True,
created_by: Optional[str] = None,
) -> AuthUserORM:
ok, reason = validate_username(username)
if not ok:
raise ValueError(reason)
ok, reason = validate_password(password)
if not ok:
raise ValueError(reason)
normalized = normalize_username(username)
normalized_role = _normalize_role(role)
exists = await get_user_by_username(db, normalized)
if exists:
raise ValueError("用户名已存在。")
user = AuthUserORM(
username=normalized,
password_hash=hash_password(password),
role=normalized_role,
is_active=bool(is_active),
created_by=created_by,
)
db.add(user)
await db.flush()
return user
async def update_user(
db: AsyncSession,
user: AuthUserORM,
role: Optional[str] = None,
is_active: Optional[bool] = None,
password: Optional[str] = None,
) -> AuthUserORM:
if role is not None:
user.role = _normalize_role(role)
if is_active is not None:
user.is_active = bool(is_active)
if password is not None:
ok, reason = validate_password(password)
if not ok:
raise ValueError(reason)
user.password_hash = hash_password(password)
await db.flush()
return user
async def authenticate_user(
db: AsyncSession,
username: str,
password: str,
) -> Optional[AuthUserORM]:
user = await get_user_by_username(db, username)
if not user or not user.is_active:
return None
if not verify_password(password, user.password_hash):
return None
return user
async def create_session(
db: AsyncSession,
user: AuthUserORM,
ip_address: Optional[str],
user_agent: Optional[str],
) -> Tuple[str, datetime]:
token = generate_session_token()
expires_at = datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(hours=SESSION_TTL_HOURS)
session = AuthSessionORM(
token_hash=hash_session_token(token),
user_id=user.id,
expires_at=expires_at,
ip_address=(ip_address or "")[:64] or None,
user_agent=(user_agent or "")[:512] or None,
)
user.last_login_at = datetime.now(timezone.utc).replace(tzinfo=None)
db.add(session)
await db.flush()
return token, expires_at
async def get_user_by_session_token(db: AsyncSession, token: Optional[str]) -> Optional[AuthUserORM]:
if not token:
return None
token_hash = hash_session_token(token)
now = datetime.now(timezone.utc).replace(tzinfo=None)
result = await db.execute(
select(AuthUserORM)
.join(AuthSessionORM, AuthSessionORM.user_id == AuthUserORM.id)
.where(
AuthSessionORM.token_hash == token_hash,
AuthSessionORM.is_revoked == False,
AuthSessionORM.expires_at > now,
AuthUserORM.is_active == True,
)
)
return result.scalar_one_or_none()
async def revoke_session(db: AsyncSession, token: Optional[str]) -> bool:
if not token:
return False
token_hash = hash_session_token(token)
result = await db.execute(
select(AuthSessionORM).where(
AuthSessionORM.token_hash == token_hash,
AuthSessionORM.is_revoked == False,
)
)
session = result.scalar_one_or_none()
if not session:
return False
session.is_revoked = True
await db.flush()
return True
async def add_audit_log(
db: AsyncSession,
action: str,
user: Optional[AuthUserORM] = None,
user_id: Optional[int] = None,
username: Optional[str] = None,
resource: Optional[str] = None,
detail: Optional[Dict[str, Any]] = None,
ip_address: Optional[str] = None,
) -> None:
resolved_user_id = user_id
resolved_username = username
if user is not None:
if resolved_user_id is None:
try:
resolved_user_id = user.id
except Exception:
resolved_user_id = None
if resolved_username is None:
try:
resolved_username = user.username
except Exception:
resolved_username = None
entry = AuthAuditLogORM(
user_id=resolved_user_id,
username=resolved_username,
action=action,
resource=resource,
detail=detail or None,
ip_address=(ip_address or "")[:64] or None,
)
db.add(entry)
await db.flush()
def to_user_payload(user: AuthUserORM) -> Dict[str, Any]:
return {
"id": user.id,
"username": user.username,
"role": user.role,
"is_active": user.is_active,
"created_at": user.created_at,
"last_login_at": user.last_login_at,
}