6989a98d75
- Arkitektur: docs/auth/passwordless-architecture.md - Backend: iom/quixzoom-auth-service/ (FastAPI + Redis) - Webb: quixzoom-market-pages/se/login/ (QR-kod + polling) - App: iom/quixzoom-app/src/features/auth/ (push + deep links) Flöde: QR-kod → app-godkännande → webb-inloggad
348 lines
13 KiB
JavaScript
348 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* LANDVEX MASTER API
|
|
* ==================
|
|
* Single source of truth för ALLA system.
|
|
* Läser från: amos, aamos_ledger, landvex, wavult_identity
|
|
* Exponerar: Enhetligt REST API för admin, appar, externa system
|
|
*
|
|
* Regler:
|
|
* 1. INGEN mock-data
|
|
* 2. INGEN hårdkodad data
|
|
* 3. ALLT kommer från databas
|
|
* 4. ALLA ändringar går via API:et
|
|
*/
|
|
|
|
import express from 'express';
|
|
import pg from 'pg';
|
|
const { Pool } = pg;
|
|
import cors from 'cors';
|
|
|
|
const app = express();
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
|
|
const PORT = process.env.PORT || 7073;
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// DATABASUPPKOPLINGAR
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
const pools = {
|
|
amos: new Pool({
|
|
host: 'localhost',
|
|
port: 5432,
|
|
database: 'amos',
|
|
user: 'postgres',
|
|
password: 'quixzo…2026'
|
|
}),
|
|
ledger: new Pool({
|
|
host: 'localhost',
|
|
port: 5432,
|
|
database: 'aamos_ledger',
|
|
user: 'postgres',
|
|
password: 'quixzo…2026'
|
|
}),
|
|
landvex: new Pool({
|
|
host: 'localhost',
|
|
port: 5432,
|
|
database: 'landvex',
|
|
user: 'postgres',
|
|
password: 'quixzo…2026'
|
|
})
|
|
};
|
|
|
|
// Health check för alla databaser
|
|
async function checkDatabases() {
|
|
const status = {};
|
|
for (const [name, pool] of Object.entries(pools)) {
|
|
try {
|
|
const start = Date.now();
|
|
await pool.query('SELECT 1');
|
|
status[name] = { ok: true, latency_ms: Date.now() - start };
|
|
} catch (e) {
|
|
status[name] = { ok: false, error: e.message };
|
|
}
|
|
}
|
|
return status;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// MIDDLEWARE
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
app.use((req, res, next) => {
|
|
console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
|
|
next();
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// HEALTH & STATUS
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
app.get('/health', async (req, res) => {
|
|
const dbStatus = await checkDatabases();
|
|
const allOk = Object.values(dbStatus).every(s => s.ok);
|
|
|
|
res.status(allOk ? 200 : 503).json({
|
|
status: allOk ? 'ok' : 'degraded',
|
|
service: 'landvex-master-api',
|
|
version: '1.0.0',
|
|
databases: dbStatus,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// QUIXZOOM ENDPOINTS (från amos-databasen)
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
app.get('/api/v1/quixzoom/stats', async (req, res) => {
|
|
try {
|
|
const [missions, active, submissions, contributors] = await Promise.all([
|
|
pools.amos.query('SELECT COUNT(*) FROM quixzoom.missions'),
|
|
pools.amos.query("SELECT COUNT(*) FROM quixzoom.missions WHERE status = 'active'"),
|
|
pools.amos.query('SELECT COUNT(*) FROM quixzoom.submissions'),
|
|
pools.amos.query('SELECT COUNT(DISTINCT user_id) FROM quixzoom.claims')
|
|
]);
|
|
|
|
res.json({
|
|
missions_total: parseInt(missions.rows[0].count),
|
|
missions_active: parseInt(active.rows[0].count),
|
|
submissions_total: parseInt(submissions.rows[0].count),
|
|
contributors_total: parseInt(contributors.rows[0].count),
|
|
source: 'amos.quixzoom',
|
|
cached: false
|
|
});
|
|
} catch (e) {
|
|
res.status(500).json({ error: e.message, source: 'amos.quixzoom' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/v1/quixzoom/missions', async (req, res) => {
|
|
try {
|
|
const { status, limit = 50, offset = 0 } = req.query;
|
|
|
|
let query = `
|
|
SELECT
|
|
m.id,
|
|
m.title,
|
|
m.description,
|
|
m.latitude as lat,
|
|
m.longitude as lon,
|
|
m.area_name as address,
|
|
m.status,
|
|
m.reward_credits as reward_sek,
|
|
m.required_photos,
|
|
m.created_at,
|
|
m.expires_at,
|
|
COUNT(s.id) as submission_count
|
|
FROM quixzoom.missions m
|
|
LEFT JOIN quixzoom.submissions s ON s.mission_id = m.id
|
|
`;
|
|
|
|
const params = [];
|
|
if (status) {
|
|
query += ' WHERE m.status = $1';
|
|
params.push(status);
|
|
}
|
|
|
|
query += ` GROUP BY m.id ORDER BY m.created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
|
|
params.push(limit, offset);
|
|
|
|
const result = await pools.amos.query(query, params);
|
|
const totalResult = await pools.amos.query('SELECT COUNT(*) FROM quixzoom.missions');
|
|
|
|
res.json({
|
|
missions: result.rows,
|
|
count: result.rows.length,
|
|
total: parseInt(totalResult.rows[0].count),
|
|
source: 'amos.quixzoom.missions'
|
|
});
|
|
} catch (e) {
|
|
res.status(500).json({ error: e.message, source: 'amos.quixzoom.missions' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/v1/quixzoom/missions/:id', async (req, res) => {
|
|
try {
|
|
const result = await pools.amos.query(`
|
|
SELECT * FROM quixzoom.missions WHERE id = $1
|
|
`, [req.params.id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Mission not found' });
|
|
}
|
|
|
|
res.json(result.rows[0]);
|
|
} catch (e) {
|
|
res.status(500).json({ error: e.message });
|
|
}
|
|
});
|
|
|
|
app.get('/api/v1/quixzoom/contributors', async (req, res) => {
|
|
try {
|
|
const result = await pools.amos.query(`
|
|
SELECT
|
|
u.id,
|
|
u.email,
|
|
u.display_name as name,
|
|
u.city,
|
|
u.status,
|
|
COUNT(DISTINCT s.id) as submission_count,
|
|
COALESCE(SUM(s.reward_credits), 0) as total_earnings
|
|
FROM quixzoom.users u
|
|
LEFT JOIN quixzoom.submissions s ON s.user_id = u.id
|
|
GROUP BY u.id
|
|
ORDER BY total_earnings DESC
|
|
LIMIT 50
|
|
`);
|
|
|
|
res.json({
|
|
contributors: result.rows,
|
|
count: result.rows.length,
|
|
source: 'amos.quixzoom.users'
|
|
});
|
|
} catch (e) {
|
|
res.status(500).json({ error: e.message });
|
|
}
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// LEDGER ENDPOINTS (från aamos_ledger-databasen)
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
app.get('/api/v1/ledger/accounts', async (req, res) => {
|
|
try {
|
|
const result = await pools.ledger.query(`
|
|
SELECT
|
|
a.id,
|
|
a.code as account_number,
|
|
a.name,
|
|
a.account_type,
|
|
COALESCE(SUM(jl.debit), 0) as total_debit,
|
|
COALESCE(SUM(jl.credit), 0) as total_credit,
|
|
COALESCE(SUM(jl.debit), 0) - COALESCE(SUM(jl.credit), 0) as balance
|
|
FROM accounts a
|
|
LEFT JOIN journal_lines jl ON jl.account_id = a.id
|
|
GROUP BY a.id
|
|
ORDER BY a.code
|
|
`);
|
|
|
|
res.json({
|
|
accounts: result.rows,
|
|
count: result.rows.length,
|
|
source: 'aamos_ledger.accounts'
|
|
});
|
|
} catch (e) {
|
|
res.status(500).json({ error: e.message });
|
|
}
|
|
});
|
|
|
|
app.get('/api/v1/ledger/trial-balance', async (req, res) => {
|
|
try {
|
|
const result = await pools.ledger.query(`
|
|
SELECT
|
|
a.code as account_number,
|
|
a.name,
|
|
a.account_type,
|
|
COALESCE(SUM(jl.debit), 0) as total_debit,
|
|
COALESCE(SUM(jl.credit), 0) as total_credit
|
|
FROM accounts a
|
|
LEFT JOIN journal_lines jl ON jl.account_id = a.id
|
|
GROUP BY a.id
|
|
HAVING COALESCE(SUM(jl.debit), 0) > 0 OR COALESCE(SUM(jl.credit), 0) > 0
|
|
ORDER BY a.code
|
|
`);
|
|
|
|
const totalDebit = result.rows.reduce((sum, r) => sum + parseFloat(r.total_debit), 0);
|
|
const totalCredit = result.rows.reduce((sum, r) => sum + parseFloat(r.total_credit), 0);
|
|
|
|
res.json({
|
|
rows: result.rows,
|
|
total_debit: totalDebit,
|
|
total_credit: totalCredit,
|
|
balanced: Math.abs(totalDebit - totalCredit) < 0.01,
|
|
source: 'aamos_ledger.journal_lines'
|
|
});
|
|
} catch (e) {
|
|
res.status(500).json({ error: e.message });
|
|
}
|
|
});
|
|
|
|
app.get('/api/v1/ledger/journal-entries', async (req, res) => {
|
|
try {
|
|
const { limit = 50, offset = 0 } = req.query;
|
|
|
|
const result = await pools.ledger.query(`
|
|
SELECT
|
|
je.id,
|
|
je.entry_number,
|
|
je.description,
|
|
je.entry_date,
|
|
je.status,
|
|
jl.account_id,
|
|
a.account_number,
|
|
a.name as account_name,
|
|
jl.debit,
|
|
jl.credit
|
|
FROM journal_entries je
|
|
JOIN journal_lines jl ON jl.journal_entry_id = je.id
|
|
JOIN accounts a ON a.id = jl.account_id
|
|
ORDER BY je.entry_date DESC, je.entry_number DESC
|
|
LIMIT $1 OFFSET $2
|
|
`, [limit, offset]);
|
|
|
|
res.json({
|
|
entries: result.rows,
|
|
count: result.rows.length,
|
|
source: 'aamos_ledger.journal_entries'
|
|
});
|
|
} catch (e) {
|
|
res.status(500).json({ error: e.message });
|
|
}
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// SYSTEM ENDPOINTS
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
app.get('/api/v1/system/services', async (req, res) => {
|
|
// Hämta faktisk status från systemd
|
|
const services = [
|
|
{ name: 'aamos-ledger', port: 3250, db: 'aamos_ledger' },
|
|
{ name: 'quixzoom-mission', port: 7060, db: 'amos' },
|
|
{ name: 'landvex-api', port: 8081, db: 'landvex' },
|
|
{ name: 'quixzoom-api', port: 8080, db: 'amos' },
|
|
{ name: 'nginx', port: 80, db: null }
|
|
];
|
|
|
|
res.json({ services });
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// START
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
app.listen(PORT, '0.0.0.0', async () => {
|
|
console.log(`╔══════════════════════════════════════════════════════════════╗`);
|
|
console.log(`║ LANDVEX MASTER API v1.0.0 ║`);
|
|
console.log(`║ Single Source of Truth ║`);
|
|
console.log(`╠══════════════════════════════════════════════════════════════╣`);
|
|
console.log(`║ Port: ${PORT} ║`);
|
|
console.log(`║ Databases: amos, aamos_ledger, landvex ║`);
|
|
console.log(`╠══════════════════════════════════════════════════════════════╣`);
|
|
console.log(`║ Endpoints: ║`);
|
|
console.log(`║ GET /health ║`);
|
|
console.log(`║ GET /api/v1/quixzoom/stats ║`);
|
|
console.log(`║ GET /api/v1/quixzoom/missions ║`);
|
|
console.log(`║ GET /api/v1/quixzoom/contributors ║`);
|
|
console.log(`║ GET /api/v1/ledger/accounts ║`);
|
|
console.log(`║ GET /api/v1/ledger/trial-balance ║`);
|
|
console.log(`║ GET /api/v1/ledger/journal-entries ║`);
|
|
console.log(`╚══════════════════════════════════════════════════════════════╝`);
|
|
|
|
const dbStatus = await checkDatabases();
|
|
console.log('\nDatabase status:', dbStatus);
|
|
});
|