Archived
Remove account-based DynDNS onboarding
This commit is contained in:
1 parent
86060d6bdc
commit
148918b283
8 files changed
+67
-599
No files matched your search
+28
-153
@@ -84,12 +84,7 @@ class DdnsStore:
|
||||
update_ipv6=update_ipv6,
|
||||
)
|
||||
|
||||
def create_account(
|
||||
self,
|
||||
hostname: str,
|
||||
username: str | None = None,
|
||||
owner_user_id: int | None = None,
|
||||
) -> dict[str, str]:
|
||||
def create_account(self, hostname: str, username: str | None = None) -> dict[str, str]:
|
||||
now = datetime.now(UTC).isoformat()
|
||||
token = secrets.token_urlsafe(24)
|
||||
update_slug = secrets.token_urlsafe(32)
|
||||
@@ -100,10 +95,10 @@ class DdnsStore:
|
||||
with self.connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO accounts(hostname, username, token_hash, update_slug, management_hash, owner_user_id, created_at, disabled)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 0)
|
||||
INSERT INTO accounts(hostname, username, token_hash, update_slug, management_hash, created_at, disabled)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 0)
|
||||
""",
|
||||
(hostname, username, token_hash, update_slug, management_hash, owner_user_id, now),
|
||||
(hostname, username, token_hash, update_slug, management_hash, now),
|
||||
)
|
||||
return {
|
||||
"hostname": hostname,
|
||||
@@ -113,21 +108,18 @@ class DdnsStore:
|
||||
"management_slug": management_slug,
|
||||
}
|
||||
|
||||
def list_accounts(self, owner_user_id: int | None = None) -> list[dict[str, str | int | None]]:
|
||||
def list_accounts(self) -> list[dict[str, str | int | None]]:
|
||||
with self.connect() as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
sql = """
|
||||
SELECT accounts.hostname, accounts.username, accounts.update_slug, accounts.owner_user_id,
|
||||
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
|
||||
"""
|
||||
params: tuple[int, ...] = ()
|
||||
if owner_user_id is not None:
|
||||
sql += " WHERE accounts.owner_user_id = ?"
|
||||
params = (owner_user_id,)
|
||||
sql += " ORDER BY accounts.hostname"
|
||||
rows = conn.execute(sql, params).fetchall()
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def get_account_by_slug(self, update_slug: str) -> dict[str, str | int | None] | None:
|
||||
@@ -147,7 +139,7 @@ class DdnsStore:
|
||||
conn.row_factory = sqlite3.Row
|
||||
account = conn.execute(
|
||||
"""
|
||||
SELECT accounts.hostname, accounts.username, accounts.update_slug, accounts.owner_user_id,
|
||||
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
|
||||
@@ -178,66 +170,47 @@ class DdnsStore:
|
||||
conn.execute("DELETE FROM accounts WHERE hostname = ?", (hostname,))
|
||||
conn.execute("DELETE FROM records WHERE hostname = ?", (hostname,))
|
||||
|
||||
def create_domain_challenge(self, domain: str, owner_user_id: int | None = None) -> dict[str, str]:
|
||||
def create_domain_challenge(self, domain: str) -> dict[str, str]:
|
||||
token = f"ff-ddns-{secrets.token_urlsafe(24)}"
|
||||
claim_secret = secrets.token_urlsafe(32)
|
||||
claim_hash = hash_lookup_token(claim_secret)
|
||||
now = datetime.now(UTC).isoformat()
|
||||
with self.connect() as conn:
|
||||
existing = conn.execute("SELECT owner_user_id, verified_at FROM domain_challenges WHERE domain = ?", (domain,)).fetchone()
|
||||
if existing and existing[1]:
|
||||
existing = conn.execute("SELECT verified_at FROM domain_challenges WHERE domain = ?", (domain,)).fetchone()
|
||||
if existing and existing[0]:
|
||||
raise sqlite3.IntegrityError("domain is already verified")
|
||||
if existing and owner_user_id is not None and existing[0] is not None and int(existing[0]) != owner_user_id:
|
||||
raise sqlite3.IntegrityError("domain challenge belongs to another user")
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO domain_challenges(domain, token, claim_hash, owner_user_id, created_at, verified_at)
|
||||
VALUES (?, ?, ?, ?, ?, NULL)
|
||||
INSERT INTO domain_challenges(domain, token, claim_hash, created_at, verified_at)
|
||||
VALUES (?, ?, ?, ?, NULL)
|
||||
ON CONFLICT(domain) DO UPDATE SET
|
||||
token=excluded.token,
|
||||
claim_hash=excluded.claim_hash,
|
||||
owner_user_id=excluded.owner_user_id,
|
||||
created_at=excluded.created_at,
|
||||
verified_at=NULL
|
||||
""",
|
||||
(domain, token, claim_hash, owner_user_id, now),
|
||||
(domain, token, claim_hash, now),
|
||||
)
|
||||
return {"domain": domain, "token": token, "claim_secret": claim_secret}
|
||||
|
||||
def verify_domain_challenge(self, domain: str, owner_user_id: int | None = None) -> dict[str, str | None] | None:
|
||||
def verify_domain_challenge(self, domain: str) -> dict[str, str | None] | None:
|
||||
now = datetime.now(UTC).isoformat()
|
||||
with self.connect() as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
sql = "SELECT domain, token, verified_at FROM domain_challenges WHERE domain = ?"
|
||||
params: tuple[str, ...] | tuple[str, int] = (domain,)
|
||||
if owner_user_id is not None:
|
||||
sql += " AND owner_user_id = ?"
|
||||
params = (domain, owner_user_id)
|
||||
challenge = conn.execute(sql, params).fetchone()
|
||||
challenge = conn.execute("SELECT domain, token, verified_at FROM domain_challenges WHERE domain = ?", (domain,)).fetchone()
|
||||
if not challenge:
|
||||
return None
|
||||
if owner_user_id is not None:
|
||||
conn.execute("UPDATE domain_challenges SET verified_at = ? WHERE domain = ? AND owner_user_id = ?", (now, domain, owner_user_id))
|
||||
else:
|
||||
conn.execute("UPDATE domain_challenges SET verified_at = ? WHERE domain = ?", (now, domain))
|
||||
conn.execute("UPDATE domain_challenges SET verified_at = ? WHERE domain = ?", (now, domain))
|
||||
result = dict(challenge)
|
||||
result["verified_at"] = now
|
||||
return result
|
||||
|
||||
def get_domain_challenge(
|
||||
self,
|
||||
domain: str,
|
||||
owner_user_id: int | None = None,
|
||||
claim_secret: str | None = None,
|
||||
) -> dict[str, str | None] | None:
|
||||
def get_domain_challenge(self, domain: str, claim_secret: str | None = None) -> dict[str, str | None] | None:
|
||||
with self.connect() as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
sql = "SELECT domain, token, verified_at FROM domain_challenges WHERE domain = ?"
|
||||
params: tuple[str, ...] | tuple[str, int] = (domain,)
|
||||
if owner_user_id is not None:
|
||||
sql += " AND owner_user_id = ?"
|
||||
params = (domain, owner_user_id)
|
||||
elif claim_secret is not None:
|
||||
params = (domain,)
|
||||
if claim_secret is not None:
|
||||
sql += " AND claim_hash = ?"
|
||||
params = (domain, hash_lookup_token(claim_secret))
|
||||
challenge = conn.execute(sql, params).fetchone()
|
||||
@@ -256,86 +229,16 @@ class DdnsStore:
|
||||
result["claim_secret"] = claim_secret
|
||||
return result
|
||||
|
||||
def is_domain_verified(
|
||||
self,
|
||||
domain: str,
|
||||
owner_user_id: int | None = None,
|
||||
claim_secret: str | None = None,
|
||||
) -> bool:
|
||||
def is_domain_verified(self, domain: str, claim_secret: str | None = None) -> bool:
|
||||
with self.connect() as conn:
|
||||
sql = "SELECT verified_at FROM domain_challenges WHERE domain = ?"
|
||||
params: tuple[str, ...] | tuple[str, int] = (domain,)
|
||||
if owner_user_id is not None:
|
||||
sql += " AND owner_user_id = ?"
|
||||
params = (domain, owner_user_id)
|
||||
elif claim_secret is not None:
|
||||
params = (domain,)
|
||||
if claim_secret is not None:
|
||||
sql += " AND claim_hash = ?"
|
||||
params = (domain, hash_lookup_token(claim_secret))
|
||||
verified_at = conn.execute(sql, params).fetchone()
|
||||
return bool(verified_at and verified_at[0])
|
||||
|
||||
def count_user_accounts(self, owner_user_id: int) -> int:
|
||||
with self.connect() as conn:
|
||||
row = conn.execute("SELECT COUNT(*) FROM accounts WHERE owner_user_id = ? AND disabled = 0", (owner_user_id,)).fetchone()
|
||||
return int(row[0])
|
||||
|
||||
def create_user(self, email: str, password: str, invite_code: str | None, require_invite: bool) -> dict[str, str | int]:
|
||||
now = datetime.now(UTC).isoformat()
|
||||
password_hash = hash_secret(password)
|
||||
with self.connect() as conn:
|
||||
if require_invite:
|
||||
invite = conn.execute(
|
||||
"SELECT code, used_at FROM invites WHERE code = ?",
|
||||
(invite_code or "",),
|
||||
).fetchone()
|
||||
if not invite or invite[1]:
|
||||
raise ValueError("invalid invite code")
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
INSERT INTO users(email, password_hash, created_at, disabled)
|
||||
VALUES (?, ?, ?, 0)
|
||||
""",
|
||||
(email, password_hash, now),
|
||||
)
|
||||
user_id = int(cursor.lastrowid)
|
||||
if require_invite and invite_code:
|
||||
conn.execute("UPDATE invites SET used_at = ?, used_by_user_id = ? WHERE code = ?", (now, user_id, invite_code))
|
||||
return {"id": user_id, "email": email}
|
||||
|
||||
def authenticate_user(self, email: str, password: str) -> dict[str, str | int] | None:
|
||||
with self.connect() as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
user = conn.execute(
|
||||
"SELECT id, email, password_hash, disabled FROM users WHERE email = ?",
|
||||
(email,),
|
||||
).fetchone()
|
||||
if not user or user["disabled"] or not verify_secret(password, user["password_hash"]):
|
||||
return None
|
||||
return {"id": int(user["id"]), "email": str(user["email"])}
|
||||
|
||||
def get_user(self, user_id: int) -> dict[str, str | int] | None:
|
||||
with self.connect() as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
user = conn.execute("SELECT id, email, disabled FROM users WHERE id = ?", (user_id,)).fetchone()
|
||||
if not user or user["disabled"]:
|
||||
return None
|
||||
return {"id": int(user["id"]), "email": str(user["email"])}
|
||||
|
||||
def create_invite(self) -> dict[str, str]:
|
||||
code = secrets.token_urlsafe(18)
|
||||
now = datetime.now(UTC).isoformat()
|
||||
with self.connect() as conn:
|
||||
conn.execute("INSERT INTO invites(code, created_at) VALUES (?, ?)", (code, now))
|
||||
return {"code": code}
|
||||
|
||||
def list_invites(self) -> list[dict[str, str | None]]:
|
||||
with self.connect() as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
rows = conn.execute(
|
||||
"SELECT code, created_at, used_at FROM invites ORDER BY created_at DESC LIMIT 50"
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def list_records(self) -> list[dict[str, str | None]]:
|
||||
with self.connect() as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
@@ -451,16 +354,13 @@ class DdnsStore:
|
||||
token_hash TEXT NOT NULL,
|
||||
update_slug TEXT NOT NULL UNIQUE,
|
||||
management_hash TEXT UNIQUE,
|
||||
owner_user_id INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
disabled INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(owner_user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
disabled INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
"""
|
||||
)
|
||||
_ensure_column(conn, "accounts", "update_slug", "TEXT")
|
||||
_ensure_column(conn, "accounts", "management_hash", "TEXT")
|
||||
_ensure_column(conn, "accounts", "owner_user_id", "INTEGER")
|
||||
for row in conn.execute("SELECT hostname FROM accounts WHERE update_slug IS NULL OR update_slug = ''"):
|
||||
conn.execute("UPDATE accounts SET update_slug = ? WHERE hostname = ?", (secrets.token_urlsafe(32), row[0]))
|
||||
for row in conn.execute("SELECT hostname FROM accounts WHERE management_hash IS NULL OR management_hash = ''"):
|
||||
@@ -473,37 +373,12 @@ class DdnsStore:
|
||||
domain TEXT PRIMARY KEY,
|
||||
token TEXT NOT NULL,
|
||||
claim_hash TEXT,
|
||||
owner_user_id INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
verified_at TEXT,
|
||||
FOREIGN KEY(owner_user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
verified_at TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
_ensure_column(conn, "domain_challenges", "claim_hash", "TEXT")
|
||||
_ensure_column(conn, "domain_challenges", "owner_user_id", "INTEGER")
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
disabled INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS invites (
|
||||
code TEXT PRIMARY KEY,
|
||||
created_at TEXT NOT NULL,
|
||||
used_at TEXT,
|
||||
used_by_user_id INTEGER,
|
||||
FOREIGN KEY(used_by_user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _default_username(hostname: str) -> str:
|
||||
|
||||
Reference in new issue
Block a user