feat: Initialize FastAPI application with Azure authentication and transaction management

- Added .env.example for environment variable configuration.
- Created app initialization files and core settings management.
- Implemented API routers for reporting and transaction endpoints.
- Developed transaction management service with CRUD operations.
- Integrated Azure OAuth for user authentication.
- Designed dashboard view with transaction filtering and display.
- Added Swagger UI documentation with custom dark theme.
- Created static and template files for frontend styling and layout.
This commit is contained in:
2026-05-10 22:17:30 +12:00
parent e86513d5ea
commit d50c1c5bba
26 changed files with 800 additions and 182 deletions
+5
View File
@@ -0,0 +1,5 @@
from app.views.auth import router as auth_router
from app.views.dashboard import router as dashboard_router
from app.views.docs import router as docs_router
__all__ = ["auth_router", "dashboard_router", "docs_router"]
+46
View File
@@ -0,0 +1,46 @@
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import RedirectResponse
from app.core.auth import get_oauth
from app.core.settings import get_settings
router = APIRouter()
@router.get("/login", include_in_schema=False)
async def login(request: Request):
settings = get_settings()
if not settings.azure_configured:
raise HTTPException(
status_code=500,
detail=(
"Azure Entra ID auth is not configured. Set AZURE_TENANT_ID, "
"AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET."
),
)
oauth = get_oauth()
redirect_uri = request.url_for("auth_callback")
return await oauth.azure.authorize_redirect(request, redirect_uri)
@router.get("/auth/callback", include_in_schema=False, name="auth_callback")
async def auth_callback(request: Request):
oauth = get_oauth()
token = await oauth.azure.authorize_access_token(request)
userinfo = token.get("userinfo")
if not userinfo:
userinfo = await oauth.azure.parse_id_token(request, token)
request.session["user"] = {
"sub": userinfo.get("sub"),
"name": userinfo.get("name") or userinfo.get("preferred_username"),
"email": userinfo.get("email") or userinfo.get("preferred_username"),
}
return RedirectResponse(url="/", status_code=302)
@router.get("/logout", include_in_schema=False)
async def logout(request: Request):
request.session.clear()
return RedirectResponse(url="/", status_code=302)
+55
View File
@@ -0,0 +1,55 @@
from pathlib import Path
from fastapi import APIRouter, Query, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from app.service.recon_service import list_transactions
router = APIRouter()
project_root = Path(__file__).resolve().parents[2]
templates = Jinja2Templates(directory=str(project_root / "data" / "templates"))
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
async def dashboard(
request: Request,
recon_job_name: str | None = Query(default=None),
as_at_date: str | None = Query(default=None),
) -> HTMLResponse:
user = request.session.get("user")
if not user:
return RedirectResponse(url="/login", status_code=302)
transactions = list_transactions()
if as_at_date:
transactions = [item for item in transactions if item.date.isoformat() == as_at_date]
results = [
{
"Transaction ID": item.transaction_id,
"Date": item.date.isoformat(),
"Account": item.account,
"Amount": f"{item.amount:,.2f}",
"Status": item.status,
"Flag": item.flag,
}
for item in transactions
]
return templates.TemplateResponse(
request=request,
name="dashboard.html",
context={
"request": request,
"title": "Recon Ranger Dashboard",
"results": results,
"user": user,
"recon_job_name": recon_job_name,
"as_at_date": as_at_date,
"prev_cursor": None,
"next_cursor": None,
},
)
+45
View File
@@ -0,0 +1,45 @@
from fastapi import APIRouter, Request
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.responses import HTMLResponse
router = APIRouter()
@router.get("/api/docs", include_in_schema=False)
async def swagger_ui_dark(request: Request) -> HTMLResponse:
base = get_swagger_ui_html(
openapi_url=request.app.openapi_url,
title="Recon Ranger — API Docs",
swagger_css_url="/static/api/swagger-ui.css",
)
html = base.body.decode()
html = html.replace(
"</head>",
(
'<link rel="stylesheet" href="/static/css/styles.css">\n'
'<link rel="stylesheet" href="/static/api/swagger-dark.css">\n'
"<style>.swagger-ui .topbar { display: none; }</style>\n"
"</head>"
),
)
nav_html = """
<header>
<nav>
<div class=\"menu\">
<div class=\"logo\"><a href=\"/\">Recon Ranger</a></div>
<ul>
<li><a href=\"/\">Home</a></li>
<li><a href=\"/api/docs\">API</a></li>
<li><a href=\"mailto:someone@example.com\">Contact</a></li>
</ul>
</div>
</nav>
</header>
<div style=\"padding-top:70px; position:relative; z-index:1\">
"""
html = html.replace("<body>", "<body>" + nav_html)
html = html.replace("</body>", "</div></body>")
return HTMLResponse(html)