FluxIP/app/auth/repository.py

122 lines
4.0 KiB
Python

from __future__ import annotations
import sqlite3
from dataclasses import dataclass
from app.core.time import to_iso
from app.database.database import Database
@dataclass(slots=True)
class AdminUser:
id: int
username: str
password_hash: str
last_login_at: str | None
@dataclass(slots=True)
class SessionRecord:
token_hash: str
csrf_hash: str
user_id: int
username: str
expires_at: str
class AuthRepository:
def __init__(self, database: Database) -> None:
self.database = database
def has_admin(self) -> bool:
with self.database.connect() as connection:
return (
connection.execute("SELECT 1 FROM admin_users WHERE id = 1").fetchone() is not None
)
def create_admin(self, username: str, password_hash: str) -> AdminUser:
now = to_iso()
try:
with self.database.connect() as connection, connection:
connection.execute(
"""
INSERT INTO admin_users(id, username, password_hash, created_at, updated_at)
VALUES (1, ?, ?, ?, ?)
""",
(username, password_hash, now, now),
)
except sqlite3.IntegrityError as exc:
raise ValueError("ADMIN_ALREADY_EXISTS") from exc
return AdminUser(1, username, password_hash, None)
def get_admin(self) -> AdminUser | None:
with self.database.connect() as connection:
row = connection.execute(
"SELECT id, username, password_hash, last_login_at FROM admin_users WHERE id = 1"
).fetchone()
if row is None:
return None
return AdminUser(**dict(row))
def update_login_time(self) -> None:
now = to_iso()
with self.database.connect() as connection, connection:
connection.execute(
"UPDATE admin_users SET last_login_at = ?, updated_at = ? WHERE id = 1", (now, now)
)
def update_password(self, password_hash: str) -> None:
now = to_iso()
with self.database.connect() as connection, connection:
connection.execute(
"UPDATE admin_users SET password_hash = ?, updated_at = ? WHERE id = 1",
(password_hash, now),
)
connection.execute("DELETE FROM auth_sessions")
def create_session(
self,
*,
token_hash: str,
csrf_hash: str,
expires_at: str,
) -> None:
now = to_iso()
with self.database.connect() as connection, connection:
connection.execute("DELETE FROM auth_sessions WHERE expires_at <= ?", (now,))
connection.execute(
"""
INSERT INTO auth_sessions(
token_hash, user_id, csrf_hash, expires_at, created_at, last_seen_at
) VALUES (?, 1, ?, ?, ?, ?)
""",
(token_hash, csrf_hash, expires_at, now, now),
)
def get_session(self, token_hash: str) -> SessionRecord | None:
now = to_iso()
with self.database.connect() as connection, connection:
row = connection.execute(
"""
SELECT s.token_hash, s.csrf_hash, s.user_id, u.username, s.expires_at
FROM auth_sessions AS s
JOIN admin_users AS u ON u.id = s.user_id
WHERE s.token_hash = ? AND s.expires_at > ?
""",
(token_hash, now),
).fetchone()
if row:
connection.execute(
"UPDATE auth_sessions SET last_seen_at = ? WHERE token_hash = ?",
(now, token_hash),
)
return SessionRecord(**dict(row)) if row else None
def delete_session(self, token_hash: str) -> None:
with self.database.connect() as connection, connection:
connection.execute("DELETE FROM auth_sessions WHERE token_hash = ?", (token_hash,))
class SessionConflictError(Exception):
pass