diff --git a/api/main.py b/api/main.py index b6b3fdc..71562bf 100644 --- a/api/main.py +++ b/api/main.py @@ -1,39 +1,52 @@ +import logging from contextlib import asynccontextmanager -import os from fastapi import FastAPI -from api.routes import templates, forms -from api.db.init_db import init_db -from api.errors.handlers import register_exception_handlers from fastapi.middleware.cors import CORSMiddleware +from sqlmodel import SQLModel + +from api.db.database import engine from api.routes import forms, templates +from api.errors.handlers import register_exception_handlers +from api.middleware.rate_limiter import register_rate_limiter + +logger = logging.getLogger("fireform") + @asynccontextmanager async def lifespan(app: FastAPI): - # Startup: Initialize the database and seed it if necessary - print("Initializing database...") - init_db() + logger.info("Starting FireForm — initializing database tables") + SQLModel.metadata.create_all(engine) + logger.info("Database tables ready") yield - # Shutdown logic goes here if needed + logger.info("Shutting down FireForm") -app = FastAPI(lifespan=lifespan) -register_exception_handlers(app) +app = FastAPI( + title="FireForm API", + description="AI-powered PDF form filling for first responders", + version="0.1.0", + lifespan=lifespan, +) -default_origins = "http://127.0.0.1:5173" -allowed_origins = [ - origin.strip() - for origin in os.getenv("FRONTEND_ORIGINS", default_origins).split(",") - if origin.strip() -] +register_exception_handlers(app) +register_rate_limiter(app) app.add_middleware( CORSMiddleware, - allow_origins=allowed_origins, - allow_credentials=False, + allow_origins=[ + "http://127.0.0.1:5500", + "http://localhost:5500", + ], + allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(templates.router) app.include_router(forms.router) + + +@app.get("/health", tags=["system"]) +def health_check(): + return {"status": "healthy", "service": "fireform"} \ No newline at end of file diff --git a/frontend/app.js b/frontend/app.js index 2f6de80..5a3104d 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -32,6 +32,8 @@ const elements = { let templates = loadTemplates(); let activeObjectUrl = null; let selectedTemplateFile = null; +let isTemplateSubmitting = false; +let isFillSubmitting = false; initialize(); @@ -246,6 +248,14 @@ function normalizeFields(rawFields) { async function handleTemplateSubmit(event) { event.preventDefault(); + + if (isTemplateSubmitting) { + setStatus(elements.templateFormMessage, "Request already in progress...", "info"); + return; + } + + isTemplateSubmitting = true; + clearJson(elements.templateFormResponse); setStatus(elements.templateFormMessage, ""); @@ -259,11 +269,13 @@ async function handleTemplateSubmit(event) { "Name, PDF file, and template directory are required.", "error" ); + isTemplateSubmitting = false; return; } if (normalized.error) { setStatus(elements.templateFormMessage, normalized.error, "error"); + isTemplateSubmitting = false; return; } @@ -280,6 +292,7 @@ async function handleTemplateSubmit(event) { }; setStatus(elements.templateFormMessage, "Creating template...", "info"); + const response = await fetch(`${API_BASE_URL}/templates/create`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -287,12 +300,14 @@ async function handleTemplateSubmit(event) { }); const body = await parseJsonResponse(response); + if (!response.ok) { throw new Error(extractErrorMessage(body, response.status)); } upsertTemplate(body); await refreshTemplatesFromApi(); + elements.fillTemplateId.value = String(body.id || ""); elements.serverPdfPath.value = body.pdf_path || ""; @@ -301,12 +316,16 @@ async function handleTemplateSubmit(event) { `Template created (id: ${body.id}). PDF saved at ${upload.pdf_path}.`, "success" ); + showJson(elements.templateFormResponse, body); } catch (error) { setStatus(elements.templateFormMessage, error.message, "error"); + } finally { + isTemplateSubmitting = false; } } + async function uploadTemplatePdf(file, directory) { const formData = new FormData(); formData.append("file", file, file.name); @@ -327,6 +346,14 @@ async function uploadTemplatePdf(file, directory) { async function handleFillSubmit(event) { event.preventDefault(); + + if (isFillSubmitting) { + setStatus(elements.fillFormMessage, "Request already in progress...", "info"); + return; + } + + isFillSubmitting = true; + clearJson(elements.fillFormResponse); setStatus(elements.fillFormMessage, ""); @@ -335,11 +362,13 @@ async function handleFillSubmit(event) { if (!Number.isInteger(templateId) || templateId < 1) { setStatus(elements.fillFormMessage, "Template ID must be a positive integer.", "error"); + isFillSubmitting = false; return; } if (!inputText) { setStatus(elements.fillFormMessage, "Input text is required.", "error"); + isFillSubmitting = false; return; } @@ -350,6 +379,7 @@ async function handleFillSubmit(event) { try { setStatus(elements.fillFormMessage, "Submitting form fill request...", "info"); + const response = await fetch(`${API_BASE_URL}/forms/fill`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -357,6 +387,7 @@ async function handleFillSubmit(event) { }); const body = await parseJsonResponse(response); + if (!response.ok) { throw new Error(extractErrorMessage(body, response.status)); } @@ -372,9 +403,12 @@ async function handleFillSubmit(event) { `Form filled (submission id: ${body.id}).`, "success" ); + showJson(elements.fillFormResponse, body); } catch (error) { setStatus(elements.fillFormMessage, error.message, "error"); + } finally { + isFillSubmitting = false; } } diff --git a/tests/test_template_file_routes.py b/tests/test_template_file_routes.py new file mode 100644 index 0000000..7bae8c8 --- /dev/null +++ b/tests/test_template_file_routes.py @@ -0,0 +1,71 @@ +from fastapi.testclient import TestClient +from api.main import app + +client = TestClient(app) + + +def test_upload_template_pdf(): + pdf_content = b"%PDF-1.4 test pdf content" + + files = { + "file": ("sample_test.pdf", pdf_content, "application/pdf") + } + + data = { + "directory": "src/inputs" + } + + response = client.post( + "/templates/upload", + files=files, + data=data + ) + + assert response.status_code == 200 + + response_data = response.json() + + assert "filename" in response_data + assert "pdf_path" in response_data + assert response_data["filename"].endswith(".pdf") + + +def test_preview_template_pdf(): + response = client.get( + "/templates/preview", + params={"path": "src/inputs/file.pdf"} + ) + + assert response.status_code == 200 + assert response.headers["content-type"] == "application/pdf" + +def test_upload_non_pdf_should_fail(): + files = { + "file": ("sample.txt", b"not a pdf", "text/plain") + } + + response = client.post( + "/templates/upload", + files=files, + data={"directory": "src/inputs"} + ) + + assert response.status_code == 400 + + +def test_preview_missing_file_should_fail(): + response = client.get( + "/templates/preview", + params={"path": "src/inputs/does_not_exist.pdf"} + ) + + assert response.status_code == 404 + + +def test_preview_outside_project_should_fail(): + response = client.get( + "/templates/preview", + params={"path": "/etc/passwd"} + ) + + assert response.status_code == 400