Archived
Build KarlDNS self-hosted DynDNS service
This commit is contained in:
1 parent
45bc38d950
commit
ca92dc211a
65 files changed
+9200
-3570
No files matched your search
@@ -0,0 +1,180 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import secrets
|
||||
import sqlite3
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from ..ddns_security import hash_lookup_token, hash_secret, verify_secret
|
||||
from .types import AccountRow
|
||||
|
||||
|
||||
class AccountsMixin:
|
||||
def create_account(self, hostname: str, username: str | None = None) -> AccountRow:
|
||||
now = datetime.now(UTC).isoformat()
|
||||
token = secrets.token_urlsafe(24)
|
||||
update_slug = secrets.token_urlsafe(32)
|
||||
management_slug = secrets.token_urlsafe(32)
|
||||
username = username or _default_username(hostname)
|
||||
token_hash = hash_secret(token)
|
||||
management_hash = hash_lookup_token(management_slug)
|
||||
with self.connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO accounts(hostname, username, token_hash, update_slug, management_hash, created_at, disabled)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 0)
|
||||
""",
|
||||
(hostname, username, token_hash, update_slug, management_hash, now),
|
||||
)
|
||||
return {
|
||||
"hostname": hostname,
|
||||
"username": username,
|
||||
"password": token,
|
||||
"update_slug": update_slug,
|
||||
"management_slug": management_slug,
|
||||
}
|
||||
|
||||
def list_accounts(self, limit: int = 100, offset: int = 0) -> list[AccountRow]:
|
||||
with self.connect() as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT accounts.hostname, accounts.username, accounts.update_slug,
|
||||
accounts.created_at, accounts.disabled, records.ipv4, records.ipv6, records.updated_at
|
||||
FROM accounts
|
||||
LEFT JOIN records ON records.hostname = accounts.hostname
|
||||
ORDER BY accounts.hostname
|
||||
LIMIT ? OFFSET ?
|
||||
""",
|
||||
(limit, offset),
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
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:
|
||||
conn.row_factory = sqlite3.Row
|
||||
account = conn.execute(
|
||||
"""
|
||||
SELECT accounts.hostname, accounts.username, accounts.update_slug, accounts.dashboard_password_hash,
|
||||
accounts.created_at, accounts.disabled, records.ipv4, records.ipv6, records.updated_at
|
||||
FROM accounts
|
||||
LEFT JOIN records ON records.hostname = accounts.hostname
|
||||
WHERE accounts.hostname = ?
|
||||
""",
|
||||
(hostname,),
|
||||
).fetchone()
|
||||
return dict(account) if account else None
|
||||
|
||||
def get_account_by_slug(self, update_slug: str) -> AccountRow | None:
|
||||
with self.connect() as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
account = conn.execute(
|
||||
"""
|
||||
SELECT hostname, username, token_hash, update_slug, disabled FROM accounts
|
||||
WHERE update_slug = ?
|
||||
""",
|
||||
(update_slug,),
|
||||
).fetchone()
|
||||
return dict(account) if account else None
|
||||
|
||||
def get_account_by_management_slug(self, management_slug: str) -> AccountRow | None:
|
||||
with self.connect() as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
account = conn.execute(
|
||||
"""
|
||||
SELECT accounts.hostname, accounts.username, accounts.update_slug, accounts.dashboard_password_hash,
|
||||
accounts.created_at, accounts.disabled, records.ipv4, records.ipv6, records.updated_at
|
||||
FROM accounts
|
||||
LEFT JOIN records ON records.hostname = accounts.hostname
|
||||
WHERE accounts.management_hash = ?
|
||||
""",
|
||||
(hash_lookup_token(management_slug),),
|
||||
).fetchone()
|
||||
return dict(account) if account else None
|
||||
|
||||
def verify_account(self, hostname: str, username: str | None, token: str | None) -> bool:
|
||||
if not username or not token:
|
||||
return False
|
||||
with self.connect() as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
account = conn.execute(
|
||||
"""
|
||||
SELECT token_hash, disabled FROM accounts
|
||||
WHERE hostname = ? AND username = ?
|
||||
""",
|
||||
(hostname, username),
|
||||
).fetchone()
|
||||
if not account or account["disabled"]:
|
||||
return False
|
||||
return verify_secret(token, account["token_hash"])
|
||||
|
||||
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:
|
||||
update_slug = secrets.token_urlsafe(32)
|
||||
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:
|
||||
management_slug = secrets.token_urlsafe(32)
|
||||
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 rotate_password(self, hostname: str) -> AccountRow | None:
|
||||
token = secrets.token_urlsafe(24)
|
||||
token_hash = hash_secret(token)
|
||||
with self.connect() as conn:
|
||||
conn.execute("UPDATE accounts SET token_hash = ? WHERE hostname = ?", (token_hash, hostname))
|
||||
account = self.get_account_by_hostname(hostname)
|
||||
if account:
|
||||
account["password"] = token
|
||||
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:
|
||||
conn.row_factory = sqlite3.Row
|
||||
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"])
|
||||
|
||||
|
||||
def _default_username(hostname: str) -> str:
|
||||
label = hostname.split(".", 1)[0]
|
||||
return re.sub(r"[^a-z0-9_.-]", "-", label.lower()) or "fritzbox"
|
||||
Reference in new issue
Block a user