mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-07-31 09:06:06 -04:00
Hey! @maxdorninger, could you please take a look on the other one ;) ? Closes #145 ; seems like a must have thing - Add `auth.registration_enabled` flag (default `false` — sounds safer default in current setup) - Password registration: `/auth/register` route will return 403 if disabled - OIDC registration: `UserManager.oauth_callback` is overridden to reject user without match; so no auto-provision - `/auth/metadata` exposes the flag to the frontend - Frontend: signup link hidden on the login card; direct navigation to `/login/signup` redirects to `/login` via a `+page.ts` load guard <img width="463" height="461" alt="Screenshot 2026-05-16 at 20 52 11" src="https://github.com/user-attachments/assets/1def5142-e930-4aa6-8771-cbff54250c1f" /> <img width="450" height="391" alt="Screenshot 2026-05-16 at 20 52 22" src="https://github.com/user-attachments/assets/b4013964-cace-4aeb-a848-48ced86fcc5f" /> Also this means uses need to be created somehow -- so.. - Add POST `/users` - admin-protected endpoint to create a new users (created user can be used with OIDC) - Fronted: Add new user button and modal dialog <img width="800" height="379" alt="Screenshot 2026-05-17 at 11 32 06" src="https://github.com/user-attachments/assets/048e6c43-a1c1-42ce-a19c-fd9d916a47d9" /> <img width="537" height="439" alt="Screenshot 2026-05-17 at 11 32 12" src="https://github.com/user-attachments/assets/3f83c2c1-027b-4279-b6a1-bbc8da77efed" /> --- <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Add config toggle to enable/disable user registration; when disabled sign-up endpoints return 403 and OIDC won’t auto-create unknown users. Added admin API to create users. * **Frontend** * Login UI hides signup link when registration is disabled; signup page redirects to login. Admin users list gains “Add User” modal to create users. * **Documentation** * Authentication docs and config examples updated to document the new option. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/maxdorninger/MediaManager/pull/543?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
128 lines
3.9 KiB
Python
128 lines
3.9 KiB
Python
import uuid
|
|
from collections.abc import AsyncGenerator
|
|
from contextlib import asynccontextmanager
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, FastAPI, HTTPException, status
|
|
from fastapi_users import BaseUserManager, exceptions
|
|
from fastapi_users.router import get_oauth_router
|
|
from httpx_oauth.oauth2 import OAuth2
|
|
from sqlalchemy import select
|
|
|
|
from media_manager.auth.db import User
|
|
from media_manager.auth.schemas import (
|
|
AdminUserCreate,
|
|
AuthMetadata,
|
|
UserCreate,
|
|
UserRead,
|
|
)
|
|
from media_manager.auth.users import (
|
|
SECRET,
|
|
create_default_admin_user,
|
|
current_superuser,
|
|
fastapi_users,
|
|
get_user_manager,
|
|
openid_client,
|
|
openid_cookie_auth_backend,
|
|
)
|
|
from media_manager.config import MediaManagerConfig
|
|
from media_manager.database import DbSessionDependency
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_app: FastAPI) -> AsyncGenerator:
|
|
await create_default_admin_user()
|
|
yield
|
|
|
|
|
|
users_router = APIRouter(lifespan=lifespan)
|
|
auth_metadata_router = APIRouter()
|
|
|
|
|
|
def get_openid_router() -> APIRouter:
|
|
if openid_client:
|
|
return get_oauth_router(
|
|
oauth_client=openid_client,
|
|
backend=openid_cookie_auth_backend,
|
|
get_user_manager=fastapi_users.get_user_manager,
|
|
state_secret=SECRET,
|
|
associate_by_email=True,
|
|
is_verified_by_default=True,
|
|
redirect_url=None,
|
|
)
|
|
# this is there, so that the appropriate routes are created even if OIDC is not configured,
|
|
# e.g. for generating the frontend's openapi client
|
|
return get_oauth_router(
|
|
oauth_client=OAuth2(
|
|
client_id="mock",
|
|
client_secret="mock", # noqa: S106
|
|
authorize_endpoint="https://example.com/authorize",
|
|
access_token_endpoint="https://example.com/token", # noqa: S106
|
|
),
|
|
backend=openid_cookie_auth_backend,
|
|
get_user_manager=fastapi_users.get_user_manager,
|
|
state_secret=SECRET,
|
|
associate_by_email=False,
|
|
is_verified_by_default=False,
|
|
redirect_url=None,
|
|
)
|
|
|
|
|
|
auth_config = MediaManagerConfig().auth
|
|
openid_config = auth_config.openid_connect
|
|
|
|
|
|
@users_router.get(
|
|
"/users/all",
|
|
status_code=status.HTTP_200_OK,
|
|
dependencies=[Depends(current_superuser)],
|
|
)
|
|
async def get_all_users(db: DbSessionDependency) -> list[UserRead]:
|
|
stmt = select(User)
|
|
result = (await db.execute(stmt)).scalars().unique()
|
|
return [UserRead.model_validate(user) for user in result]
|
|
|
|
|
|
@users_router.post(
|
|
"/users/",
|
|
status_code=status.HTTP_201_CREATED,
|
|
dependencies=[Depends(current_superuser)],
|
|
)
|
|
async def admin_create_user(
|
|
payload: AdminUserCreate,
|
|
user_manager: Annotated[
|
|
BaseUserManager[User, uuid.UUID], Depends(get_user_manager)
|
|
],
|
|
) -> UserRead:
|
|
password = payload.password or user_manager.password_helper.generate()
|
|
try:
|
|
user = await user_manager.create(
|
|
UserCreate(
|
|
email=payload.email,
|
|
password=password,
|
|
is_superuser=payload.is_superuser,
|
|
is_verified=payload.is_verified,
|
|
),
|
|
safe=False,
|
|
)
|
|
except exceptions.UserAlreadyExists as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail="A user with this email already exists.",
|
|
) from exc
|
|
except exceptions.InvalidPasswordException as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Invalid password: {exc.reason}",
|
|
) from exc
|
|
return UserRead.model_validate(user)
|
|
|
|
|
|
@auth_metadata_router.get("/auth/metadata", status_code=status.HTTP_200_OK)
|
|
def get_auth_metadata() -> AuthMetadata:
|
|
providers = [openid_config.name] if openid_config.enabled else []
|
|
return AuthMetadata(
|
|
oauth_providers=providers,
|
|
registration_enabled=auth_config.registration_enabled,
|
|
)
|