Backend: - routes_auth: POST /v1/auth/dev-login — bypass Azure AD (disabled in production), creates admin in DB and sets JWT cookie; takes email + full_name - routes_auth: use settings.frontend_callback_url instead of parsing CORS origins for SSO post-login redirect — configurable per environment - config: add frontend_callback_url setting - dependencies: fix get_current_admin — was querying _id as string (ObjectId bug) and filtering is_active:True (never set by SSO flow) Frontend: - Login.tsx: dev login form shown in non-production builds below SSO button - api.ts: use import.meta.env.BASE_URL so API paths work under any subpath prefix - main.tsx: pass BASE_URL as BrowserRouter basename for correct SPA routing - vite.config.ts: read VITE_BASE_PATH env var to set Vite base (default /) - nginx.conf: serve app at /cost-tracker/ prefix, proxy API routes internally - Dockerfile: accept VITE_BASE_PATH build arg, copy build to /cost-tracker/ subdir Infra: - docker-compose.yml: API host port 8003 (8001 taken by ppt-tool on optical-dev) - infra/deploy/apache-cost-tracker.conf: Apache include for optical-dev routing - infra/deploy/deploy.sh: one-shot deploy script (clone/pull, build, Apache config) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
46 lines
1.5 KiB
Python
46 lines
1.5 KiB
Python
from typing import Optional
|
|
|
|
from bson import ObjectId
|
|
from fastapi import Cookie, Depends, HTTPException, Request, status
|
|
from motor.motor_asyncio import AsyncIOMotorDatabase
|
|
|
|
from .database import get_database
|
|
from .security import decode_token
|
|
|
|
|
|
async def get_db(db: AsyncIOMotorDatabase = Depends(get_database)) -> AsyncIOMotorDatabase:
|
|
return db
|
|
|
|
|
|
async def get_current_admin(
|
|
request: Request,
|
|
access_token: Optional[str] = Cookie(default=None),
|
|
db: AsyncIOMotorDatabase = Depends(get_db),
|
|
) -> dict:
|
|
"""Require authenticated admin (Microsoft SSO or dev JWT stored in cookie)."""
|
|
token = access_token
|
|
if not token:
|
|
auth_header = request.headers.get("Authorization", "")
|
|
if auth_header.startswith("Bearer "):
|
|
token = auth_header[7:]
|
|
|
|
if not token:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
|
|
|
|
payload = decode_token(token)
|
|
if not payload:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
|
|
|
admin_id = payload.get("sub")
|
|
if not admin_id:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
|
|
|
try:
|
|
admin = await db.admins.find_one({"_id": ObjectId(admin_id)})
|
|
except Exception:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
|
|
|
if not admin:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin not found")
|
|
|
|
return admin
|