diff --git a/tenants/demo/20-keycloak.yaml b/tenants/demo/20-keycloak.yaml index bd25926..85a2f4d 100644 --- a/tenants/demo/20-keycloak.yaml +++ b/tenants/demo/20-keycloak.yaml @@ -17,7 +17,7 @@ spec: containers: - name: keycloak image: quay.io/keycloak/keycloak:26.0 - args: ["start-dev"] + args: ["start-dev", "--import-realm"] env: - name: KC_DB value: postgres @@ -64,6 +64,14 @@ spec: initialDelaySeconds: 30 periodSeconds: 10 failureThreshold: 40 + volumeMounts: + - name: realm-import + mountPath: /opt/keycloak/data/import + readOnly: true + volumes: + - name: realm-import + configMap: + name: kc-realm-import --- apiVersion: v1 kind: Service diff --git a/tenants/demo/30-apps-stubs.yaml b/tenants/demo/30-apps-stubs.yaml index db68120..a9c4047 100644 --- a/tenants/demo/30-apps-stubs.yaml +++ b/tenants/demo/30-apps-stubs.yaml @@ -1,4 +1,58 @@ -# Esqueletos (stubs) de Backend, BFF e Frontend do tenant demo + Ingress TLS +# Apps reais do tenant demo: backend Resource Server (Node) + SPA OIDC Code+PKCE +--- +apiVersion: v1 +kind: ConfigMap +metadata: { name: backend-code, namespace: demo-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: 'demo' })); 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')); --- apiVersion: apps/v1 kind: Deployment @@ -10,17 +64,29 @@ spec: metadata: { labels: { app: backend } } spec: containers: - - name: whoami - image: traefik/whoami:latest - args: ["--name", "athletic-map-backend demo (stub)"] - ports: [{ containerPort: 80 }] + - name: backend + image: node:20-alpine + command: ["node", "/app/server.js"] + env: + - { name: ISSUER, value: "https://auth-demo.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://demo.187.77.37.184.nip.io" } + ports: [{ containerPort: 8080 }] + volumeMounts: [{ name: code, mountPath: /app }] + readinessProbe: + httpGet: { path: /api/public/health, port: 8080 } + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: code + configMap: { name: backend-code } --- apiVersion: v1 kind: Service metadata: { name: backend, namespace: demo-prod } spec: selector: { app: backend } - ports: [{ port: 80, targetPort: 80 }] + ports: [{ port: 80, targetPort: 8080 }] --- apiVersion: apps/v1 kind: Deployment @@ -50,13 +116,38 @@ metadata: { name: frontend-index, namespace: demo-prod } data: index.html: | - Athletic Map — Demo - -

Athletic Map

-

Frontend (stub) — silo demo no k3s

-

tenant: demo

+ + Athletic Map — Demo + + +

Carregando…

+ --- apiVersion: apps/v1 kind: Deployment @@ -65,7 +156,9 @@ spec: replicas: 1 selector: { matchLabels: { app: frontend } } template: - metadata: { labels: { app: frontend } } + metadata: + labels: { app: frontend } + annotations: { configVersion: "spa-1" } spec: containers: - name: nginx @@ -97,23 +190,14 @@ spec: - hosts: - demo.187.77.37.184.nip.io - auth-demo.187.77.37.184.nip.io - - api-demo.187.77.37.184.nip.io - - bff-demo.187.77.37.184.nip.io secretName: demo-tls rules: - host: demo.187.77.37.184.nip.io http: paths: + - { path: /api, pathType: Prefix, backend: { service: { name: backend, port: { number: 80 } } } } - { path: /, pathType: Prefix, backend: { service: { name: frontend, port: { number: 80 } } } } - host: auth-demo.187.77.37.184.nip.io http: paths: - { path: /, pathType: Prefix, backend: { service: { name: keycloak, port: { number: 8080 } } } } - - host: api-demo.187.77.37.184.nip.io - http: - paths: - - { path: /, pathType: Prefix, backend: { service: { name: backend, port: { number: 80 } } } } - - host: bff-demo.187.77.37.184.nip.io - http: - paths: - - { path: /, pathType: Prefix, backend: { service: { name: bff, port: { number: 80 } } } } diff --git a/tenants/demo/35-realm-import-cm.yaml b/tenants/demo/35-realm-import-cm.yaml new file mode 100644 index 0000000..0519c2b --- /dev/null +++ b/tenants/demo/35-realm-import-cm.yaml @@ -0,0 +1,51 @@ +apiVersion: v1 +data: + athleticmap-realm.json: | + { + "realm": "athleticmap", + "enabled": true, + "displayName": "Athletic Map", + "loginWithEmailAllowed": true, + "roles": { + "realm": [ + { "name": "admin" }, + { "name": "atm_athlete" }, + { "name": "atm_trainer" }, + { "name": "atm_team_admin" }, + { "name": "atm_fed_admin" } + ] + }, + "clients": [ + { + "clientId": "spa", + "name": "Athletic Map SPA", + "publicClient": true, + "protocol": "openid-connect", + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "redirectUris": ["https://demo.187.77.37.184.nip.io/*"], + "webOrigins": ["https://demo.187.77.37.184.nip.io"], + "attributes": { + "pkce.code.challenge.method": "S256", + "post.logout.redirect.uris": "https://demo.187.77.37.184.nip.io/*" + } + } + ], + "users": [ + { + "username": "atleta1", + "enabled": true, + "emailVerified": true, + "email": "atleta1@demo.local", + "firstName": "Atleta", + "lastName": "Um", + "credentials": [{ "type": "password", "value": "Teste@123", "temporary": false }], + "realmRoles": ["atm_athlete"] + } + ] + } +kind: ConfigMap +metadata: + name: kc-realm-import + namespace: demo-prod