186 lines
4.7 KiB
Python
186 lines
4.7 KiB
Python
from typing import Any
|
|
|
|
from fastapi import HTTPException, Request, status
|
|
from passlib.context import CryptContext
|
|
|
|
from app.db.database import get_connection
|
|
from app.db.migrations import run_migrations
|
|
|
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
|
|
|
|
def _table_columns(con, table_name: str) -> set[str]:
|
|
rows = con.execute(f"PRAGMA table_info({table_name})").fetchall()
|
|
return {row["name"] for row in rows}
|
|
|
|
|
|
def _user_select_columns(con) -> str:
|
|
columns = _table_columns(con, "users")
|
|
enabled_select = "COALESCE(is_enabled, 1) AS is_enabled" if "is_enabled" in columns else "1 AS is_enabled"
|
|
return f"id, username, display_name, email, password_hash, role, is_active, {enabled_select}, created_at, updated_at"
|
|
|
|
|
|
def mark_last_login(user_id: int) -> None:
|
|
con = get_connection()
|
|
columns = _table_columns(con, "users")
|
|
if "last_login_at" in columns:
|
|
con.execute(
|
|
"""
|
|
UPDATE users
|
|
SET last_login_at = CURRENT_TIMESTAMP,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
""",
|
|
(user_id,),
|
|
)
|
|
con.commit()
|
|
con.close()
|
|
|
|
|
|
def hash_password(password: str) -> str:
|
|
return pwd_context.hash(password)
|
|
|
|
|
|
def verify_password(plain_password: str, password_hash: str) -> bool:
|
|
if not password_hash:
|
|
return False
|
|
|
|
return pwd_context.verify(plain_password, password_hash)
|
|
|
|
|
|
def get_user_by_username(username: str) -> dict[str, Any] | None:
|
|
run_migrations()
|
|
con = get_connection()
|
|
|
|
row = con.execute(
|
|
f"""
|
|
SELECT {_user_select_columns(con)}
|
|
FROM users
|
|
WHERE username = ?
|
|
""",
|
|
(username,),
|
|
).fetchone()
|
|
|
|
con.close()
|
|
return dict(row) if row else None
|
|
|
|
|
|
def find_or_create_oauth_user(username: str, display_name: str, email: str) -> dict[str, Any]:
|
|
username = username.strip()
|
|
display_name = (display_name or username).strip() or username
|
|
email = (email or "").strip()
|
|
|
|
run_migrations()
|
|
con = get_connection()
|
|
row = con.execute(
|
|
f"""
|
|
SELECT {_user_select_columns(con)}
|
|
FROM users
|
|
WHERE username = ?
|
|
""",
|
|
(username,),
|
|
).fetchone()
|
|
|
|
if row:
|
|
con.execute(
|
|
"""
|
|
UPDATE users
|
|
SET display_name = ?,
|
|
email = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
""",
|
|
(display_name, email, row["id"]),
|
|
)
|
|
else:
|
|
con.execute(
|
|
"""
|
|
INSERT INTO users (
|
|
username,
|
|
display_name,
|
|
email,
|
|
password_hash,
|
|
role,
|
|
is_active,
|
|
created_at,
|
|
updated_at
|
|
)
|
|
VALUES (?, ?, ?, '', 'developer', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
""",
|
|
(username, display_name, email),
|
|
)
|
|
|
|
con.commit()
|
|
user_id = row["id"] if row else con.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
|
user = con.execute(
|
|
f"""
|
|
SELECT {_user_select_columns(con)}
|
|
FROM users
|
|
WHERE id = ?
|
|
""",
|
|
(user_id,),
|
|
).fetchone()
|
|
|
|
con.close()
|
|
return dict(user)
|
|
|
|
|
|
def get_user_by_id(user_id: int) -> dict[str, Any] | None:
|
|
run_migrations()
|
|
con = get_connection()
|
|
|
|
row = con.execute(
|
|
f"""
|
|
SELECT {_user_select_columns(con)}
|
|
FROM users
|
|
WHERE id = ?
|
|
""",
|
|
(user_id,),
|
|
).fetchone()
|
|
|
|
con.close()
|
|
return dict(row) if row else None
|
|
|
|
|
|
def authenticate_user(username: str, password: str) -> dict[str, Any] | None:
|
|
user = get_user_by_username(username.strip())
|
|
|
|
if not user or not user.get("is_active") or not user.get("is_enabled", 1):
|
|
return None
|
|
|
|
if not verify_password(password, user.get("password_hash") or ""):
|
|
return None
|
|
|
|
mark_last_login(int(user["id"]))
|
|
return user
|
|
|
|
|
|
def current_user(request: Request) -> dict[str, Any] | None:
|
|
user_id = request.session.get("user_id")
|
|
if not user_id:
|
|
return None
|
|
|
|
try:
|
|
user_id = int(user_id)
|
|
except (TypeError, ValueError):
|
|
request.session.pop("user_id", None)
|
|
return None
|
|
|
|
user = get_user_by_id(user_id)
|
|
if not user or not user.get("is_active") or not user.get("is_enabled", 1):
|
|
request.session.pop("user_id", None)
|
|
return None
|
|
|
|
return user
|
|
|
|
|
|
def require_user(request: Request) -> dict[str, Any]:
|
|
user = current_user(request)
|
|
if user:
|
|
return user
|
|
|
|
raise HTTPException(
|
|
status_code=status.HTTP_303_SEE_OTHER,
|
|
headers={"Location": "/portal/login"},
|
|
)
|