Files
boc/quixzoom-capture-pipeline/api/server.js
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

178 lines
4.9 KiB
JavaScript

/**
* QUIXZOOM Capture Pipeline — API Server
*
* Huvudserver som exponerar:
* - Upload API
* - Status API
* - Dataset API
* - Admin API
*
* Teknik: Node.js, Express, Redis
*/
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const Redis = require('ioredis');
const uploadRouter = require('./upload');
const app = express();
const PORT = process.env.PORT || 3000;
// Redis-anslutning
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD,
});
// Middleware
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minuter
max: 100, // Max 100 requests per window
message: { error: 'För många förfrågningar, försök igen senare' },
});
app.use('/api/', limiter);
// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Health check
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
version: '1.0.0',
});
});
// API-routes
app.use('/api', uploadRouter);
// Dataset API — hämta träningsdata
app.get('/api/dataset/:version', async (req, res) => {
try {
const { version } = req.params;
const { format = 'json', limit = 1000, offset = 0 } = req.query;
// Hämta dataset från Redis eller R2
const datasetKey = `quixzoom:dataset:${version}`;
const datasetInfo = await redis.hgetall(datasetKey);
if (!datasetInfo || Object.keys(datasetInfo).length === 0) {
return res.status(404).json({ error: 'Dataset not found' });
}
res.json({
version,
format,
limit: parseInt(limit),
offset: parseInt(offset),
total: parseInt(datasetInfo.total || 0),
createdAt: datasetInfo.createdAt,
url: `https://${process.env.R2_BUCKET_NAME}.r2.cloudflarestorage.com/datasets/${version}.jsonl`,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Dataset API — lista alla versioner
app.get('/api/datasets', async (req, res) => {
try {
const keys = await redis.keys('quizoom:dataset:*');
const datasets = [];
for (const key of keys) {
const info = await redis.hgetall(key);
datasets.push({
version: key.replace('quixzoom:dataset:', ''),
...info,
});
}
res.json({ datasets });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Admin API — statistik
app.get('/api/stats', async (req, res) => {
try {
const stats = {
uploads: {
total: parseInt(await redis.get('quixzoom:stats:uploads:total') || 0),
today: parseInt(await redis.get(`quixzoom:stats:uploads:${new Date().toISOString().split('T')[0]}`) || 0),
},
processing: {
pending: await redis.llen('quixzoom:processing:queue'),
completed: parseInt(await redis.get('quixzoom:stats:processing:completed') || 0),
failed: parseInt(await redis.get('quixzoom:stats:processing:failed') || 0),
},
taxonomy: {
tagged: parseInt(await redis.get('quixzoom:stats:taxonomy:tagged') || 0),
pending: parseInt(await redis.get('quixzoom:stats:taxonomy:pending') || 0),
},
contributors: {
total: parseInt(await redis.get('quixzoom:stats:contributors:total') || 0),
active: parseInt(await redis.get('quixzoom:stats:contributors:active') || 0),
},
};
res.json(stats);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Admin API — köstatus
app.get('/api/queue/status', async (req, res) => {
try {
const queues = {
upload: await redis.llen('quixzoom:upload:queue'),
processing: await redis.llen('quixzoom:processing:queue'),
ai: await redis.llen('quixzoom:ai:queue'),
taxonomy: await redis.llen('quixzoom:taxonomy:queue'),
dataset: await redis.llen('quixzoom:dataset:queue'),
};
res.json({ queues });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Felhantering
app.use((err, req, res, next) => {
console.error('API Error:', err);
res.status(err.status || 500).json({
error: err.message || 'Internal server error',
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
});
});
// 404
app.use((req, res) => {
res.status(404).json({ error: 'Endpoint not found' });
});
// Starta server
app.listen(PORT, () => {
console.log(`[SERVER] QUIXZOOM Capture Pipeline API running on port ${PORT}`);
console.log(`[SERVER] Environment: ${process.env.NODE_ENV || 'development'}`);
});
module.exports = app;