feat(piloto): backend Spring Boot real (Resource Server)
This commit is contained in:
@@ -1,61 +1,7 @@
|
||||
# Apps reais do tenant piloto (walking skeleton da Fase 1 do roadmap):
|
||||
# - backend: OAuth2 Resource Server (Node, sem libs externas) que valida o JWT do realm athleticmap
|
||||
# - frontend: SPA que faz login OIDC Authorization Code + PKCE (keycloak-js) e chama /api/me
|
||||
# - bff: mantido como stub (whoami) — sera substituido na evolucao
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata: { name: backend-code, namespace: piloto-prod }
|
||||
data:
|
||||
server.js: |
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const crypto = require('crypto');
|
||||
const ISSUER = process.env.ISSUER;
|
||||
const JWKS_URL = process.env.JWKS_URL;
|
||||
const ORIGIN = process.env.CORS_ORIGIN || '*';
|
||||
let keys = {}, keysAt = 0;
|
||||
function fetchJson(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const lib = url.startsWith('https') ? https : http;
|
||||
lib.get(url, (r) => { let d = ''; r.on('data', c => d += c); r.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(e); } }); }).on('error', reject);
|
||||
});
|
||||
}
|
||||
async function key(kid) {
|
||||
if (Date.now() - keysAt > 300000) { const j = await fetchJson(JWKS_URL); keys = {}; j.keys.forEach(k => keys[k.kid] = k); keysAt = Date.now(); }
|
||||
return keys[kid];
|
||||
}
|
||||
async function verify(token) {
|
||||
const [h, p, s] = token.split('.');
|
||||
const header = JSON.parse(Buffer.from(h, 'base64url').toString());
|
||||
const jwk = await key(header.kid);
|
||||
if (!jwk) throw new Error('unknown kid');
|
||||
const pub = crypto.createPublicKey({ key: jwk, format: 'jwk' });
|
||||
const ok = crypto.verify('RSA-SHA256', Buffer.from(h + '.' + p), pub, Buffer.from(s, 'base64url'));
|
||||
if (!ok) throw new Error('bad signature');
|
||||
const c = JSON.parse(Buffer.from(p, 'base64url').toString());
|
||||
if (c.iss !== ISSUER) throw new Error('bad issuer');
|
||||
if (c.exp * 1000 < Date.now()) throw new Error('expired');
|
||||
return c;
|
||||
}
|
||||
const server = http.createServer(async (req, res) => {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Access-Control-Allow-Origin', ORIGIN);
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type');
|
||||
if (req.method === 'OPTIONS') { res.statusCode = 204; res.end(); return; }
|
||||
if (req.url === '/api/public/health') { res.end(JSON.stringify({ status: 'UP', service: 'athletic-map-backend', tenant: 'piloto' })); return; }
|
||||
if (req.url === '/api/me') {
|
||||
const a = req.headers.authorization || '';
|
||||
if (!a.startsWith('Bearer ')) { res.statusCode = 401; res.end(JSON.stringify({ error: 'unauthorized' })); return; }
|
||||
try {
|
||||
const c = await verify(a.slice(7));
|
||||
res.end(JSON.stringify({ subject: c.sub, preferredUsername: c.preferred_username, email: c.email, roles: (c.realm_access || {}).roles || [], issuer: c.iss, validatedBy: 'athletic-map-backend (Node Resource Server)' }));
|
||||
} catch (e) { res.statusCode = 401; res.end(JSON.stringify({ error: 'invalid_token', detail: String(e.message) })); }
|
||||
return;
|
||||
}
|
||||
res.statusCode = 404; res.end(JSON.stringify({ error: 'not_found' }));
|
||||
});
|
||||
server.listen(8080, () => console.log('backend listening on 8080'));
|
||||
# Apps do tenant piloto:
|
||||
# - backend: Spring Boot OAuth2 Resource Server (imagem athletic-map-backend:1.0, importada no k3s)
|
||||
# - frontend: SPA OIDC Authorization Code + PKCE (keycloak-js) chamando /api/me
|
||||
# - bff: stub (whoami) — sera substituido na evolucao
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
@@ -68,21 +14,17 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: backend
|
||||
image: node:20-alpine
|
||||
command: ["node", "/app/server.js"]
|
||||
image: docker.io/library/athletic-map-backend:1.0
|
||||
imagePullPolicy: Never
|
||||
env:
|
||||
- { name: ISSUER, value: "https://auth.187.77.37.184.nip.io/realms/athleticmap" }
|
||||
- { name: JWKS_URL, value: "http://keycloak:8080/realms/athleticmap/protocol/openid-connect/certs" }
|
||||
- { name: CORS_ORIGIN, value: "https://piloto.187.77.37.184.nip.io" }
|
||||
- { name: ATM_JWK_SET_URI, value: "http://keycloak:8080/realms/athleticmap/protocol/openid-connect/certs" }
|
||||
- { name: ATM_ISSUER, value: "https://auth.187.77.37.184.nip.io/realms/athleticmap" }
|
||||
ports: [{ containerPort: 8080 }]
|
||||
volumeMounts: [{ name: code, mountPath: /app }]
|
||||
readinessProbe:
|
||||
httpGet: { path: /api/public/health, port: 8080 }
|
||||
initialDelaySeconds: 5
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 10
|
||||
volumes:
|
||||
- name: code
|
||||
configMap: { name: backend-code }
|
||||
failureThreshold: 24
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
|
||||
Reference in New Issue
Block a user