- matrix_provisioning.py: Create Matrix users via Synapse admin API - Auto-create Matrix account when user registers in DAARION - Return matrix_user_id in registration response
319 lines
8.9 KiB
Python
319 lines
8.9 KiB
Python
"""
|
|
Auth Service - Main Application
|
|
DAARION.city Authentication & Authorization
|
|
"""
|
|
from fastapi import FastAPI, HTTPException, Depends, Request, Header
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from contextlib import asynccontextmanager
|
|
from typing import Optional
|
|
from uuid import UUID
|
|
import logging
|
|
|
|
from config import get_settings
|
|
from models import (
|
|
RegisterRequest, RegisterResponse,
|
|
LoginRequest, TokenResponse,
|
|
RefreshRequest, RefreshResponse,
|
|
LogoutRequest, StatusResponse,
|
|
IntrospectRequest, IntrospectResponse,
|
|
UserResponse, HealthResponse, ErrorResponse
|
|
)
|
|
from database import (
|
|
get_pool, close_pool,
|
|
create_user, get_user_by_email, get_user_by_id, get_user_roles,
|
|
create_session, get_session, revoke_session, is_session_valid
|
|
)
|
|
from security import (
|
|
hash_password, verify_password,
|
|
create_access_token, create_refresh_token,
|
|
decode_access_token, decode_refresh_token
|
|
)
|
|
from matrix_provisioning import provision_matrix_user
|
|
|
|
# Setup logging
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
settings = get_settings()
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
# Startup
|
|
logger.info(f"Starting {settings.service_name} v{settings.service_version}")
|
|
await get_pool()
|
|
yield
|
|
# Shutdown
|
|
await close_pool()
|
|
logger.info("Auth service stopped")
|
|
|
|
|
|
app = FastAPI(
|
|
title="DAARION Auth Service",
|
|
description="Authentication & Authorization for DAARION.city",
|
|
version=settings.service_version,
|
|
lifespan=lifespan
|
|
)
|
|
|
|
# CORS
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"], # In production, specify exact origins
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
# Dependency: Get current user from token
|
|
async def get_current_user(
|
|
authorization: Optional[str] = Header(None)
|
|
) -> Optional[dict]:
|
|
if not authorization or not authorization.startswith("Bearer "):
|
|
return None
|
|
|
|
token = authorization[7:] # Remove "Bearer " prefix
|
|
payload = decode_access_token(token)
|
|
|
|
if not payload:
|
|
return None
|
|
|
|
return payload
|
|
|
|
|
|
async def require_auth(
|
|
authorization: Optional[str] = Header(None)
|
|
) -> dict:
|
|
user = await get_current_user(authorization)
|
|
if not user:
|
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
return user
|
|
|
|
|
|
# Health check
|
|
@app.get("/healthz", response_model=HealthResponse)
|
|
async def health_check():
|
|
return HealthResponse(
|
|
status="ok",
|
|
service=settings.service_name,
|
|
version=settings.service_version
|
|
)
|
|
|
|
|
|
# Register
|
|
@app.post("/api/auth/register", response_model=RegisterResponse, status_code=201)
|
|
async def register(request: RegisterRequest):
|
|
# Check if user exists
|
|
existing = await get_user_by_email(request.email)
|
|
if existing:
|
|
raise HTTPException(status_code=400, detail="Email already registered")
|
|
|
|
# Hash password and create user
|
|
password_hash = hash_password(request.password)
|
|
user = await create_user(
|
|
email=request.email,
|
|
password_hash=password_hash,
|
|
display_name=request.display_name
|
|
)
|
|
|
|
logger.info(f"User registered: {request.email}")
|
|
|
|
# Provision Matrix user (async, don't block registration)
|
|
matrix_info = None
|
|
try:
|
|
matrix_info = await provision_matrix_user(
|
|
user_id=str(user['id']),
|
|
email=user['email'],
|
|
display_name=user['display_name']
|
|
)
|
|
logger.info(f"Matrix user provisioned: {matrix_info.get('matrix_user_id')}")
|
|
except Exception as e:
|
|
logger.warning(f"Matrix provisioning failed (non-blocking): {e}")
|
|
|
|
return RegisterResponse(
|
|
user_id=user['id'],
|
|
email=user['email'],
|
|
display_name=user['display_name'],
|
|
roles=["user"],
|
|
matrix_user_id=matrix_info.get('matrix_user_id') if matrix_info else None
|
|
)
|
|
|
|
|
|
# Login
|
|
@app.post("/api/auth/login", response_model=TokenResponse)
|
|
async def login(
|
|
request: LoginRequest,
|
|
req: Request
|
|
):
|
|
# Get user
|
|
user = await get_user_by_email(request.email)
|
|
if not user:
|
|
raise HTTPException(status_code=401, detail="Invalid email or password")
|
|
|
|
# Verify password
|
|
if not verify_password(request.password, user['password_hash']):
|
|
raise HTTPException(status_code=401, detail="Invalid email or password")
|
|
|
|
# Check if active
|
|
if not user['is_active']:
|
|
raise HTTPException(status_code=403, detail="Account is disabled")
|
|
|
|
# Get roles
|
|
roles = await get_user_roles(user['id'])
|
|
if user['is_admin'] and 'admin' not in roles:
|
|
roles.append('admin')
|
|
|
|
# Create session
|
|
user_agent = req.headers.get("user-agent")
|
|
ip_address = req.client.host if req.client else None
|
|
session_id = await create_session(
|
|
user_id=user['id'],
|
|
user_agent=user_agent,
|
|
ip_address=ip_address,
|
|
ttl_seconds=settings.refresh_token_ttl
|
|
)
|
|
|
|
# Create tokens
|
|
access_token = create_access_token(
|
|
user_id=user['id'],
|
|
email=user['email'],
|
|
display_name=user['display_name'],
|
|
roles=roles
|
|
)
|
|
refresh_token = create_refresh_token(
|
|
user_id=user['id'],
|
|
session_id=session_id
|
|
)
|
|
|
|
logger.info(f"User logged in: {request.email}")
|
|
|
|
return TokenResponse(
|
|
access_token=access_token,
|
|
refresh_token=refresh_token,
|
|
token_type="Bearer",
|
|
expires_in=settings.access_token_ttl,
|
|
user=UserResponse(
|
|
id=user['id'],
|
|
email=user['email'],
|
|
display_name=user['display_name'],
|
|
avatar_url=user['avatar_url'],
|
|
roles=roles,
|
|
is_active=user['is_active'],
|
|
created_at=user['created_at']
|
|
)
|
|
)
|
|
|
|
|
|
# Refresh token
|
|
@app.post("/api/auth/refresh", response_model=RefreshResponse)
|
|
async def refresh(request: RefreshRequest):
|
|
# Decode refresh token
|
|
payload = decode_refresh_token(request.refresh_token)
|
|
if not payload:
|
|
raise HTTPException(status_code=401, detail="Invalid refresh token")
|
|
|
|
# Check session
|
|
session_id = UUID(payload['session_id'])
|
|
if not await is_session_valid(session_id):
|
|
raise HTTPException(status_code=401, detail="Session expired or revoked")
|
|
|
|
# Get user
|
|
user_id = UUID(payload['sub'])
|
|
user = await get_user_by_id(user_id)
|
|
if not user or not user['is_active']:
|
|
raise HTTPException(status_code=401, detail="User not found or disabled")
|
|
|
|
# Get roles
|
|
roles = await get_user_roles(user_id)
|
|
if user['is_admin'] and 'admin' not in roles:
|
|
roles.append('admin')
|
|
|
|
# Revoke old session and create new one
|
|
await revoke_session(session_id)
|
|
new_session_id = await create_session(
|
|
user_id=user_id,
|
|
ttl_seconds=settings.refresh_token_ttl
|
|
)
|
|
|
|
# Create new tokens
|
|
access_token = create_access_token(
|
|
user_id=user_id,
|
|
email=user['email'],
|
|
display_name=user['display_name'],
|
|
roles=roles
|
|
)
|
|
new_refresh_token = create_refresh_token(
|
|
user_id=user_id,
|
|
session_id=new_session_id
|
|
)
|
|
|
|
return RefreshResponse(
|
|
access_token=access_token,
|
|
refresh_token=new_refresh_token,
|
|
token_type="Bearer",
|
|
expires_in=settings.access_token_ttl
|
|
)
|
|
|
|
|
|
# Logout
|
|
@app.post("/api/auth/logout", response_model=StatusResponse)
|
|
async def logout(request: LogoutRequest):
|
|
# Decode refresh token
|
|
payload = decode_refresh_token(request.refresh_token)
|
|
if payload:
|
|
session_id = UUID(payload['session_id'])
|
|
await revoke_session(session_id)
|
|
|
|
return StatusResponse(status="ok")
|
|
|
|
|
|
# Get current user
|
|
@app.get("/api/auth/me", response_model=UserResponse)
|
|
async def get_me(current_user: dict = Depends(require_auth)):
|
|
user_id = UUID(current_user['sub'])
|
|
user = await get_user_by_id(user_id)
|
|
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
roles = await get_user_roles(user_id)
|
|
if user['is_admin'] and 'admin' not in roles:
|
|
roles.append('admin')
|
|
|
|
return UserResponse(
|
|
id=user['id'],
|
|
email=user['email'],
|
|
display_name=user['display_name'],
|
|
avatar_url=user['avatar_url'],
|
|
roles=roles,
|
|
is_active=user['is_active'],
|
|
created_at=user['created_at']
|
|
)
|
|
|
|
|
|
# Introspect token (for other services)
|
|
@app.post("/api/auth/introspect", response_model=IntrospectResponse)
|
|
async def introspect(request: IntrospectRequest):
|
|
payload = decode_access_token(request.token)
|
|
|
|
if not payload:
|
|
return IntrospectResponse(active=False)
|
|
|
|
return IntrospectResponse(
|
|
active=True,
|
|
sub=payload.get('sub'),
|
|
email=payload.get('email'),
|
|
roles=payload.get('roles', []),
|
|
exp=payload.get('exp')
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(
|
|
"main:app",
|
|
host="0.0.0.0",
|
|
port=settings.port,
|
|
reload=settings.debug
|
|
)
|