Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 31 additions & 18 deletions api/main.py
Original file line number Diff line number Diff line change
@@ -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"}
34 changes: 34 additions & 0 deletions frontend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const elements = {
let templates = loadTemplates();
let activeObjectUrl = null;
let selectedTemplateFile = null;
let isTemplateSubmitting = false;
let isFillSubmitting = false;

initialize();

Expand Down Expand Up @@ -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, "");

Expand All @@ -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;
}

Expand All @@ -280,19 +292,22 @@ 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" },
body: JSON.stringify(payload),
});

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 || "";

Expand All @@ -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);
Expand All @@ -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, "");

Expand All @@ -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;
}

Expand All @@ -350,13 +379,15 @@ 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" },
body: JSON.stringify(payload),
});

const body = await parseJsonResponse(response);

if (!response.ok) {
throw new Error(extractErrorMessage(body, response.status));
}
Expand All @@ -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;
}
}

Expand Down
71 changes: 71 additions & 0 deletions tests/test_template_file_routes.py
Original file line number Diff line number Diff line change
@@ -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