bae705aa97
- 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
178 lines
4.9 KiB
JavaScript
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;
|