Standalone Shonar Desktop: vendor portable sources + local engine; decouple from ~/Projects/Shonar
- shared/ = portable Android-origin sources vendored from deferred/desktop-server (app/build.gradle.kts srcDir repointed; PlaybackController.kt excluded as Android-only) - backend/ = bundled-lite engine (SQLite + inline queue); .venv symlinked from the old checkout, PYTHONPATH pins THIS backend's code over any editable install - repoRoot() resolves this project dir (env SHONAR_REPO still wins); desktop-dev.sh watches shared/ + backend/ - Verified: :app:compileKotlin + :app:test green (23 tests); engine boots on :8010, self-migrates, /healthz ok
This commit is contained in:
commit
76c867fca4
136 changed files with 21099 additions and 0 deletions
91
backend/shonar/api/v1/auth.py
Normal file
91
backend/shonar/api/v1/auth.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
"""Auth endpoints (rate-limited)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
|
||||
from shonar.api.deps import CurrentUser, SessionDep
|
||||
from shonar.api.schemas_common import (
|
||||
DeleteAccountRequest,
|
||||
LoginRequest,
|
||||
LogoutRequest,
|
||||
RefreshRequest,
|
||||
RegisterRequest,
|
||||
TokenPair,
|
||||
UserOut,
|
||||
)
|
||||
from shonar.core.config import get_settings
|
||||
from shonar.core.ratelimit import auth_limit
|
||||
from shonar.core.security import create_access_token
|
||||
from shonar.services import auth as auth_service
|
||||
from shonar.services.auth import AuthError
|
||||
|
||||
router = APIRouter(tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/auth/register", response_model=TokenPair, status_code=201)
|
||||
@auth_limit
|
||||
async def register(body: RegisterRequest, request: Request, session: SessionDep):
|
||||
settings = get_settings()
|
||||
if not settings.allow_registration:
|
||||
raise HTTPException(403, "Registration is disabled on this server.") from None
|
||||
try:
|
||||
user = await auth_service.register_user(
|
||||
session, body.email, body.password, body.display_name
|
||||
)
|
||||
except AuthError as e:
|
||||
raise HTTPException(e.status_code, e.message) from None
|
||||
access, ttl = create_access_token(user.id)
|
||||
from shonar.services.auth import issue_refresh_token
|
||||
|
||||
refresh, _ = await issue_refresh_token(session, user.id, None, None)
|
||||
return TokenPair(access_token=access, expires_in=ttl, refresh_token=refresh)
|
||||
|
||||
|
||||
@router.post("/auth/login", response_model=TokenPair)
|
||||
@auth_limit
|
||||
async def login(body: LoginRequest, request: Request, session: SessionDep):
|
||||
try:
|
||||
user, refresh, device = await auth_service.login(
|
||||
session, body.email, body.password, body.device_name, body.platform
|
||||
)
|
||||
except AuthError as e:
|
||||
raise HTTPException(e.status_code, e.message) from None
|
||||
access, ttl = create_access_token(user.id, device.id)
|
||||
return TokenPair(
|
||||
access_token=access, expires_in=ttl, refresh_token=refresh, device_id=device.id
|
||||
)
|
||||
|
||||
|
||||
@router.post("/auth/refresh", response_model=TokenPair)
|
||||
@auth_limit
|
||||
async def refresh(body: RefreshRequest, request: Request, session: SessionDep):
|
||||
try:
|
||||
user, new_refresh, device_id = await auth_service.rotate_refresh_token(
|
||||
session, body.refresh_token
|
||||
)
|
||||
except AuthError as e:
|
||||
raise HTTPException(e.status_code, e.message) from None
|
||||
access, ttl = create_access_token(user.id, device_id)
|
||||
return TokenPair(
|
||||
access_token=access, expires_in=ttl, refresh_token=new_refresh, device_id=device_id
|
||||
)
|
||||
|
||||
|
||||
@router.post("/auth/logout", status_code=204)
|
||||
async def logout(body: LogoutRequest, session: SessionDep):
|
||||
await auth_service.logout(session, body.refresh_token)
|
||||
|
||||
|
||||
@router.get("/auth/me", response_model=UserOut)
|
||||
async def me(user: CurrentUser):
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/auth/delete-account", status_code=202)
|
||||
async def delete_account(body: DeleteAccountRequest, user: CurrentUser, session: SessionDep):
|
||||
try:
|
||||
await auth_service.delete_account(session, user, body.password)
|
||||
except AuthError as e:
|
||||
raise HTTPException(e.status_code, e.message) from None
|
||||
return {"detail": "Account scheduled for deletion.", "grace_days": 30}
|
||||
Loading…
Add table
Add a link
Reference in a new issue