LandveX Gateway v0.1.0 + Operations API + Communications Module — ALL RUNNING
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* LandveX Operations API
|
||||
*
|
||||
* Government customers, billing, contracts, self-service
|
||||
* Port: 3005
|
||||
*/
|
||||
|
||||
import http from 'http';
|
||||
import { handleRequest } from './router.mjs';
|
||||
|
||||
const PORT = process.env.PORT || 3005;
|
||||
|
||||
const server = http.createServer(handleRequest);
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`🏛️ LandveX Operations API running on port ${PORT}`);
|
||||
console.log(`📋 Endpoints:`);
|
||||
console.log(` GET /health → Health check`);
|
||||
console.log(` GET /api/v1/government/me → My profile`);
|
||||
console.log(` PUT /api/v1/government/payment → Update payment settings`);
|
||||
console.log(` GET /api/v1/government/budget → Budget overview`);
|
||||
console.log(` POST /api/v1/government/areas → Add watch area`);
|
||||
console.log(` GET /api/v1/government/areas → List watch areas`);
|
||||
console.log(` POST /api/v1/government/users → Invite user`);
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* LandveX Operations API
|
||||
*
|
||||
* Government customers, billing, contracts, self-service
|
||||
* Port: 3005
|
||||
*/
|
||||
|
||||
import http from 'http';
|
||||
import { handleRequest } from './router.js';
|
||||
|
||||
const PORT = process.env.PORT || 3005;
|
||||
|
||||
const server = http.createServer(handleRequest);
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`🏛️ LandveX Operations API running on port ${PORT}`);
|
||||
console.log(`📋 Endpoints:`);
|
||||
console.log(` GET /health → Health check`);
|
||||
console.log(` GET /api/v1/government/me → My profile`);
|
||||
console.log(` PUT /api/v1/government/payment → Update payment settings`);
|
||||
console.log(` GET /api/v1/government/budget → Budget overview`);
|
||||
console.log(` POST /api/v1/government/areas → Add watch area`);
|
||||
console.log(` GET /api/v1/government/areas → List watch areas`);
|
||||
console.log(` POST /api/v1/government/users → Invite user`);
|
||||
});
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* LandveX Operations API — Router
|
||||
* Zero-dependency HTTP router
|
||||
*/
|
||||
|
||||
import http from 'http';
|
||||
import { URL } from 'url';
|
||||
|
||||
// In-memory store (replace with PostgreSQL)
|
||||
const customers = {};
|
||||
const watchAreas = {};
|
||||
const users = {};
|
||||
|
||||
export function handleRequest(req, res) {
|
||||
// CORS
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.statusCode = 204;
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
||||
const path = url.pathname;
|
||||
|
||||
// Health check
|
||||
if (path === '/health') {
|
||||
res.end(JSON.stringify({
|
||||
status: 'ok',
|
||||
version: '0.1.0-operations',
|
||||
service: 'operations',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Government customer endpoints
|
||||
if (path === '/api/v1/government/me') {
|
||||
handleGetProfile(req, res);
|
||||
} else if (path === '/api/v1/government/payment' && req.method === 'PUT') {
|
||||
handleUpdatePayment(req, res);
|
||||
} else if (path === '/api/v1/government/budget') {
|
||||
handleGetBudget(req, res);
|
||||
} else if (path === '/api/v1/government/areas' && req.method === 'POST') {
|
||||
handleAddArea(req, res);
|
||||
} else if (path === '/api/v1/government/areas' && req.method === 'GET') {
|
||||
handleListAreas(req, res);
|
||||
} else if (path === '/api/v1/government/users' && req.method === 'POST') {
|
||||
handleInviteUser(req, res);
|
||||
} else {
|
||||
res.statusCode = 404;
|
||||
res.end(JSON.stringify({ error: 'Not Found', path }));
|
||||
}
|
||||
}
|
||||
|
||||
function handleGetProfile(req, res) {
|
||||
const profile = {
|
||||
id: 'gov-001',
|
||||
orgNumber: '212000-0142',
|
||||
name: 'Stockholms stad',
|
||||
type: 'kommun',
|
||||
address: 'Stockholms stadshus, 111 83 Stockholm',
|
||||
contactEmail: 'inkop@stockholm.se',
|
||||
paymentSettings: {
|
||||
method: 'invoice',
|
||||
invoiceAddress: 'Stockholms stad, FE 101, 838 73 Frösön',
|
||||
reference: 'Avd. Gatu- och fastighetskontoret',
|
||||
ocrNumber: '2120000142',
|
||||
},
|
||||
budgetLimits: {
|
||||
monthly: 50000,
|
||||
yearly: 600000,
|
||||
alertAtPercent: 80,
|
||||
blockAtPercent: 100,
|
||||
},
|
||||
createdAt: '2024-01-15T10:00:00Z',
|
||||
};
|
||||
|
||||
res.end(JSON.stringify(profile));
|
||||
}
|
||||
|
||||
function handleUpdatePayment(req, res) {
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
message: 'Payment settings updated',
|
||||
settings: data,
|
||||
}));
|
||||
} catch {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleGetBudget(req, res) {
|
||||
const budget = {
|
||||
monthlyLimit: 50000,
|
||||
monthlyUsed: 32450,
|
||||
monthlyRemaining: 17550,
|
||||
percentUsed: 64.9,
|
||||
yearlyLimit: 600000,
|
||||
yearlyUsed: 187340,
|
||||
yearlyRemaining: 412660,
|
||||
yearlyPercentUsed: 31.2,
|
||||
alertTriggered: false,
|
||||
blockTriggered: false,
|
||||
forecast: {
|
||||
projectedYearly: 582000,
|
||||
status: 'on_track',
|
||||
},
|
||||
breakdown: [
|
||||
{ category: 'Road Inspection', amount: 12400 },
|
||||
{ category: 'Bridge Survey', amount: 8900 },
|
||||
{ category: 'Property Scan', amount: 6750 },
|
||||
{ category: 'Emergency Call', amount: 4400 },
|
||||
],
|
||||
};
|
||||
|
||||
res.end(JSON.stringify(budget));
|
||||
}
|
||||
|
||||
function handleAddArea(req, res) {
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
const area = {
|
||||
id: `area-${Date.now()}`,
|
||||
...data,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const customerId = 'gov-001';
|
||||
if (!watchAreas[customerId]) watchAreas[customerId] = [];
|
||||
watchAreas[customerId].push(area);
|
||||
|
||||
res.statusCode = 201;
|
||||
res.end(JSON.stringify(area));
|
||||
} catch {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleListAreas(req, res) {
|
||||
const customerId = 'gov-001';
|
||||
const areas = watchAreas[customerId] || [];
|
||||
|
||||
res.end(JSON.stringify({
|
||||
areas,
|
||||
count: areas.length,
|
||||
}));
|
||||
}
|
||||
|
||||
function handleInviteUser(req, res) {
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
const user = {
|
||||
id: `user-${Date.now()}`,
|
||||
email: data.email,
|
||||
name: data.name,
|
||||
role: data.role || 'viewer',
|
||||
invitedAt: new Date().toISOString(),
|
||||
status: 'pending',
|
||||
};
|
||||
|
||||
const customerId = 'gov-001';
|
||||
if (!users[customerId]) users[customerId] = [];
|
||||
users[customerId].push(user);
|
||||
|
||||
res.statusCode = 201;
|
||||
res.end(JSON.stringify(user));
|
||||
} catch {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* LandveX Operations API — Router
|
||||
* Zero-dependency HTTP router
|
||||
*/
|
||||
|
||||
import http from 'http';
|
||||
import { URL } from 'url';
|
||||
|
||||
// In-memory store (replace with PostgreSQL)
|
||||
const customers: Record<string, any> = {};
|
||||
const watchAreas: Record<string, any[]> = {};
|
||||
const users: Record<string, any[]> = {};
|
||||
|
||||
export function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||
// CORS
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.statusCode = 204;
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
||||
const path = url.pathname;
|
||||
|
||||
// Health check
|
||||
if (path === '/health') {
|
||||
res.end(JSON.stringify({
|
||||
status: 'ok',
|
||||
version: '0.1.0-operations',
|
||||
service: 'operations',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Government customer endpoints
|
||||
if (path === '/api/v1/government/me') {
|
||||
handleGetProfile(req, res);
|
||||
} else if (path === '/api/v1/government/payment' && req.method === 'PUT') {
|
||||
handleUpdatePayment(req, res);
|
||||
} else if (path === '/api/v1/government/budget') {
|
||||
handleGetBudget(req, res);
|
||||
} else if (path === '/api/v1/government/areas' && req.method === 'POST') {
|
||||
handleAddArea(req, res);
|
||||
} else if (path === '/api/v1/government/areas' && req.method === 'GET') {
|
||||
handleListAreas(req, res);
|
||||
} else if (path === '/api/v1/government/users' && req.method === 'POST') {
|
||||
handleInviteUser(req, res);
|
||||
} else {
|
||||
res.statusCode = 404;
|
||||
res.end(JSON.stringify({ error: 'Not Found', path }));
|
||||
}
|
||||
}
|
||||
|
||||
function handleGetProfile(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||
// Mock profile — in real implementation, extract from JWT
|
||||
const profile = {
|
||||
id: 'gov-001',
|
||||
orgNumber: '212000-0142',
|
||||
name: 'Stockholms stad',
|
||||
type: 'kommun',
|
||||
address: 'Stockholms stadshus, 111 83 Stockholm',
|
||||
contactEmail: 'inkop@stockholm.se',
|
||||
paymentSettings: {
|
||||
method: 'invoice',
|
||||
invoiceAddress: 'Stockholms stad, FE 101, 838 73 Frösön',
|
||||
reference: 'Avd. Gatu- och fastighetskontoret',
|
||||
ocrNumber: '2120000142',
|
||||
},
|
||||
budgetLimits: {
|
||||
monthly: 50000,
|
||||
yearly: 600000,
|
||||
alertAtPercent: 80,
|
||||
blockAtPercent: 100,
|
||||
},
|
||||
createdAt: '2024-01-15T10:00:00Z',
|
||||
};
|
||||
|
||||
res.end(JSON.stringify(profile));
|
||||
}
|
||||
|
||||
function handleUpdatePayment(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
message: 'Payment settings updated',
|
||||
settings: data,
|
||||
}));
|
||||
} catch {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleGetBudget(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||
const budget = {
|
||||
monthlyLimit: 50000,
|
||||
monthlyUsed: 32450,
|
||||
monthlyRemaining: 17550,
|
||||
percentUsed: 64.9,
|
||||
yearlyLimit: 600000,
|
||||
yearlyUsed: 187340,
|
||||
yearlyRemaining: 412660,
|
||||
yearlyPercentUsed: 31.2,
|
||||
alertTriggered: false,
|
||||
blockTriggered: false,
|
||||
forecast: {
|
||||
projectedYearly: 582000,
|
||||
status: 'on_track',
|
||||
},
|
||||
breakdown: [
|
||||
{ category: 'Road Inspection', amount: 12400 },
|
||||
{ category: 'Bridge Survey', amount: 8900 },
|
||||
{ category: 'Property Scan', amount: 6750 },
|
||||
{ category: 'Emergency Call', amount: 4400 },
|
||||
],
|
||||
};
|
||||
|
||||
res.end(JSON.stringify(budget));
|
||||
}
|
||||
|
||||
function handleAddArea(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
const area = {
|
||||
id: `area-${Date.now()}`,
|
||||
...data,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const customerId = 'gov-001';
|
||||
if (!watchAreas[customerId]) watchAreas[customerId] = [];
|
||||
watchAreas[customerId].push(area);
|
||||
|
||||
res.statusCode = 201;
|
||||
res.end(JSON.stringify(area));
|
||||
} catch {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleListAreas(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||
const customerId = 'gov-001';
|
||||
const areas = watchAreas[customerId] || [];
|
||||
|
||||
res.end(JSON.stringify({
|
||||
areas,
|
||||
count: areas.length,
|
||||
}));
|
||||
}
|
||||
|
||||
function handleInviteUser(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
const user = {
|
||||
id: `user-${Date.now()}`,
|
||||
email: data.email,
|
||||
name: data.name,
|
||||
role: data.role || 'viewer',
|
||||
invitedAt: new Date().toISOString(),
|
||||
status: 'pending',
|
||||
};
|
||||
|
||||
const customerId = 'gov-001';
|
||||
if (!users[customerId]) users[customerId] = [];
|
||||
users[customerId].push(user);
|
||||
|
||||
res.statusCode = 201;
|
||||
res.end(JSON.stringify(user));
|
||||
} catch {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user