Sync dev changes into release-1.0
This commit is contained in:
@@ -65,6 +65,111 @@ def init_db() -> None:
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS user_contacts (
|
||||
user_id INTEGER PRIMARY KEY,
|
||||
email TEXT,
|
||||
email_verified INTEGER NOT NULL DEFAULT 0,
|
||||
discord TEXT,
|
||||
discord_verified INTEGER NOT NULL DEFAULT 0,
|
||||
telegram TEXT,
|
||||
telegram_verified INTEGER NOT NULL DEFAULT 0,
|
||||
matrix TEXT,
|
||||
matrix_verified INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS invite_profiles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
max_uses INTEGER,
|
||||
expires_in_days INTEGER,
|
||||
require_captcha INTEGER NOT NULL DEFAULT 0,
|
||||
password_rules_json TEXT,
|
||||
allow_referrals INTEGER NOT NULL DEFAULT 0,
|
||||
referral_uses INTEGER,
|
||||
user_expiry_days INTEGER,
|
||||
user_expiry_action TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS invites (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
profile_id INTEGER,
|
||||
created_by TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT,
|
||||
max_uses INTEGER,
|
||||
uses_count INTEGER NOT NULL DEFAULT 0,
|
||||
disabled INTEGER NOT NULL DEFAULT 0,
|
||||
require_captcha INTEGER NOT NULL DEFAULT 0,
|
||||
password_rules_json TEXT,
|
||||
allow_referrals INTEGER NOT NULL DEFAULT 0,
|
||||
referral_uses INTEGER,
|
||||
user_expiry_days INTEGER,
|
||||
user_expiry_action TEXT,
|
||||
is_referral INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS password_resets (
|
||||
token TEXT PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
used_at TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS user_expiry (
|
||||
user_id INTEGER PRIMARY KEY,
|
||||
expires_at TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
warning_sent_at TEXT,
|
||||
disabled_at TEXT,
|
||||
deleted_at TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS announcements (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_by TEXT,
|
||||
subject TEXT NOT NULL,
|
||||
body_md TEXT NOT NULL,
|
||||
channels_csv TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS notification_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
channel TEXT NOT NULL,
|
||||
recipient TEXT,
|
||||
status TEXT NOT NULL,
|
||||
detail TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
@@ -271,6 +376,32 @@ def get_user_by_username(username: str) -> Optional[Dict[str, Any]]:
|
||||
}
|
||||
|
||||
|
||||
def get_user_by_email(email: str) -> Optional[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT users.id, users.username, users.password_hash, users.role, users.auth_provider,
|
||||
users.created_at, users.last_login_at, users.is_blocked
|
||||
FROM users
|
||||
JOIN user_contacts ON user_contacts.user_id = users.id
|
||||
WHERE lower(user_contacts.email) = lower(?)
|
||||
""",
|
||||
(email,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return {
|
||||
"id": row[0],
|
||||
"username": row[1],
|
||||
"password_hash": row[2],
|
||||
"role": row[3],
|
||||
"auth_provider": row[4],
|
||||
"created_at": row[5],
|
||||
"last_login_at": row[6],
|
||||
"is_blocked": bool(row[7]),
|
||||
}
|
||||
|
||||
|
||||
def get_all_users() -> list[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
@@ -327,6 +458,17 @@ def set_user_role(username: str, role: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
def delete_user(username: str) -> None:
|
||||
user_id = get_user_id(username)
|
||||
if user_id is None:
|
||||
return
|
||||
with _connect() as conn:
|
||||
conn.execute("DELETE FROM user_contacts WHERE user_id = ?", (user_id,))
|
||||
conn.execute("DELETE FROM user_expiry WHERE user_id = ?", (user_id,))
|
||||
conn.execute("DELETE FROM password_resets WHERE user_id = ?", (user_id,))
|
||||
conn.execute("DELETE FROM users WHERE id = ?", (user_id,))
|
||||
|
||||
|
||||
def verify_user_password(username: str, password: str) -> Optional[Dict[str, Any]]:
|
||||
user = get_user_by_username(username)
|
||||
if not user:
|
||||
@@ -347,6 +489,603 @@ def set_user_password(username: str, password: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
def get_user_id(username: str) -> Optional[int]:
|
||||
with _connect() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT id FROM users WHERE username = ?",
|
||||
(username,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return int(row[0])
|
||||
|
||||
|
||||
def upsert_user_contact(
|
||||
username: str,
|
||||
email: Optional[str] = None,
|
||||
discord: Optional[str] = None,
|
||||
telegram: Optional[str] = None,
|
||||
matrix: Optional[str] = None,
|
||||
verified: Optional[Dict[str, bool]] = None,
|
||||
) -> None:
|
||||
user_id = get_user_id(username)
|
||||
if user_id is None:
|
||||
return
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
verified = verified or {}
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO user_contacts (
|
||||
user_id,
|
||||
email,
|
||||
email_verified,
|
||||
discord,
|
||||
discord_verified,
|
||||
telegram,
|
||||
telegram_verified,
|
||||
matrix,
|
||||
matrix_verified,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
email = COALESCE(excluded.email, user_contacts.email),
|
||||
email_verified = COALESCE(excluded.email_verified, user_contacts.email_verified),
|
||||
discord = COALESCE(excluded.discord, user_contacts.discord),
|
||||
discord_verified = COALESCE(excluded.discord_verified, user_contacts.discord_verified),
|
||||
telegram = COALESCE(excluded.telegram, user_contacts.telegram),
|
||||
telegram_verified = COALESCE(excluded.telegram_verified, user_contacts.telegram_verified),
|
||||
matrix = COALESCE(excluded.matrix, user_contacts.matrix),
|
||||
matrix_verified = COALESCE(excluded.matrix_verified, user_contacts.matrix_verified),
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
email,
|
||||
1 if verified.get("email") else 0,
|
||||
discord,
|
||||
1 if verified.get("discord") else 0,
|
||||
telegram,
|
||||
1 if verified.get("telegram") else 0,
|
||||
matrix,
|
||||
1 if verified.get("matrix") else 0,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def get_user_contact(username: str) -> Optional[Dict[str, Any]]:
|
||||
user_id = get_user_id(username)
|
||||
if user_id is None:
|
||||
return None
|
||||
with _connect() as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT email, email_verified, discord, discord_verified, telegram, telegram_verified, matrix, matrix_verified
|
||||
FROM user_contacts
|
||||
WHERE user_id = ?
|
||||
""",
|
||||
(user_id,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return {
|
||||
"email": row[0],
|
||||
"email_verified": bool(row[1]),
|
||||
"discord": row[2],
|
||||
"discord_verified": bool(row[3]),
|
||||
"telegram": row[4],
|
||||
"telegram_verified": bool(row[5]),
|
||||
"matrix": row[6],
|
||||
"matrix_verified": bool(row[7]),
|
||||
}
|
||||
|
||||
|
||||
def get_all_contacts() -> list[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT users.username, user_contacts.email, user_contacts.discord,
|
||||
user_contacts.telegram, user_contacts.matrix
|
||||
FROM user_contacts
|
||||
JOIN users ON users.id = user_contacts.user_id
|
||||
"""
|
||||
).fetchall()
|
||||
results: list[Dict[str, Any]] = []
|
||||
for row in rows:
|
||||
results.append(
|
||||
{
|
||||
"username": row[0],
|
||||
"email": row[1],
|
||||
"discord": row[2],
|
||||
"telegram": row[3],
|
||||
"matrix": row[4],
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def set_user_expiry(username: str, expires_at: str, action: str) -> None:
|
||||
user_id = get_user_id(username)
|
||||
if user_id is None:
|
||||
return
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO user_expiry (user_id, expires_at, action)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
expires_at = excluded.expires_at,
|
||||
action = excluded.action
|
||||
""",
|
||||
(user_id, expires_at, action),
|
||||
)
|
||||
|
||||
|
||||
def get_expired_users(now_iso: str) -> list[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT users.username, users.is_blocked, user_expiry.expires_at, user_expiry.action, user_expiry.warning_sent_at
|
||||
FROM user_expiry
|
||||
JOIN users ON users.id = user_expiry.user_id
|
||||
WHERE user_expiry.expires_at <= ?
|
||||
AND user_expiry.deleted_at IS NULL
|
||||
""",
|
||||
(now_iso,),
|
||||
).fetchall()
|
||||
results: list[Dict[str, Any]] = []
|
||||
for row in rows:
|
||||
results.append(
|
||||
{
|
||||
"username": row[0],
|
||||
"is_blocked": bool(row[1]),
|
||||
"expires_at": row[2],
|
||||
"action": row[3],
|
||||
"warning_sent_at": row[4],
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def get_users_expiring_by(cutoff_iso: str) -> list[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT users.username, user_expiry.expires_at, user_expiry.action, user_expiry.warning_sent_at
|
||||
FROM user_expiry
|
||||
JOIN users ON users.id = user_expiry.user_id
|
||||
WHERE user_expiry.expires_at <= ?
|
||||
AND user_expiry.warning_sent_at IS NULL
|
||||
""",
|
||||
(cutoff_iso,),
|
||||
).fetchall()
|
||||
results: list[Dict[str, Any]] = []
|
||||
for row in rows:
|
||||
results.append(
|
||||
{
|
||||
"username": row[0],
|
||||
"expires_at": row[1],
|
||||
"action": row[2],
|
||||
"warning_sent_at": row[3],
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def mark_expiry_warning_sent(username: str) -> None:
|
||||
user_id = get_user_id(username)
|
||||
if user_id is None:
|
||||
return
|
||||
timestamp = datetime.now(timezone.utc).isoformat()
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE user_expiry SET warning_sent_at = ? WHERE user_id = ?
|
||||
""",
|
||||
(timestamp, user_id),
|
||||
)
|
||||
|
||||
|
||||
def mark_expiry_disabled(username: str) -> None:
|
||||
user_id = get_user_id(username)
|
||||
if user_id is None:
|
||||
return
|
||||
timestamp = datetime.now(timezone.utc).isoformat()
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE user_expiry SET disabled_at = ? WHERE user_id = ?
|
||||
""",
|
||||
(timestamp, user_id),
|
||||
)
|
||||
|
||||
|
||||
def mark_expiry_deleted(username: str) -> None:
|
||||
user_id = get_user_id(username)
|
||||
if user_id is None:
|
||||
return
|
||||
timestamp = datetime.now(timezone.utc).isoformat()
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE user_expiry SET deleted_at = ? WHERE user_id = ?
|
||||
""",
|
||||
(timestamp, user_id),
|
||||
)
|
||||
|
||||
|
||||
def list_invite_profiles() -> list[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT id, name, description, max_uses, expires_in_days, require_captcha,
|
||||
password_rules_json, allow_referrals, referral_uses, user_expiry_days,
|
||||
user_expiry_action, created_at, updated_at
|
||||
FROM invite_profiles
|
||||
ORDER BY name COLLATE NOCASE
|
||||
"""
|
||||
).fetchall()
|
||||
results: list[Dict[str, Any]] = []
|
||||
for row in rows:
|
||||
results.append(
|
||||
{
|
||||
"id": row[0],
|
||||
"name": row[1],
|
||||
"description": row[2],
|
||||
"max_uses": row[3],
|
||||
"expires_in_days": row[4],
|
||||
"require_captcha": bool(row[5]),
|
||||
"password_rules": json.loads(row[6]) if row[6] else None,
|
||||
"allow_referrals": bool(row[7]),
|
||||
"referral_uses": row[8],
|
||||
"user_expiry_days": row[9],
|
||||
"user_expiry_action": row[10],
|
||||
"created_at": row[11],
|
||||
"updated_at": row[12],
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def get_invite_profile(profile_id: int) -> Optional[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT id, name, description, max_uses, expires_in_days, require_captcha,
|
||||
password_rules_json, allow_referrals, referral_uses, user_expiry_days,
|
||||
user_expiry_action
|
||||
FROM invite_profiles
|
||||
WHERE id = ?
|
||||
""",
|
||||
(profile_id,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return {
|
||||
"id": row[0],
|
||||
"name": row[1],
|
||||
"description": row[2],
|
||||
"max_uses": row[3],
|
||||
"expires_in_days": row[4],
|
||||
"require_captcha": bool(row[5]),
|
||||
"password_rules": json.loads(row[6]) if row[6] else None,
|
||||
"allow_referrals": bool(row[7]),
|
||||
"referral_uses": row[8],
|
||||
"user_expiry_days": row[9],
|
||||
"user_expiry_action": row[10],
|
||||
}
|
||||
|
||||
|
||||
def create_invite_profile(
|
||||
name: str,
|
||||
description: Optional[str] = None,
|
||||
max_uses: Optional[int] = None,
|
||||
expires_in_days: Optional[int] = None,
|
||||
require_captcha: bool = False,
|
||||
password_rules: Optional[Dict[str, Any]] = None,
|
||||
allow_referrals: bool = False,
|
||||
referral_uses: Optional[int] = None,
|
||||
user_expiry_days: Optional[int] = None,
|
||||
user_expiry_action: Optional[str] = None,
|
||||
) -> int:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
rules_json = json.dumps(password_rules, ensure_ascii=True) if password_rules else None
|
||||
with _connect() as conn:
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
INSERT INTO invite_profiles (
|
||||
name,
|
||||
description,
|
||||
max_uses,
|
||||
expires_in_days,
|
||||
require_captcha,
|
||||
password_rules_json,
|
||||
allow_referrals,
|
||||
referral_uses,
|
||||
user_expiry_days,
|
||||
user_expiry_action,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
name,
|
||||
description,
|
||||
max_uses,
|
||||
expires_in_days,
|
||||
1 if require_captcha else 0,
|
||||
rules_json,
|
||||
1 if allow_referrals else 0,
|
||||
referral_uses,
|
||||
user_expiry_days,
|
||||
user_expiry_action,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
return int(cursor.lastrowid)
|
||||
|
||||
|
||||
def create_invite(
|
||||
code: str,
|
||||
created_by: Optional[str],
|
||||
profile_id: Optional[int],
|
||||
expires_at: Optional[str],
|
||||
max_uses: Optional[int],
|
||||
require_captcha: bool,
|
||||
password_rules: Optional[Dict[str, Any]],
|
||||
allow_referrals: bool,
|
||||
referral_uses: Optional[int],
|
||||
user_expiry_days: Optional[int],
|
||||
user_expiry_action: Optional[str],
|
||||
is_referral: bool = False,
|
||||
) -> None:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
rules_json = json.dumps(password_rules, ensure_ascii=True) if password_rules else None
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO invites (
|
||||
code,
|
||||
profile_id,
|
||||
created_by,
|
||||
created_at,
|
||||
expires_at,
|
||||
max_uses,
|
||||
require_captcha,
|
||||
password_rules_json,
|
||||
allow_referrals,
|
||||
referral_uses,
|
||||
user_expiry_days,
|
||||
user_expiry_action,
|
||||
is_referral
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
code,
|
||||
profile_id,
|
||||
created_by,
|
||||
now,
|
||||
expires_at,
|
||||
max_uses,
|
||||
1 if require_captcha else 0,
|
||||
rules_json,
|
||||
1 if allow_referrals else 0,
|
||||
referral_uses,
|
||||
user_expiry_days,
|
||||
user_expiry_action,
|
||||
1 if is_referral else 0,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def list_invites(limit: int = 200) -> list[Dict[str, Any]]:
|
||||
limit = max(1, min(limit, 500))
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT id, code, profile_id, created_by, created_at, expires_at, max_uses, uses_count,
|
||||
disabled, require_captcha, password_rules_json, allow_referrals, referral_uses,
|
||||
user_expiry_days, user_expiry_action, is_referral
|
||||
FROM invites
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
results: list[Dict[str, Any]] = []
|
||||
for row in rows:
|
||||
results.append(
|
||||
{
|
||||
"id": row[0],
|
||||
"code": row[1],
|
||||
"profile_id": row[2],
|
||||
"created_by": row[3],
|
||||
"created_at": row[4],
|
||||
"expires_at": row[5],
|
||||
"max_uses": row[6],
|
||||
"uses_count": row[7],
|
||||
"disabled": bool(row[8]),
|
||||
"require_captcha": bool(row[9]),
|
||||
"password_rules": json.loads(row[10]) if row[10] else None,
|
||||
"allow_referrals": bool(row[11]),
|
||||
"referral_uses": row[12],
|
||||
"user_expiry_days": row[13],
|
||||
"user_expiry_action": row[14],
|
||||
"is_referral": bool(row[15]),
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def list_invites_by_creator(username: str, is_referral: bool = False) -> list[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT code, created_at, expires_at, max_uses, uses_count, disabled, is_referral
|
||||
FROM invites
|
||||
WHERE created_by = ? AND is_referral = ?
|
||||
ORDER BY created_at DESC
|
||||
""",
|
||||
(username, 1 if is_referral else 0),
|
||||
).fetchall()
|
||||
results: list[Dict[str, Any]] = []
|
||||
for row in rows:
|
||||
results.append(
|
||||
{
|
||||
"code": row[0],
|
||||
"created_at": row[1],
|
||||
"expires_at": row[2],
|
||||
"max_uses": row[3],
|
||||
"uses_count": row[4],
|
||||
"disabled": bool(row[5]),
|
||||
"is_referral": bool(row[6]),
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def get_invite_by_code(code: str) -> Optional[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT id, code, profile_id, created_by, created_at, expires_at, max_uses, uses_count,
|
||||
disabled, require_captcha, password_rules_json, allow_referrals, referral_uses,
|
||||
user_expiry_days, user_expiry_action, is_referral
|
||||
FROM invites
|
||||
WHERE code = ?
|
||||
""",
|
||||
(code,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return {
|
||||
"id": row[0],
|
||||
"code": row[1],
|
||||
"profile_id": row[2],
|
||||
"created_by": row[3],
|
||||
"created_at": row[4],
|
||||
"expires_at": row[5],
|
||||
"max_uses": row[6],
|
||||
"uses_count": row[7],
|
||||
"disabled": bool(row[8]),
|
||||
"require_captcha": bool(row[9]),
|
||||
"password_rules": json.loads(row[10]) if row[10] else None,
|
||||
"allow_referrals": bool(row[11]),
|
||||
"referral_uses": row[12],
|
||||
"user_expiry_days": row[13],
|
||||
"user_expiry_action": row[14],
|
||||
"is_referral": bool(row[15]),
|
||||
}
|
||||
|
||||
|
||||
def increment_invite_use(code: str) -> None:
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE invites SET uses_count = uses_count + 1 WHERE code = ?
|
||||
""",
|
||||
(code,),
|
||||
)
|
||||
|
||||
|
||||
def disable_invite(code: str) -> None:
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE invites SET disabled = 1 WHERE code = ?
|
||||
""",
|
||||
(code,),
|
||||
)
|
||||
|
||||
|
||||
def delete_invite(code: str) -> None:
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
DELETE FROM invites WHERE code = ?
|
||||
""",
|
||||
(code,),
|
||||
)
|
||||
|
||||
|
||||
def create_password_reset(token: str, username: str, expires_at: str) -> None:
|
||||
user_id = get_user_id(username)
|
||||
if user_id is None:
|
||||
return
|
||||
created_at = datetime.now(timezone.utc).isoformat()
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO password_resets (token, user_id, created_at, expires_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(token, user_id, created_at, expires_at),
|
||||
)
|
||||
|
||||
|
||||
def get_password_reset(token: str) -> Optional[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT users.username, password_resets.expires_at, password_resets.used_at
|
||||
FROM password_resets
|
||||
JOIN users ON users.id = password_resets.user_id
|
||||
WHERE password_resets.token = ?
|
||||
""",
|
||||
(token,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return {"username": row[0], "expires_at": row[1], "used_at": row[2]}
|
||||
|
||||
|
||||
def mark_password_reset_used(token: str) -> None:
|
||||
used_at = datetime.now(timezone.utc).isoformat()
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE password_resets SET used_at = ? WHERE token = ?
|
||||
""",
|
||||
(used_at, token),
|
||||
)
|
||||
|
||||
|
||||
def save_announcement(
|
||||
created_by: Optional[str],
|
||||
subject: str,
|
||||
body_md: str,
|
||||
channels_csv: Optional[str],
|
||||
) -> None:
|
||||
created_at = datetime.now(timezone.utc).isoformat()
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO announcements (created_by, subject, body_md, channels_csv, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(created_by, subject, body_md, channels_csv, created_at),
|
||||
)
|
||||
|
||||
|
||||
def log_notification(channel: str, recipient: Optional[str], status: str, detail: Optional[str]) -> None:
|
||||
created_at = datetime.now(timezone.utc).isoformat()
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO notification_log (channel, recipient, status, detail, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(channel, recipient, status, detail, created_at),
|
||||
)
|
||||
|
||||
|
||||
def _backfill_auth_providers() -> None:
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
|
||||
Reference in New Issue
Block a user