limit max number of users

This commit is contained in:
Davide Scaini
2026-04-10 12:35:34 +02:00
parent cbac82a2ba
commit 683b7d9c1b
4 changed files with 62 additions and 3 deletions
+17 -1
View File
@@ -18,8 +18,10 @@ console = Console()
@click.option("--port", default=4041, help="Bind port (default: 4041)")
@click.option("--strava-client-id", default=None, envvar="STRAVA_CLIENT_ID", help="Strava OAuth client ID (enables per-user Strava sync)")
@click.option("--strava-client-secret", default=None, envvar="STRAVA_CLIENT_SECRET", help="Strava OAuth client secret")
@click.option("--max-users", default=None, type=int, help="Override max users for this instance (0 = unlimited; updates the DB setting)")
def serve(data_dir: str, site_dir: Optional[str], host: str, port: int,
strava_client_id: Optional[str], strava_client_secret: Optional[str]) -> None:
strava_client_id: Optional[str], strava_client_secret: Optional[str],
max_users: Optional[int]) -> None:
"""Start the bincio multi-user application server.
Handles auth, user management, and write operations.
@@ -29,6 +31,7 @@ def serve(data_dir: str, site_dir: Optional[str], host: str, port: int,
"""
import uvicorn
import bincio.serve.server as srv
from bincio.serve.db import open_db, set_setting, get_setting
dd = Path(data_dir).expanduser().resolve()
if not (dd / "instance.db").exists():
@@ -36,6 +39,11 @@ def serve(data_dir: str, site_dir: Optional[str], host: str, port: int,
f"No instance.db found in {dd}. Run `bincio init --data-dir {dd}` first."
)
if max_users is not None:
db = open_db(dd)
set_setting(db, "max_users", str(max_users))
db.close()
srv.data_dir = dd
if site_dir:
srv.site_dir = Path(site_dir).expanduser().resolve()
@@ -44,11 +52,19 @@ def serve(data_dir: str, site_dir: Optional[str], host: str, port: int,
if strava_client_secret:
srv.strava_client_secret = strava_client_secret
db = open_db(dd)
current_limit = get_setting(db, "max_users")
db.close()
console.print(f"[bold]bincio serve[/bold]")
console.print(f" Data: [cyan]{dd}[/cyan]")
if srv.site_dir:
console.print(f" Site: [cyan]{srv.site_dir}[/cyan]")
console.print(f" URL: [cyan]http://{host}:{port}[/cyan]")
if current_limit and int(current_limit) > 0:
console.print(f" Users: [yellow]max {current_limit}[/yellow]")
else:
console.print(f" Users: [dim]unlimited[/dim]")
console.print()
uvicorn.run(srv.app, host=host, port=port, log_level="info")
+27
View File
@@ -45,6 +45,11 @@ CREATE TABLE IF NOT EXISTS invites (
used_at INTEGER
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS sessions_handle ON sessions(handle);
CREATE INDEX IF NOT EXISTS invites_created_by ON invites(created_by);
"""
@@ -149,6 +154,28 @@ def delete_user(db: sqlite3.Connection, handle: str) -> None:
db.commit()
def count_users(db: sqlite3.Connection) -> int:
"""Return the total number of registered users."""
row = db.execute("SELECT COUNT(*) FROM users").fetchone()
return row[0] if row else 0
# ── Settings ──────────────────────────────────────────────────────────────────
def get_setting(db: sqlite3.Connection, key: str) -> Optional[str]:
row = db.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
return row["value"] if row else None
def set_setting(db: sqlite3.Connection, key: str, value: str) -> None:
db.execute(
"INSERT INTO settings (key, value) VALUES (?, ?) "
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
(key, value),
)
db.commit()
# ── Sessions ──────────────────────────────────────────────────────────────────
def create_session(db: sqlite3.Connection, handle: str) -> str:
+10 -2
View File
@@ -17,13 +17,14 @@ console = Console()
@click.option("--password", required=True, hide_input=True, confirmation_prompt=True, help="Admin password")
@click.option("--display-name", default="", help="Admin display name (defaults to handle)")
@click.option("--name", default="", help="Instance name shown in the feed")
def init(data_dir: str, handle: str, password: str, display_name: str, name: str) -> None:
@click.option("--max-users", default=0, type=int, help="Maximum number of users allowed (0 = unlimited)")
def init(data_dir: str, handle: str, password: str, display_name: str, name: str, max_users: int) -> None:
"""Bootstrap a fresh bincio multi-user instance.
Creates the SQLite database, the admin user, the per-user data directory,
and prints a first invite code. Safe to re-run — skips steps already done.
"""
from bincio.serve.db import create_invite, create_user, get_user, open_db
from bincio.serve.db import create_invite, create_user, get_user, open_db, set_setting
dd = Path(data_dir).expanduser().resolve()
dd.mkdir(parents=True, exist_ok=True)
@@ -67,6 +68,13 @@ def init(data_dir: str, handle: str, password: str, display_name: str, name: str
else:
console.print(" [yellow]·[/yellow] root index.json already exists — skipping")
# ── User limit ────────────────────────────────────────────────────────────
if max_users > 0:
set_setting(db, "max_users", str(max_users))
console.print(f" [green]✓[/green] user limit set to {max_users}")
else:
console.print(" [dim]·[/dim] no user limit (unlimited)")
# ── First invite code ─────────────────────────────────────────────────────
code = create_invite(db, handle)
+8
View File
@@ -25,10 +25,12 @@ from bincio.serve.db import (
authenticate,
create_invite,
create_session,
count_users,
create_user,
delete_session,
get_invite,
get_session,
get_setting,
get_user,
list_invites,
list_users,
@@ -221,6 +223,12 @@ async def register(request: Request) -> JSONResponse:
if get_user(_get_db(), handle):
raise HTTPException(409, "Handle already taken")
max_users_val = get_setting(_get_db(), "max_users")
if max_users_val is not None:
limit = int(max_users_val)
if limit > 0 and count_users(_get_db()) >= limit:
raise HTTPException(403, f"This instance has reached its user limit ({limit})")
create_user(_get_db(), handle, display, password, is_admin=False)
use_invite(_get_db(), code, handle)