feat: add catalogue check by creating a small service runner

This commit is contained in:
2025-11-20 13:04:42 +01:00
parent 9ab4fcbe81
commit 74c8eacbf2
4 changed files with 364 additions and 3 deletions

97
API_SERVICE.md Normal file
View File

@@ -0,0 +1,97 @@
# Signature Validation API Service
This is a lightweight Python service that provides signature validation for the PHP application.
## Why a separate service?
The `bibapi` library is Python-only and provides access to your library catalog. Rather than rewriting this in PHP, we keep a small Python service running just for signature validation.
## Running the Service
### Option 1: Direct Python
```bash
python api_service.py
```
### Option 2: With uvicorn
```bash
uvicorn api_service:app --host 0.0.0.0 --port 8001
```
### Option 3: Docker (if you can run containers internally)
```dockerfile
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY api_service.py .
EXPOSE 8001
CMD ["uvicorn", "api_service:app", "--host", "0.0.0.0", "--port", "8001"]
```
```bash
docker build -t signature-api .
docker run -d -p 8001:8001 signature-api
```
## Configuration
Set the API endpoint in your PHP config. Update `php/config.php`:
```php
// Signature validation API endpoint (optional)
define('SIGNATURE_API_URL', getenv('SIGNATURE_API_URL') ?: 'http://localhost:8001');
```
## Testing
```bash
# Health check
curl http://localhost:8001/health
# Validate a signature
curl "http://localhost:8001/api/validate-signature?signature=ABC123"
```
## Production Deployment
1. **Same server**: Run on a different port (8001) alongside your PHP application
2. **Separate server**: Run on internal network, update `SIGNATURE_API_URL` in PHP config
3. **Systemd service** (Linux):
Create `/etc/systemd/system/signature-api.service`:
```ini
[Unit]
Description=Signature Validation API
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/signature-api
Environment="PATH=/var/www/signature-api/.venv/bin"
ExecStart=/var/www/signature-api/.venv/bin/uvicorn api_service:app --host 0.0.0.0 --port 8001
Restart=always
[Install]
WantedBy=multi-user.target
```
Then:
```bash
sudo systemctl enable signature-api
sudo systemctl start signature-api
```
## Security
- In production, update CORS `allow_origins` to only your PHP server domain
- Consider adding API key authentication if exposed to public network
- Run behind reverse proxy (nginx/Apache) with SSL
## Notes
- The service is stateless and lightweight
- No data persistence required
- Can be scaled horizontally if needed
- Falls back gracefully if unavailable (ELSA form fields just won't have validation hints)

67
api_service.py Normal file
View File

@@ -0,0 +1,67 @@
"""
Lightweight Python API service for signature validation
This can run independently to support the PHP application
"""
from fastapi import FastAPI, Query
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from bibapi import catalogue
import re
import os
app = FastAPI(title="Signature Validation API")
# Allow PHP application to call this API
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # In production, restrict to your PHP server domain
allow_credentials=True,
allow_methods=["GET"],
allow_headers=["*"],
)
# Initialize catalogue for signature validation
cat = catalogue.Catalogue()
@app.get("/api/validate-signature")
async def validate_signature(signature: str = Query(...)):
"""Validate a book signature and return total pages"""
try:
book_result = cat.get_book_with_data(signature)
if book_result and hasattr(book_result, "pages") and book_result.pages:
# Try to extract numeric page count
pages_str = str(book_result.pages)
# Extract first number from pages string (e.g., "245 S." -> 245)
match = re.search(r"(\d+)", pages_str)
if match:
total_pages = int(match.group(1))
return JSONResponse(
{"valid": True, "total_pages": total_pages, "signature": signature}
)
return JSONResponse(
{
"valid": False,
"error": "Signatur nicht gefunden oder keine Seitenzahl verfügbar",
"signature": signature,
}
)
except Exception as e:
return JSONResponse(
{
"valid": False,
"error": f"Fehler bei der Validierung: {str(e)}",
"signature": signature,
},
status_code=500
)
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {"status": "ok", "service": "signature-validation"}
if __name__ == "__main__":
import uvicorn
port = int(os.getenv("API_PORT", "8001"))
uvicorn.run(app, host="0.0.0.0", port=port)

View File

@@ -24,5 +24,8 @@ define('MAIL_TO', getenv('MAIL_TO') ?: 'semesterapparate@ph-freiburg.de');
define('BASE_PATH', __DIR__); define('BASE_PATH', __DIR__);
define('STATIC_PATH', '/static'); define('STATIC_PATH', '/static');
// Signature validation API (optional Python service)
define('SIGNATURE_API_URL', getenv('SIGNATURE_API_URL') ?: 'http://localhost:8001');
// Email regex pattern // Email regex pattern
define('EMAIL_REGEX', '/^[^@\s]+@[^@\s]+\.[^@\s]+$/'); define('EMAIL_REGEX', '/^[^@\s]+@[^@\s]+\.[^@\s]+$/');

View File

@@ -34,7 +34,7 @@
</p> </p>
</div> </div>
<form method="post" action="submit_elsa.php" class="request-form"> <form method="post" action="submit_elsa.php" class="request-form" data-api-url="<?php echo SIGNATURE_API_URL; ?>">
<h2>Allgemeine Informationen</h2> <h2>Allgemeine Informationen</h2>
<div class="grid-form"> <div class="grid-form">
<div class="form-field"> <div class="form-field">
@@ -137,6 +137,16 @@
}; };
let sectionCounter = 0; let sectionCounter = 0;
// Track signature validations
let signatureTracking = {};
let validationTimers = {};
// Get API URL from form data attribute
const getApiUrl = () => {
const form = document.querySelector('.request-form');
return form ? form.dataset.apiUrl : 'http://localhost:8001';
};
// Theme toggle functionality // Theme toggle functionality
(function() { (function() {
@@ -271,6 +281,7 @@
'<td><input type="number" name="monografie_pages_from[]" data-section="' + sectionId + '" required min="1"></td>' + '<td><input type="number" name="monografie_pages_from[]" data-section="' + sectionId + '" required min="1"></td>' +
'<td><input type="number" name="monografie_pages_to[]" data-section="' + sectionId + '" required min="1"></td>' + '<td><input type="number" name="monografie_pages_to[]" data-section="' + sectionId + '" required min="1"></td>' +
'<td><button type="button" class="btn-icon" onclick="removeRow(\'' + rowId + '\')" title="Zeile entfernen"><span class="mdi mdi-delete"></span></button></td>'; '<td><button type="button" class="btn-icon" onclick="removeRow(\'' + rowId + '\')" title="Zeile entfernen"><span class="mdi mdi-delete"></span></button></td>';
attachSignatureListeners(row);
} else if (type === 'zeitschriftenartikel') { } else if (type === 'zeitschriftenartikel') {
row.innerHTML = '<td><input type="text" name="zeitschrift_author[]" data-section="' + sectionId + '" required></td>' + row.innerHTML = '<td><input type="text" name="zeitschrift_author[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="zeitschrift_year[]" data-section="' + sectionId + '" required></td>' + '<td><input type="text" name="zeitschrift_year[]" data-section="' + sectionId + '" required></td>' +
@@ -291,6 +302,7 @@
'<td><input type="text" name="herausgeber_signature[]" data-section="' + sectionId + '" placeholder="Optional"></td>' + '<td><input type="text" name="herausgeber_signature[]" data-section="' + sectionId + '" placeholder="Optional"></td>' +
'<td><input type="number" name="herausgeber_pages_from[]" data-section="' + sectionId + '" required min="1"></td>' + '<td><input type="number" name="herausgeber_pages_from[]" data-section="' + sectionId + '" required min="1"></td>' +
'<td><input type="number" name="herausgeber_pages_to[]" data-section="' + sectionId + '" required min="1"></td>' + '<td><input type="number" name="herausgeber_pages_to[]" data-section="' + sectionId + '" required min="1"></td>' +
attachSignatureListeners(row);
'<td><button type="button" class="btn-icon" onclick="removeRow(\'' + rowId + '\')" title="Zeile entfernen"><span class="mdi mdi-delete"></span></button></td>'; '<td><button type="button" class="btn-icon" onclick="removeRow(\'' + rowId + '\')" title="Zeile entfernen"><span class="mdi mdi-delete"></span></button></td>';
} }
} }
@@ -298,21 +310,203 @@
function removeRow(rowId) { function removeRow(rowId) {
const row = document.getElementById(rowId); const row = document.getElementById(rowId);
if (row) { if (row) {
const signatureInput = row.querySelector('input[name*="_signature"]');
if (signatureInput) {
cleanupSignatureTracking(signatureInput);
}
row.remove(); row.remove();
updateSubmitButton();
}
}
function validateSignature(signatureInput, pagesFromInput, pagesToInput) {
const signature = signatureInput.value.trim();
if (!signature) {
signatureInput.classList.remove('signature-validating', 'signature-valid', 'signature-invalid');
return;
}
const inputId = signatureInput.id || signatureInput.name;
if (validationTimers[inputId]) {
clearTimeout(validationTimers[inputId]);
}
signatureInput.classList.add('signature-validating');
signatureInput.classList.remove('signature-valid', 'signature-invalid');
validationTimers[inputId] = setTimeout(async () => {
try {
const apiUrl = getApiUrl();
const response = await fetch(apiUrl + '/api/validate-signature?signature=' + encodeURIComponent(signature));
const data = await response.json();
if (data.valid) {
if (!signatureTracking[signature]) {
signatureTracking[signature] = {
totalPages: data.total_pages,
inputs: []
};
} else {
signatureTracking[signature].totalPages = data.total_pages;
}
const existingIndex = signatureTracking[signature].inputs.findIndex(
item => item.signature === signatureInput
);
if (existingIndex === -1) {
signatureTracking[signature].inputs.push({
signature: signatureInput,
pagesFrom: pagesFromInput,
pagesTo: pagesToInput
});
}
signatureInput.classList.remove('signature-validating');
signatureInput.classList.add('signature-valid');
signatureInput.title = 'Signatur gefunden: ' + data.total_pages + ' Seiten';
checkSignatureThreshold(signature);
} else {
signatureInput.classList.remove('signature-validating');
signatureInput.classList.add('signature-invalid');
signatureInput.title = data.error || 'Signatur nicht gefunden';
updateSubmitButton();
}
} catch (error) {
signatureInput.classList.remove('signature-validating');
signatureInput.classList.add('signature-invalid');
signatureInput.title = 'Validierungsfehler - API nicht erreichbar';
updateSubmitButton();
}
}, 800);
}
function checkSignatureThreshold(signature) {
const tracking = signatureTracking[signature];
if (!tracking) return;
let totalRequestedPages = 0;
const threshold = Math.ceil(tracking.totalPages * 0.15);
tracking.inputs.forEach(item => {
const from = parseInt(item.pagesFrom.value) || 0;
const to = parseInt(item.pagesTo.value) || 0;
if (from > 0 && to > 0 && to >= from) {
totalRequestedPages += (to - from + 1);
}
});
const isOverThreshold = totalRequestedPages > threshold;
tracking.inputs.forEach(item => {
const row = item.signature.closest('tr');
if (row) {
if (isOverThreshold) {
row.classList.add('threshold-exceeded');
item.signature.title = 'Warnung: Gesamtanzahl der Seiten (' + totalRequestedPages + ') überschreitet 15% (' + threshold + ' Seiten)';
} else {
row.classList.remove('threshold-exceeded');
item.signature.title = 'Signatur gefunden: ' + tracking.totalPages + ' Seiten (Aktuell: ' + totalRequestedPages + '/' + threshold + ')';
}
}
});
updateSubmitButton();
}
function cleanupSignatureTracking(signatureInput) {
const signature = signatureInput.value.trim();
if (signature && signatureTracking[signature]) {
signatureTracking[signature].inputs = signatureTracking[signature].inputs.filter(
item => item.signature !== signatureInput
);
if (signatureTracking[signature].inputs.length === 0) {
delete signatureTracking[signature];
} else {
checkSignatureThreshold(signature);
}
}
}
function updateSubmitButton() {
const submitButton = document.querySelector('button[type="submit"]');
const hasInvalidSignatures = document.querySelectorAll('.signature-invalid').length > 0;
const hasThresholdExceeded = document.querySelectorAll('.threshold-exceeded').length > 0;
if (hasInvalidSignatures || hasThresholdExceeded) {
submitButton.disabled = true;
if (hasThresholdExceeded) {
submitButton.title = 'Formular kann nicht abgesendet werden: 15%-Grenze überschritten';
} else {
submitButton.title = 'Formular kann nicht abgesendet werden: Ungültige Signaturen';
}
} else {
submitButton.disabled = false;
submitButton.title = '';
}
}
function attachSignatureListeners(row) {
const signatureInput = row.querySelector('input[name*="_signature"]');
const pagesFromInput = row.querySelector('input[name*="_pages_from"]');
const pagesToInput = row.querySelector('input[name*="_pages_to"]');
if (signatureInput && pagesFromInput && pagesToInput) {
if (!signatureInput.id) {
signatureInput.id = 'sig-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
}
signatureInput.addEventListener('input', () => {
const oldSignature = signatureInput.dataset.lastSignature || '';
const newSignature = signatureInput.value.trim();
if (oldSignature && oldSignature !== newSignature) {
const tempInput = document.createElement('input');
tempInput.value = oldSignature;
cleanupSignatureTracking(tempInput);
}
signatureInput.dataset.lastSignature = newSignature;
validateSignature(signatureInput, pagesFromInput, pagesToInput);
});
pagesFromInput.addEventListener('input', () => {
const signature = signatureInput.value.trim();
if (signature && signatureTracking[signature]) {
checkSignatureThreshold(signature);
}
});
pagesToInput.addEventListener('input', () => {
const signature = signatureInput.value.trim();
if (signature && signatureTracking[signature]) {
checkSignatureThreshold(signature);
}
});
} }
} }
function removeMediaSection(sectionId) { function removeMediaSection(sectionId) {
const section = document.getElementById(sectionId); const section = document.getElementById(sectionId);
if (section) { if (section) {
if (confirm('Möchten Sie diese Sektion wirklich entfernen?')) { if (const rows = section.querySelectorAll('tr[id]');
const type = section.getAttribute('data-type'); rows.forEach(row => {
const signatureInput = row.querySelector('input[name*="_signature"]');
if (signatureInput) {
cleanupSignatureTracking(signatureInput);
}
});
section.remove(); section.remove();
const btn = document.getElementById('btn-' + type); const btn = document.getElementById('btn-' + type);
if (btn) { if (btn) {
btn.disabled = false; btn.disabled = false;
btn.title = 'Sektion hinzufügen'; btn.title = 'Sektion hinzufügen';
} }
updateSubmitButton(); btn.disabled = false;
btn.title = 'Sektion hinzufügen';
}
} }
} }
} }