Files
quiz/app/config.py
ameer 74c1745559 feat(roster): gate joins on registered student-ID list
Adds an optional roster.json (set of allowed student IDs) loaded at
startup. add_participant() raises StudentIdNotInRoster when the gate is
on and the supplied id is not present; route returns 403 with a clear
message and logs a roster_reject audit event. Names are NOT checked
against the roster: the join form asks for a current name as a soft
deterrent, but the only hard check is the id.

Includes a deploy/build_roster.py helper that turns class_register
attendance.xlsx into roster.json. Bootstrap env file now exports
QUIZ_ROSTER_PATH; missing file disables the gate (legacy behaviour).

Also drops the user-facing "The cookie is per-device." line from the
join card — students don't need to know the implementation; replaced
with "Enter your registered student ID and your current full name."
2026-05-05 22:02:03 +08:00

54 lines
1.7 KiB
Python

"""Application configuration."""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
def load_dotenv(path: str | Path = ".env") -> None:
env_path = Path(path)
if not env_path.exists():
return
for raw_line in env_path.read_text().splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
@dataclass(slots=True)
class Settings:
db_path: str = "./quiz.db"
secret_key: str | None = None
admin_password: str | None = None
host: str = "127.0.0.1"
port: int = 8001
public_url: str = "https://quiz.ahkhan.me"
log_level: str = "INFO"
pool_path: str = "./pool.json"
roster_path: str = "./roster.json"
default_session_id: str = "main"
@classmethod
def from_env(cls) -> "Settings":
load_dotenv()
return cls(
db_path=os.getenv("QUIZ_DB_PATH", "./quiz.db"),
secret_key=os.getenv("QUIZ_SECRET_KEY"),
admin_password=os.getenv("QUIZ_ADMIN_PASSWORD"),
host=os.getenv("QUIZ_HOST", "127.0.0.1"),
port=int(os.getenv("QUIZ_PORT", "8001")),
public_url=os.getenv("QUIZ_PUBLIC_URL", "https://quiz.ahkhan.me").rstrip("/"),
log_level=os.getenv("QUIZ_LOG_LEVEL", "INFO"),
pool_path=os.getenv("QUIZ_POOL_PATH", "./pool.json"),
roster_path=os.getenv("QUIZ_ROSTER_PATH", "./roster.json"),
default_session_id=os.getenv("QUIZ_SESSION_ID", "main"),
)
@property
def secure_cookies(self) -> bool:
return self.public_url.startswith("https://")