feat: Ipad app production readiness, Colony orchestration, Social posting
All checks were successful
Production Readiness / backend-contracts (pull_request) Successful in 3m19s
Production Readiness / webos-typecheck (pull_request) Successful in 2m38s
Production Readiness / ipad-parse (pull_request) Successful in 1m44s

This commit is contained in:
Sayan Datta
2026-05-03 18:28:04 +05:30
parent acfc602157
commit 6c93e31741
86 changed files with 20349 additions and 1655 deletions

View File

@@ -2,13 +2,15 @@ from __future__ import annotations
import os
import re
from datetime import datetime, timezone
import secrets
import hashlib
from datetime import datetime, timedelta, timezone
from pathlib import Path
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status
from pydantic import BaseModel
from backend.auth.dependencies import UserPrincipal, get_current_user
from backend.auth.dependencies import UserPrincipal, create_access_token, get_current_user
from backend.auth.service import (
list_tenant_users,
login_with_directory,
@@ -31,6 +33,37 @@ class LoginRequest(BaseModel):
password: str
class PasswordRecoveryRequest(BaseModel):
email: str
class SessionSwitchRequest(BaseModel):
userId: str
def _role_level(role: str) -> int:
levels = {"JUNIOR_BROKER": 0, "SENIOR_BROKER": 1, "SALES_DIRECTOR": 2, "ADMIN": 3, "SUPERADMIN": 4}
return levels.get(role.upper(), -1)
def _sso_provider_descriptor(provider: str, tenant_id: str) -> dict[str, str | bool]:
normalized = provider.strip().lower()
env_prefix = f"VELOCITY_SSO_{normalized.upper().replace('-', '_')}"
kind = os.getenv(f"{env_prefix}_TYPE", "oauth").strip().lower()
return {
"id": normalized,
"name": os.getenv(f"{env_prefix}_NAME", normalized.replace("_", " ").title()),
"type": kind,
"tenantId": tenant_id,
"authorizationUrl": os.getenv(f"{env_prefix}_AUTH_URL", ""),
"tokenUrl": os.getenv(f"{env_prefix}_TOKEN_URL", ""),
"issuer": os.getenv(f"{env_prefix}_ISSUER", ""),
"clientId": os.getenv(f"{env_prefix}_CLIENT_ID", ""),
"metadataUrl": os.getenv(f"{env_prefix}_METADATA_URL", ""),
"enabled": bool(os.getenv(f"{env_prefix}_AUTH_URL") or os.getenv(f"{env_prefix}_METADATA_URL")),
}
@router.post("/api/auth/login", tags=["Auth"])
async def login(body: LoginRequest, request: Request):
"""
@@ -44,6 +77,138 @@ async def login(body: LoginRequest, request: Request):
)
@router.post("/api/auth/password-recovery", tags=["Auth"])
async def request_password_recovery(body: PasswordRecoveryRequest, request: Request):
await ensure_user_directory_schema(request.app)
pool = getattr(request.app.state, "db_pool", None)
if pool is None:
raise HTTPException(status_code=503, detail="Database unavailable.")
normalized_email = body.email.strip().lower()
if not normalized_email or "@" not in normalized_email:
raise HTTPException(status_code=422, detail="email is required.")
raw_token = secrets.token_urlsafe(32)
token_hash = hashlib.sha256(raw_token.encode("utf-8")).hexdigest()
expires_at = datetime.now(timezone.utc) + timedelta(minutes=int(os.getenv("VELOCITY_PASSWORD_RECOVERY_MINUTES", "30")))
async with pool.acquire() as conn:
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS auth_password_recovery_requests (
request_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL,
token_hash TEXT,
expires_at TIMESTAMPTZ,
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
status TEXT NOT NULL DEFAULT 'requested'
)
"""
)
await conn.execute(
"""
INSERT INTO auth_password_recovery_requests (email, token_hash, expires_at)
VALUES ($1, $2, $3)
""",
normalized_email,
token_hash,
expires_at,
)
response = {
"status": "ok",
"message": "Password recovery request recorded.",
"expiresAt": expires_at.isoformat(),
}
if os.getenv("VELOCITY_AUTH_RETURN_RECOVERY_TOKEN", "").lower() in {"1", "true", "yes"}:
response["recoveryToken"] = raw_token
return response
@router.get("/api/auth/sso/providers", tags=["Auth"])
async def list_sso_providers(user: UserPrincipal = Depends(get_current_user)):
raw = os.getenv("VELOCITY_SSO_PROVIDERS", "")
providers = [
_sso_provider_descriptor(provider, user.tenant_id)
for provider in raw.split(",")
if provider.strip()
]
return {
"status": "ok",
"data": {
"enabled": bool(providers),
"providers": providers,
"tenantId": user.tenant_id,
},
}
@router.get("/api/auth/sso/{provider_id}/start", tags=["Auth"])
async def start_sso(provider_id: str, user: UserPrincipal = Depends(get_current_user)):
provider = _sso_provider_descriptor(provider_id, user.tenant_id)
if not provider["enabled"]:
raise HTTPException(status_code=404, detail="SSO provider is not configured.")
state = secrets.token_urlsafe(24)
auth_url = str(provider["authorizationUrl"])
separator = "&" if "?" in auth_url else "?"
redirect_url = (
f"{auth_url}{separator}"
f"client_id={provider['clientId']}&"
f"response_type=code&"
f"scope=openid%20email%20profile&"
f"state={state}"
)
return {"status": "ok", "data": {"provider": provider, "redirectUrl": redirect_url, "state": state}}
@router.get("/api/auth/mdm/config", tags=["Auth"])
async def get_mdm_config(user: UserPrincipal = Depends(get_current_user)):
required = os.getenv("VELOCITY_MDM_REQUIRED", "").lower() in {"1", "true", "yes"}
payload = {
"VelocityBackendURL": os.getenv("VELOCITY_PUBLIC_BACKEND_URL", ""),
"VelocityDreamWeaverURL": os.getenv("VELOCITY_DREAM_WEAVER_URL", ""),
"VelocityTenantID": user.tenant_id,
"VelocitySSOProvider": os.getenv("VELOCITY_DEFAULT_SSO_PROVIDER", ""),
}
return {
"status": "ok",
"data": {
"tenantId": user.tenant_id,
"managedConfigurationRequired": required,
"configurationKeys": list(payload.keys()),
"payload": payload,
},
}
@router.post("/api/auth/session-switch", tags=["Auth"])
async def request_session_switch(
body: SessionSwitchRequest,
request: Request,
user: UserPrincipal = Depends(get_current_user),
):
if user.role.upper() not in {"ADMIN", "SUPERADMIN"}:
raise HTTPException(status_code=403, detail="Admin access required for session switching.")
users = await list_tenant_users(app=request.app, user=user)
target = next((item for item in users if str(item.get("user_id") or item.get("id")) == body.userId), None)
if not target:
raise HTTPException(status_code=404, detail="Target user not found in tenant.")
if _role_level(str(target.get("role") or "")) > _role_level(user.role):
raise HTTPException(status_code=403, detail="Cannot switch into a higher-privilege account.")
switched_token = create_access_token(
user_id=target["user_id"],
role=target["role"],
tenant_id=target["tenant_id"],
)
return {
"status": "ok",
"data": {
"switchAllowed": True,
"targetUser": target,
"requiresReauthentication": False,
"accessToken": switched_token,
"tokenType": "bearer",
"expiresIn": 28800,
},
}
@router.get("/api/auth/me", tags=["Auth"])
async def me(request: Request, user: UserPrincipal = Depends(get_current_user)):
return await read_authenticated_user_profile(app=request.app, user=user)
@@ -51,7 +216,7 @@ async def me(request: Request, user: UserPrincipal = Depends(get_current_user)):
@router.get("/api/auth/users", tags=["Auth"])
async def list_auth_users(request: Request, user: UserPrincipal = Depends(get_current_user)):
return await list_tenant_users(app=request.app, user=user)
return {"status": "ok", "data": await list_tenant_users(app=request.app, user=user)}
@router.post("/api/auth/profile/avatar", tags=["Auth"])