Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ HISTORY_LOOKBACK_DAYS=30
LIBRARYSYNC_JWT_ACCESS_TOKEN_MINUTES=60
LIBRARYSYNC_JWT_ALGORITHM=HS256
LIBRARYSYNC_ALLOW_REGISTRATION=true
LIBRARYSYNC_MAX_USERS=1
LIBRARYSYNC_GZIP_ENABLED=true
LIBRARYSYNC_GZIP_MIN_SIZE=500
# Enable or disable dashboard statistics (set to false to disable)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ All defaults below are from `.env.example`.
- `LIBRARYSYNC_JWT_ACCESS_TOKEN_MINUTES` (default `60`): access token lifetime.
- `LIBRARYSYNC_JWT_ALGORITHM` (default `HS256`): JWT signing algorithm.
- `LIBRARYSYNC_ALLOW_REGISTRATION` (default `true`): enable `/api/auth/register`.
- `LIBRARYSYNC_MAX_USERS` (default `1`): max number of registered users (`-1` for unlimited).

### OAuth
- `TRAKT_CLIENT_ID` (default `your_trakt_client_id`): Trakt OAuth app client ID.
Expand Down
9 changes: 8 additions & 1 deletion backend/src/librarysync/api/routes_auth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from librarysync.api.deps import get_current_user, get_db
Expand Down Expand Up @@ -35,6 +35,13 @@ def _normalize_username(username: str) -> str:
async def register(payload: LoginRequest, db: AsyncSession = Depends(get_db)) -> UserOut:
if not settings.allow_registration:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Registration disabled")
if settings.max_users >= 0:
result = await db.execute(select(func.count(User.id)))
user_count = result.scalar_one()
if user_count >= settings.max_users:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="User limit reached"
)
username = _normalize_username(payload.username)
result = await db.execute(select(User).where(User.username == username))
existing = result.scalar_one_or_none()
Expand Down
2 changes: 2 additions & 0 deletions backend/src/librarysync/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Settings:
jwt_access_token_minutes: int
jwt_algorithm: str
allow_registration: bool
max_users: int
gzip_enabled: bool
gzip_min_size: int
trakt_rate_limit_per_minute: int
Expand Down Expand Up @@ -59,6 +60,7 @@ def load_settings() -> Settings:
allow_registration=(
(_get_env("LIBRARYSYNC_ALLOW_REGISTRATION", "true") or "true").lower() == "true"
),
max_users=int(_get_env("LIBRARYSYNC_MAX_USERS", "1") or "1"),
gzip_enabled=(
(_get_env("LIBRARYSYNC_GZIP_ENABLED", "true") or "true").lower() == "true"
),
Expand Down
12 changes: 11 additions & 1 deletion backend/src/librarysync/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from librarysync.api import (
routes_activity,
Expand All @@ -19,7 +21,7 @@
routes_settings,
routes_watchlist,
)
from librarysync.api.deps import get_optional_user
from librarysync.api.deps import get_db, get_optional_user
from librarysync.config import settings
from librarysync.db.migrate import run_migrations
from librarysync.db.models import User
Expand Down Expand Up @@ -216,14 +218,22 @@ async def index(
async def login(
request: Request,
current_user: User | None = Depends(get_optional_user),
db: AsyncSession = Depends(get_db),
):
registration_full = False
if settings.allow_registration and settings.max_users >= 0:
result = await db.execute(select(func.count(User.id)))
user_count = result.scalar_one()
registration_full = user_count >= settings.max_users
return _render_page(
request,
"login.html",
page_title="Login",
active_page="login",
guest_only=True,
allow_registration=settings.allow_registration,
registration_full=registration_full,
max_users=settings.max_users,
current_user=current_user,
)

Expand Down
11 changes: 9 additions & 2 deletions backend/src/librarysync/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ <h2 class="section-title">Log In</h2>
<p id="login-message" class="form-message" role="status" hidden></p>
</form>
</section>
{% if allow_registration %}
{% if allow_registration and not registration_full %}
<section class="card">
<div class="card-header">
<div>
Expand All @@ -83,11 +83,18 @@ <h2 class="section-title">Create an account</h2>
<p id="register-message" class="form-message" role="status" hidden></p>
</form>
</section>
{% elif allow_registration and registration_full %}
<section class="card">
<h2 class="section-title">Registration full</h2>
<p class="mt-2 text-sm text-muted">
This server has reached its user limit.
</p>
</section>
{% else %}
<section class="card">
<h2 class="section-title">Registrations closed</h2>
<p class="mt-2 text-sm text-muted">
Registration is disabled on this server. Ask an admin to create your account.
Registration is disabled on this server.
</p>
</section>
{% endif %}
Expand Down