Files
boc/landvex-master-api/index.mjs
T
Bernt 6989a98d75 feat: Passwordless cross-device authentication
- 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
2026-07-07 07:11:50 +00:00

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);
});