Files
xiaobaifupan/next/backend/features/accounts/repository.py
T

337 lines
11 KiB
Python

from __future__ import annotations
import sqlite3
from datetime import datetime
from backend.features.accounts.models import (
MembershipAccount,
MembershipRecord,
SessionRecord,
UserRecord,
)
def _datetime(value: str) -> datetime:
return datetime.fromisoformat(value)
def _optional_datetime(value: str | None) -> datetime | None:
return _datetime(value) if value else None
def _user(row: sqlite3.Row) -> UserRecord:
return UserRecord(
id=int(row["id"]),
username=str(row["username"]),
username_key=str(row["username_key"]),
password_hash=str(row["password_hash"]),
is_admin=bool(row["is_admin"]),
status=str(row["status"]),
created_at=_datetime(str(row["created_at"])),
updated_at=_datetime(str(row["updated_at"])),
)
def _membership(row: sqlite3.Row, prefix: str = "") -> MembershipRecord:
return MembershipRecord(
user_id=int(row[f"{prefix}user_id"]),
state=str(row[f"{prefix}state"]),
expires_at=_optional_datetime(row[f"{prefix}expires_at"]),
is_permanent=bool(row[f"{prefix}is_permanent"]),
daily_llm_limit=int(row[f"{prefix}daily_llm_limit"]),
updated_at=_datetime(str(row[f"{prefix}updated_at"])),
updated_by=(
int(row[f"{prefix}updated_by"]) if row[f"{prefix}updated_by"] is not None else None
),
)
class AccountRepository:
@staticmethod
def count_users(connection: sqlite3.Connection) -> int:
return int(connection.execute("SELECT COUNT(*) FROM users").fetchone()[0])
@staticmethod
def create_user(
connection: sqlite3.Connection,
username: str,
username_key: str,
password_hash: str,
is_admin: bool,
now: datetime,
) -> UserRecord:
timestamp = now.isoformat(timespec="seconds")
cursor = connection.execute(
"""
INSERT INTO users
(username, username_key, password_hash, is_admin, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
(username, username_key, password_hash, int(is_admin), timestamp, timestamp),
)
return UserRecord(
id=int(cursor.lastrowid),
username=username,
username_key=username_key,
password_hash=password_hash,
is_admin=is_admin,
status="active",
created_at=now,
updated_at=now,
)
@staticmethod
def get_user_by_username_key(
connection: sqlite3.Connection, username_key: str
) -> UserRecord | None:
row = connection.execute(
"SELECT * FROM users WHERE username_key = ?", (username_key,)
).fetchone()
return _user(row) if row else None
@staticmethod
def get_user_by_id(connection: sqlite3.Connection, user_id: int) -> UserRecord | None:
row = connection.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
return _user(row) if row else None
@staticmethod
def create_default_membership(
connection: sqlite3.Connection, user_id: int, now: datetime
) -> MembershipRecord:
timestamp = now.isoformat(timespec="seconds")
connection.execute(
"""
INSERT INTO memberships (user_id, state, daily_llm_limit, updated_at)
VALUES (?, 'inactive', 50, ?)
""",
(user_id, timestamp),
)
return MembershipRecord(user_id, "inactive", None, False, 50, now, None)
@staticmethod
def get_membership(connection: sqlite3.Connection, user_id: int) -> MembershipRecord | None:
row = connection.execute(
"SELECT * FROM memberships WHERE user_id = ?", (user_id,)
).fetchone()
return _membership(row) if row else None
@staticmethod
def update_password(
connection: sqlite3.Connection, user_id: int, password_hash: str, now: datetime
) -> None:
connection.execute(
"UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?",
(password_hash, now.isoformat(timespec="seconds"), user_id),
)
@staticmethod
def create_session(
connection: sqlite3.Connection,
token_hash: str,
csrf_hash: str,
user_id: int,
now: datetime,
expires_at: datetime,
) -> None:
timestamp = now.isoformat(timespec="seconds")
connection.execute(
"""
INSERT INTO sessions
(token_hash, user_id, csrf_hash, created_at, expires_at, last_seen_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
token_hash,
user_id,
csrf_hash,
timestamp,
expires_at.isoformat(timespec="seconds"),
timestamp,
),
)
@staticmethod
def get_session(connection: sqlite3.Connection, token_hash: str) -> SessionRecord | None:
row = connection.execute(
"""
SELECT
u.*,
m.user_id AS membership_user_id,
m.state AS membership_state,
m.expires_at AS membership_expires_at,
m.is_permanent AS membership_is_permanent,
m.daily_llm_limit AS membership_daily_llm_limit,
m.updated_at AS membership_updated_at,
m.updated_by AS membership_updated_by,
s.token_hash AS session_token_hash,
s.csrf_hash AS session_csrf_hash,
s.expires_at AS session_expires_at
FROM sessions s
JOIN users u ON u.id = s.user_id
JOIN memberships m ON m.user_id = u.id
WHERE s.token_hash = ?
""",
(token_hash,),
).fetchone()
if row is None:
return None
return SessionRecord(
token_hash=str(row["session_token_hash"]),
csrf_hash=str(row["session_csrf_hash"]),
user=_user(row),
membership=_membership(row, "membership_"),
expires_at=_datetime(str(row["session_expires_at"])),
)
@staticmethod
def delete_session(connection: sqlite3.Connection, token_hash: str) -> None:
connection.execute("DELETE FROM sessions WHERE token_hash = ?", (token_hash,))
@staticmethod
def delete_other_sessions(
connection: sqlite3.Connection, user_id: int, token_hash: str
) -> None:
connection.execute(
"DELETE FROM sessions WHERE user_id = ? AND token_hash <> ?",
(user_id, token_hash),
)
@staticmethod
def get_encrypted_profile(connection: sqlite3.Connection, user_id: int) -> sqlite3.Row | None:
return connection.execute(
"SELECT encrypted_payload, updated_at FROM birth_profiles WHERE user_id = ?",
(user_id,),
).fetchone()
@staticmethod
def save_encrypted_profile(
connection: sqlite3.Connection,
user_id: int,
encrypted_payload: str,
now: datetime,
) -> None:
timestamp = now.isoformat(timespec="seconds")
connection.execute(
"""
INSERT INTO birth_profiles (user_id, encrypted_payload, created_at, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(user_id) DO UPDATE SET
encrypted_payload = excluded.encrypted_payload,
updated_at = excluded.updated_at
""",
(user_id, encrypted_payload, timestamp, timestamp),
)
@staticmethod
def delete_profile(connection: sqlite3.Connection, user_id: int) -> None:
connection.execute("DELETE FROM birth_profiles WHERE user_id = ?", (user_id,))
@staticmethod
def list_memberships(connection: sqlite3.Connection) -> tuple[MembershipAccount, ...]:
rows = connection.execute(
"""
SELECT
u.*,
m.user_id AS membership_user_id,
m.state AS membership_state,
m.expires_at AS membership_expires_at,
m.is_permanent AS membership_is_permanent,
m.daily_llm_limit AS membership_daily_llm_limit,
m.updated_at AS membership_updated_at,
m.updated_by AS membership_updated_by
FROM users u
JOIN memberships m ON m.user_id = u.id
ORDER BY u.id
"""
).fetchall()
return tuple(MembershipAccount(_user(row), _membership(row, "membership_")) for row in rows)
@staticmethod
def get_membership_account(
connection: sqlite3.Connection, user_id: int
) -> MembershipAccount | None:
row = connection.execute(
"""
SELECT
u.*,
m.user_id AS membership_user_id,
m.state AS membership_state,
m.expires_at AS membership_expires_at,
m.is_permanent AS membership_is_permanent,
m.daily_llm_limit AS membership_daily_llm_limit,
m.updated_at AS membership_updated_at,
m.updated_by AS membership_updated_by
FROM users u
JOIN memberships m ON m.user_id = u.id
WHERE u.id = ?
""",
(user_id,),
).fetchone()
return MembershipAccount(_user(row), _membership(row, "membership_")) if row else None
@staticmethod
def update_membership(
connection: sqlite3.Connection,
user_id: int,
state: str,
expires_at: datetime | None,
is_permanent: bool,
daily_limit: int,
updated_by: int,
now: datetime,
) -> MembershipRecord | None:
connection.execute(
"""
UPDATE memberships SET
state = ?, expires_at = ?, is_permanent = ?, daily_llm_limit = ?,
updated_at = ?, updated_by = ?
WHERE user_id = ?
""",
(
state,
expires_at.isoformat(timespec="seconds") if expires_at else None,
int(is_permanent),
daily_limit,
now.isoformat(timespec="seconds"),
updated_by,
user_id,
),
)
return AccountRepository.get_membership(connection, user_id)
class SystemCredentialRepository:
@staticmethod
def list_status(connection: sqlite3.Connection) -> dict[str, str]:
return {
str(row["name"]): str(row["updated_at"])
for row in connection.execute("SELECT name, updated_at FROM system_credentials")
}
@staticmethod
def save(
connection: sqlite3.Connection,
name: str,
encrypted_value: str,
updated_by: int,
now: datetime,
) -> None:
connection.execute(
"""
INSERT INTO system_credentials (name, encrypted_value, updated_at, updated_by)
VALUES (?, ?, ?, ?)
ON CONFLICT(name) DO UPDATE SET
encrypted_value = excluded.encrypted_value,
updated_at = excluded.updated_at,
updated_by = excluded.updated_by
""",
(name, encrypted_value, now.isoformat(timespec="seconds"), updated_by),
)
@staticmethod
def get_encrypted(connection: sqlite3.Connection, name: str) -> str | None:
row = connection.execute(
"SELECT encrypted_value FROM system_credentials WHERE name = ?", (name,)
).fetchone()
return str(row["encrypted_value"]) if row else None