This commit is contained in:
2026-03-10 15:34:22 +00:00
parent 7635caa71d
commit d641e181ba
17 changed files with 347 additions and 7 deletions

View File

@@ -5,6 +5,8 @@ from contextlib import contextmanager
from datetime import datetime, timedelta, timezone
from pathlib import Path
from werkzeug.security import check_password_hash, generate_password_hash
DATA_PATH = os.environ.get("DATA_PATH", "/app/data")
DB_PATH = Path(DATA_PATH) / "monitor.db"
@@ -55,6 +57,84 @@ def _migrate_add_rollups(conn):
conn.execute("ALTER TABLE uptime_rollups ADD COLUMN response_count INTEGER NOT NULL DEFAULT 0")
def _migrate_add_users(conn):
"""Create users table for authentication."""
conn.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
is_admin INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)")
def _seed_admin_if_empty(conn):
"""Create initial admin user from env if no users exist."""
row = conn.execute("SELECT COUNT(*) FROM users").fetchone()
if row[0] > 0:
return
admin_user = os.environ.get("ADMIN_USER")
admin_password = os.environ.get("ADMIN_PASSWORD")
if not admin_user or not admin_password:
return
password_hash = generate_password_hash(admin_password)
conn.execute(
"INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?, ?, 1, ?)",
(admin_user, password_hash, datetime.utcnow().isoformat()),
)
def create_user(username: str, password: str, is_admin: bool = False) -> int | None:
"""Create a new user. Returns user id or None if username exists."""
username = username.strip()
if not username or not password:
return None
password_hash = generate_password_hash(password)
with get_db() as conn:
try:
cur = conn.execute(
"INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?, ?, ?, ?)",
(username, password_hash, 1 if is_admin else 0, datetime.utcnow().isoformat()),
)
return cur.lastrowid
except sqlite3.IntegrityError:
return None
def get_user_by_id(user_id: int) -> dict | None:
"""Get a user by id."""
with get_db() as conn:
row = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
return dict(row) if row else None
def get_user_by_username(username: str) -> dict | None:
"""Get a user by username."""
with get_db() as conn:
row = conn.execute("SELECT * FROM users WHERE username = ?", (username.strip(),)).fetchone()
return dict(row) if row else None
def verify_user(username: str, password: str) -> dict | None:
"""Verify credentials and return user dict if valid."""
user = get_user_by_username(username)
if not user or not check_password_hash(user["password_hash"], password):
return None
return user
def list_users():
"""Return all users (id, username, is_admin, created_at)."""
with get_db() as conn:
rows = conn.execute(
"SELECT id, username, is_admin, created_at FROM users ORDER BY username"
).fetchall()
return [dict(r) for r in rows]
@contextmanager
def get_db():
_ensure_data_dir()
@@ -97,6 +177,8 @@ def init_db():
_migrate_add_status(conn)
conn.execute("CREATE INDEX IF NOT EXISTS idx_checks_status ON checks(status)")
_migrate_add_rollups(conn)
_migrate_add_users(conn)
_seed_admin_if_empty(conn)
def list_services():