2026-05-20 23:55:27 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
|
|
2026-05-21 18:30:15 +02:00
|
|
|
from ..security import hash_lookup_token, hash_secret, new_bearer_secret, verify_secret
|
2026-05-22 17:05:06 +02:00
|
|
|
from .base import StoreConnectionMixin
|
|
|
|
|
from .types import AccountRow, account_row
|
2026-05-20 23:55:27 +02:00
|
|
|
|
|
|
|
|
|
2026-05-22 17:05:06 +02:00
|
|
|
class AccountsMixin(StoreConnectionMixin):
|
2026-05-21 17:23:08 +02:00
|
|
|
def create_account(self, hostname: str, username: str | None = None, *, accepted: bool = True) -> AccountRow:
|
2026-05-20 23:55:27 +02:00
|
|
|
now = datetime.now(UTC).isoformat()
|
2026-05-21 18:30:15 +02:00
|
|
|
update_slug = new_bearer_secret()
|
|
|
|
|
management_slug = new_bearer_secret()
|
2026-05-20 23:55:27 +02:00
|
|
|
management_hash = hash_lookup_token(management_slug)
|
2026-05-21 17:23:08 +02:00
|
|
|
accepted_at = now if accepted else None
|
2026-05-20 23:55:27 +02:00
|
|
|
with self.connect() as conn:
|
|
|
|
|
conn.execute(
|
|
|
|
|
"""
|
2026-05-21 17:23:08 +02:00
|
|
|
INSERT INTO accounts(hostname, update_slug, management_hash, created_at, accepted_at, disabled)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, 0)
|
2026-05-20 23:55:27 +02:00
|
|
|
""",
|
2026-05-21 17:23:08 +02:00
|
|
|
(hostname, update_slug, management_hash, now, accepted_at),
|
2026-05-20 23:55:27 +02:00
|
|
|
)
|
|
|
|
|
return {
|
|
|
|
|
"hostname": hostname,
|
|
|
|
|
"update_slug": update_slug,
|
|
|
|
|
"management_slug": management_slug,
|
2026-05-21 17:23:08 +02:00
|
|
|
"accepted_at": accepted_at,
|
2026-05-20 23:55:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def list_accounts(self, limit: int = 100, offset: int = 0) -> list[AccountRow]:
|
|
|
|
|
with self.connect() as conn:
|
|
|
|
|
rows = conn.execute(
|
|
|
|
|
"""
|
2026-05-21 17:23:08 +02:00
|
|
|
SELECT accounts.hostname, accounts.update_slug,
|
|
|
|
|
accounts.created_at, accounts.accepted_at, accounts.disabled, records.ipv4, records.ipv6, records.updated_at
|
2026-05-20 23:55:27 +02:00
|
|
|
FROM accounts
|
|
|
|
|
LEFT JOIN records ON records.hostname = accounts.hostname
|
|
|
|
|
ORDER BY accounts.hostname
|
|
|
|
|
LIMIT ? OFFSET ?
|
|
|
|
|
""",
|
|
|
|
|
(limit, offset),
|
|
|
|
|
).fetchall()
|
2026-05-22 17:05:06 +02:00
|
|
|
return [account_row(row) for row in rows]
|
2026-05-20 23:55:27 +02:00
|
|
|
|
|
|
|
|
def count_accounts(self) -> int:
|
|
|
|
|
with self.connect() as conn:
|
|
|
|
|
row = conn.execute("SELECT COUNT(*) FROM accounts").fetchone()
|
|
|
|
|
return int(row[0])
|
|
|
|
|
|
|
|
|
|
def get_account_by_hostname(self, hostname: str) -> AccountRow | None:
|
|
|
|
|
with self.connect() as conn:
|
|
|
|
|
account = conn.execute(
|
|
|
|
|
"""
|
2026-05-21 17:23:08 +02:00
|
|
|
SELECT accounts.hostname, accounts.update_slug, accounts.dashboard_password_hash,
|
|
|
|
|
accounts.created_at, accounts.accepted_at, accounts.disabled, records.ipv4, records.ipv6, records.updated_at
|
2026-05-20 23:55:27 +02:00
|
|
|
FROM accounts
|
|
|
|
|
LEFT JOIN records ON records.hostname = accounts.hostname
|
|
|
|
|
WHERE accounts.hostname = ?
|
|
|
|
|
""",
|
|
|
|
|
(hostname,),
|
|
|
|
|
).fetchone()
|
2026-05-22 17:05:06 +02:00
|
|
|
return account_row(account) if account else None
|
2026-05-20 23:55:27 +02:00
|
|
|
|
|
|
|
|
def get_account_by_slug(self, update_slug: str) -> AccountRow | None:
|
|
|
|
|
with self.connect() as conn:
|
|
|
|
|
account = conn.execute(
|
|
|
|
|
"""
|
2026-05-21 17:23:08 +02:00
|
|
|
SELECT hostname, update_slug, accepted_at, disabled FROM accounts
|
2026-05-20 23:55:27 +02:00
|
|
|
WHERE update_slug = ?
|
|
|
|
|
""",
|
|
|
|
|
(update_slug,),
|
|
|
|
|
).fetchone()
|
2026-05-22 17:05:06 +02:00
|
|
|
return account_row(account) if account else None
|
2026-05-20 23:55:27 +02:00
|
|
|
|
|
|
|
|
def get_account_by_management_slug(self, management_slug: str) -> AccountRow | None:
|
|
|
|
|
with self.connect() as conn:
|
|
|
|
|
account = conn.execute(
|
|
|
|
|
"""
|
2026-05-21 17:23:08 +02:00
|
|
|
SELECT accounts.hostname, accounts.update_slug, accounts.dashboard_password_hash,
|
|
|
|
|
accounts.created_at, accounts.accepted_at, accounts.disabled, records.ipv4, records.ipv6, records.updated_at
|
2026-05-20 23:55:27 +02:00
|
|
|
FROM accounts
|
|
|
|
|
LEFT JOIN records ON records.hostname = accounts.hostname
|
|
|
|
|
WHERE accounts.management_hash = ?
|
|
|
|
|
""",
|
|
|
|
|
(hash_lookup_token(management_slug),),
|
|
|
|
|
).fetchone()
|
2026-05-22 17:05:06 +02:00
|
|
|
return account_row(account) if account else None
|
2026-05-20 23:55:27 +02:00
|
|
|
|
2026-05-21 17:23:08 +02:00
|
|
|
def activate_account(self, hostname: str) -> AccountRow | None:
|
|
|
|
|
now = datetime.now(UTC).isoformat()
|
2026-05-20 23:55:27 +02:00
|
|
|
with self.connect() as conn:
|
2026-05-21 17:23:08 +02:00
|
|
|
conn.execute(
|
|
|
|
|
"UPDATE accounts SET accepted_at = COALESCE(accepted_at, ?) WHERE hostname = ?",
|
|
|
|
|
(now, hostname),
|
|
|
|
|
)
|
|
|
|
|
return self.get_account_by_hostname(hostname)
|
2026-05-20 23:55:27 +02:00
|
|
|
|
|
|
|
|
def delete_account(self, hostname: str) -> None:
|
|
|
|
|
with self.connect() as conn:
|
|
|
|
|
conn.execute("DELETE FROM accounts WHERE hostname = ?", (hostname,))
|
|
|
|
|
conn.execute("DELETE FROM records WHERE hostname = ?", (hostname,))
|
|
|
|
|
|
|
|
|
|
def rotate_update_slug(self, hostname: str) -> AccountRow | None:
|
2026-05-21 18:30:15 +02:00
|
|
|
update_slug = new_bearer_secret()
|
2026-05-20 23:55:27 +02:00
|
|
|
with self.connect() as conn:
|
|
|
|
|
conn.execute("UPDATE accounts SET update_slug = ? WHERE hostname = ?", (update_slug, hostname))
|
|
|
|
|
return self.get_account_by_hostname(hostname)
|
|
|
|
|
|
|
|
|
|
def rotate_management_slug(self, hostname: str) -> AccountRow | None:
|
2026-05-21 18:30:15 +02:00
|
|
|
management_slug = new_bearer_secret()
|
2026-05-20 23:55:27 +02:00
|
|
|
management_hash = hash_lookup_token(management_slug)
|
|
|
|
|
with self.connect() as conn:
|
|
|
|
|
conn.execute("UPDATE accounts SET management_hash = ? WHERE hostname = ?", (management_hash, hostname))
|
|
|
|
|
account = self.get_account_by_hostname(hostname)
|
|
|
|
|
if account:
|
|
|
|
|
account["management_slug"] = management_slug
|
|
|
|
|
return account
|
|
|
|
|
|
|
|
|
|
def set_dashboard_password(self, hostname: str, password: str) -> AccountRow | None:
|
|
|
|
|
password_hash = hash_secret(password)
|
|
|
|
|
with self.connect() as conn:
|
|
|
|
|
conn.execute(
|
|
|
|
|
"UPDATE accounts SET dashboard_password_hash = ? WHERE hostname = ?",
|
|
|
|
|
(password_hash, hostname),
|
|
|
|
|
)
|
|
|
|
|
return self.get_account_by_hostname(hostname)
|
|
|
|
|
|
|
|
|
|
def clear_dashboard_password(self, hostname: str) -> AccountRow | None:
|
|
|
|
|
with self.connect() as conn:
|
|
|
|
|
conn.execute("UPDATE accounts SET dashboard_password_hash = NULL WHERE hostname = ?", (hostname,))
|
|
|
|
|
return self.get_account_by_hostname(hostname)
|
|
|
|
|
|
|
|
|
|
def verify_dashboard_password(self, hostname: str, password: str | None) -> bool:
|
|
|
|
|
if not password:
|
|
|
|
|
return False
|
|
|
|
|
with self.connect() as conn:
|
|
|
|
|
account = conn.execute(
|
|
|
|
|
"""
|
|
|
|
|
SELECT dashboard_password_hash, disabled FROM accounts
|
|
|
|
|
WHERE hostname = ?
|
|
|
|
|
""",
|
|
|
|
|
(hostname,),
|
|
|
|
|
).fetchone()
|
|
|
|
|
if not account or account["disabled"] or not account["dashboard_password_hash"]:
|
|
|
|
|
return False
|
|
|
|
|
return verify_secret(password, account["dashboard_password_hash"])
|