limit max number of users
This commit is contained in:
+17
-1
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user