rebuild(stage-3): establish accounts permissions and secure settings
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
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 usage_today(connection: sqlite3.Connection, user_id: int, usage_date: str) -> int:
|
||||
row = connection.execute(
|
||||
"""
|
||||
SELECT successful_calls FROM llm_usage_daily
|
||||
WHERE user_id = ? AND usage_date = ?
|
||||
""",
|
||||
(user_id, usage_date),
|
||||
).fetchone()
|
||||
return int(row["successful_calls"]) if row else 0
|
||||
|
||||
@staticmethod
|
||||
def usage_for_users(connection: sqlite3.Connection, usage_date: str) -> dict[int, int]:
|
||||
return {
|
||||
int(row["user_id"]): int(row["successful_calls"])
|
||||
for row in connection.execute(
|
||||
"SELECT user_id, successful_calls FROM llm_usage_daily WHERE usage_date = ?",
|
||||
(usage_date,),
|
||||
)
|
||||
}
|
||||
|
||||
@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
|
||||
Reference in New Issue
Block a user