Add static API-key auth, dockerize service, publish façade docs
- sms/auth.py: verify_api_key dependency (Bearer/X-API-Key, SHA-256 hash compare via secrets.compare_digest, fail-closed 503 if no hash). All routes gated via a protected router; /health stays open for probes. - config.py: new sms_api_key_hash setting (VOIPMS_SMS_API_KEY_HASH). - Dockerfile + .dockerignore + docker-compose.yml: lean python:3.13-slim image; secrets injected via env_file, never baked in; host-localhost-only port mapping (SMS_PORT override); /health healthcheck. - .env.example: committed template (placeholders only, no secrets). - sms/docs/sms-facade.dokuwiki.txt: abstraction API reference (published to the apidoc wiki at voipms:sms-facade). - README: Authentication section, Docker section, 401/503 error rows. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
26
sms/app.py
26
sms/app.py
@@ -12,9 +12,10 @@ import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import FastAPI, Query, Request, status
|
||||
from fastapi import APIRouter, Depends, FastAPI, Query, Request, status
|
||||
from fastapi.responses import JSONResponse, PlainTextResponse
|
||||
|
||||
from .auth import verify_api_key
|
||||
from .client import VoipMsSMSClient
|
||||
from .config import Settings
|
||||
from .exceptions import VoipMsApiError, VoipMsAuthError, VoipMsError, VoipMsRateLimitError
|
||||
@@ -51,6 +52,10 @@ app = FastAPI(
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
# Every route on this router requires a valid static API key. /health is the
|
||||
# only route kept on `app` directly so liveness probes stay unauthenticated.
|
||||
router = APIRouter(dependencies=[Depends(verify_api_key)])
|
||||
|
||||
|
||||
def _client(request: Request) -> VoipMsSMSClient:
|
||||
return request.app.state.client
|
||||
@@ -79,7 +84,7 @@ def _map_error(exc: VoipMsError) -> JSONResponse:
|
||||
# --- outbound (guarded) ----------------------------------------------------
|
||||
|
||||
|
||||
@app.post("/sms/send", response_model=SendResult)
|
||||
@router.post("/sms/send", response_model=SendResult)
|
||||
async def send_sms(body: SendSmsRequest, request: Request) -> SendResult:
|
||||
guard = _guard(request)
|
||||
client = _client(request)
|
||||
@@ -92,7 +97,7 @@ async def send_sms(body: SendSmsRequest, request: Request) -> SendResult:
|
||||
return result
|
||||
|
||||
|
||||
@app.post("/mms/send", response_model=SendResult)
|
||||
@router.post("/mms/send", response_model=SendResult)
|
||||
async def send_mms(body: SendMmsRequest, request: Request) -> SendResult:
|
||||
guard = _guard(request)
|
||||
client = _client(request)
|
||||
@@ -115,7 +120,7 @@ async def send_mms(body: SendMmsRequest, request: Request) -> SendResult:
|
||||
# --- history / retrieval ---------------------------------------------------
|
||||
|
||||
|
||||
@app.get("/sms", response_model=list[SmsRecord])
|
||||
@router.get("/sms", response_model=list[SmsRecord])
|
||||
async def list_sms(
|
||||
request: Request,
|
||||
sms: Annotated[int | None, Query(description="Specific SMS id")] = None,
|
||||
@@ -138,7 +143,7 @@ async def list_sms(
|
||||
return _map_error(exc)
|
||||
|
||||
|
||||
@app.get("/mms", response_model=list[MmsRecord])
|
||||
@router.get("/mms", response_model=list[MmsRecord])
|
||||
async def list_mms(
|
||||
request: Request,
|
||||
id: Annotated[int | None, Query(description="Specific MMS id")] = None,
|
||||
@@ -161,7 +166,7 @@ async def list_mms(
|
||||
return _map_error(exc)
|
||||
|
||||
|
||||
@app.get("/mms/{id}/media", response_model=MediaResult)
|
||||
@router.get("/mms/{id}/media", response_model=MediaResult)
|
||||
async def get_mms_media(
|
||||
request: Request,
|
||||
id: int,
|
||||
@@ -177,7 +182,7 @@ async def get_mms_media(
|
||||
# --- delete ----------------------------------------------------------------
|
||||
|
||||
|
||||
@app.delete("/sms/{id}", response_model=DeleteResult)
|
||||
@router.delete("/sms/{id}", response_model=DeleteResult)
|
||||
async def delete_sms(request: Request, id: int) -> DeleteResult:
|
||||
client = _client(request)
|
||||
try:
|
||||
@@ -186,7 +191,7 @@ async def delete_sms(request: Request, id: int) -> DeleteResult:
|
||||
return _map_error(exc)
|
||||
|
||||
|
||||
@app.delete("/mms/{id}", response_model=DeleteResult)
|
||||
@router.delete("/mms/{id}", response_model=DeleteResult)
|
||||
async def delete_mms(request: Request, id: int) -> DeleteResult:
|
||||
client = _client(request)
|
||||
try:
|
||||
@@ -200,4 +205,7 @@ async def delete_mms(request: Request, id: int) -> DeleteResult:
|
||||
|
||||
@app.get("/health", response_class=PlainTextResponse)
|
||||
async def health() -> str:
|
||||
return "ok"
|
||||
return "ok"
|
||||
|
||||
|
||||
app.include_router(router)
|
||||
Reference in New Issue
Block a user