122 lines
4.0 KiB
Python
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
|