livrare lot 2

This commit is contained in:
EVOTECH IT SRL 2026-07-10 03:39:53 -07:00
commit 8ecc78e729
763 changed files with 164593 additions and 0 deletions

View file

@ -0,0 +1,263 @@
# Gateway & Auth Layer - DIDI Backend 🔐
## Quick Start 🚀
```bash
# Recommended: Start via unified deployment manager
./deploy/didi.sh staging start
```
**Staging URLs:**
| Service | URL |
|---------|-----|
| Kong Gateway | http://localhost:18100 |
| Kong Admin API | http://localhost:18101 |
| Kong Manager UI | http://localhost:18102 |
| Keycloak | http://localhost:18280 |
## Overview 🔍
The Gateway & Auth Layer provides API management and authentication services:
### Services
| Service | Container Name | Staging Ports | Purpose |
|---------|---------------|---------------|---------|
| **didiKong** | `staging-gatewayAuthLayer-kong` | 18100, 18101, 18102 | API Gateway, routing, rate limiting |
| **didiKeycloak** | `staging-gatewayAuthLayer-keycloak` | 18280 | Identity provider, SSO, OAuth2/OIDC |
## Architecture 🏗️
```
┌─────────────────────────────────────────────────────────┐
│ Gateway & Auth Layer │
├───────────────────────┬─────────────────────────────────┤
│ didiKong │ didiKeycloak │
│ │ │
│ • API Gateway │ • Identity Provider │
│ • Route Management │ • User Management │
│ • Rate Limiting │ • OAuth2/OIDC │
│ • CORS Handling │ • Custom Theme │
│ • Load Balancing │ • Realm Import │
│ │ │
│ Ports: 18100-18102 │ Port: 18280 │
└───────────────────────┴─────────────────────────────────┘
│ │
▼ ▼
┌──────────────────────────────────────┐
│ Protected Services │
│ • Orchestrator API (port 18000) │
│ • Analysis Service (port 18004) │
│ • Admin Dashboard (port 13003) │
└──────────────────────────────────────┘
```
## Service Communication Flow 📬
```
Client Request → Kong Gateway (18100) → Route Rules → Backend Service
Rate Limiting
CORS Headers
(Optional) Keycloak Auth
```
## Kong Configuration 🛠️
### DB-less Mode
Kong runs in declarative (DB-less) mode with configuration in `didiKong/declarative/kong.yml`
### Configured Routes (Staging)
- `/api/v1/catalog/*` → Orchestrator (18000)
- `/api/v1/pipelines/*` → Orchestrator (18000)
- `/api/v1/runs/*` → Orchestrator (18000)
- `/orchestrator/health` → Orchestrator health check
- `/analysis/health` → Analysis service health check
- `/didiai/*` → AI Gateway (via DIDIAI_GATEWAY_URL)
- `/admin/*` → Admin Dashboard (13003)
### Enabled Plugins
- **CORS**: Cross-origin resource sharing
- **Rate Limiting**: 100/min, 2000/hr, 10000/day
- **Request ID**: UUID tracking with X-Request-ID
- **Size Limiting**: 100MB max for media files
- **Response Transform**: Add gateway headers
## Keycloak Configuration 🔑
### Admin Access
- **URL**: http://localhost:8280 (standalone) or http://localhost:18280 (staging)
- **Username**: admin
- **Password**: keycloak123
### Imported Realm
- **Realm Name**: misinformation
- **Theme**: misinformation-theme (custom)
- **Location**: `didiKeycloak/realm-import/misinformation-realm.json`
### Pre-configured Elements
- Client applications
- User roles and groups
- Authentication flows
- Custom login theme
## Quick Commands 🎯
```bash
# Service Management
make up # Start both services
make down # Stop both services
make restart # Restart both services
make status # Check service status
make logs # View logs for both services
# Individual Service Control
make up-kong # Start only Kong
make up-keycloak # Start only Keycloak
make logs-kong # View Kong logs
make logs-keycloak # View Keycloak logs
# Kong Management
make kong-reload # Reload Kong configuration
make kong-validate # Validate kong.yml syntax
# Keycloak Management
make keycloak-export # Export current realm configuration
# Maintenance
make clean # Remove containers and volumes
make rebuild # Rebuild all images
make health # Health check both services
```
## Environment Configuration 🔐
### Kong Environment
```env
KONG_DATABASE=off # DB-less mode
KONG_DECLARATIVE_CONFIG=/kong/declarative/kong.yml
KONG_PROXY_LISTEN=0.0.0.0:8000
KONG_ADMIN_LISTEN=0.0.0.0:8001
```
### Keycloak Environment
```env
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=keycloak123
KC_DB_URL=jdbc:postgresql://dataLayer-postgres:5432/keycloak_db
KC_DB_USERNAME=postgres
KC_DB_PASSWORD=postgres123
```
## Testing the Gateway 🧪
### Test Kong Gateway (Staging)
```bash
# Check Kong status
curl http://localhost:18101/status
# Test orchestrator route through Kong
curl http://localhost:18100/orchestrator/health
# Test analysis route through Kong
curl http://localhost:18100/analysis/health
```
### Test Keycloak (Staging)
```bash
# Check Keycloak health
curl http://localhost:18280/health/ready
# Access Keycloak admin console
open http://localhost:18280
```
## Troubleshooting 🔧
### Kong won't start?
```bash
# Check configuration validity
make kong-validate
# Check logs
make logs-kong
# Verify declarative config exists
ls -la didiKong/declarative/kong.yml
```
### Keycloak won't start?
```bash
# Check if database exists
docker exec dataLayer-postgres psql -U postgres -c "\l" | grep keycloak_db
# Create database if missing
make init-db
# Check logs
make logs-keycloak
```
### Services can't connect?
```bash
# Verify network exists
docker network ls | grep didi-backend
# Check all services are on same network
docker inspect gatewayAuthLayer-kong | grep NetworkMode
```
## Security Considerations 🛡️
1. **Change default passwords** in production
2. **Enable HTTPS** for all services
3. **Configure proper CORS origins** (not wildcard)
4. **Set up proper rate limiting** per consumer
5. **Enable authentication** on sensitive routes
6. **Use secrets management** for credentials
## Integration with Other Layers 🔗
### Prerequisites
- Data Layer must be running (PostgreSQL for Keycloak)
- Orchestration Layer services for API routing
- Network `didi-backend` must exist
### Downstream Services
- UI Layer will use Kong Gateway for API access
- All services can integrate with Keycloak for SSO
## Development 🛠️
### Access Service Shells
```bash
make shell-kong # Kong shell
make shell-keycloak # Keycloak shell
```
### Modify Kong Routes
1. Edit `didiKong/declarative/kong.yml`
2. Validate: `make kong-validate`
3. Reload: `make kong-reload`
### Export Keycloak Configuration
```bash
make keycloak-export
# Exported to didiKeycloak/realm-export/
```
## Next Steps 📋
1. Configure Keycloak clients for each service
2. Set up Kong OAuth2 plugin with Keycloak
3. Add service-specific rate limiting
4. Configure monitoring and alerting
5. Set up SSL/TLS termination
---
**Version**: 1.0.0
**Network**: `didi-backend`
**Project**: `didiBackend`

View file

@ -0,0 +1,362 @@
# didiKeycloak - Index
> **Deployment LOCAL (activ)**: Keycloak ruleaza ca un singur container `didi-keycloak` pe masina de deployment. Nu exista cluster SSO / Swarm.
>
> - Imagine: `quay.io/keycloak/keycloak:26.0`, pornit cu `start-dev --import-realm`.
> - Port: `28080` (host) -> `8080` (container), servit sub calea relativa `/auth` (`KC_HTTP_RELATIVE_PATH=/auth`).
> - `KC_HOSTNAME_STRICT=false`, `KC_PROXY_HEADERS=xforwarded` — hostname derivat din headerele proxy-ului din fata.
> - **Doua realm-uri** importate din `realm-import/`: `didi-clients` (useri finali) + `didi-admins` (operatori: admin / moderator / senior_moderator).
> - Temele custom sunt bind-mount-uite din folderul acesta in `/opt/keycloak/themes/`.
> - Master credentials: `admin/admin123` (`KEYCLOAK_ADMIN` / `KEYCLOAK_ADMIN_PASSWORD`).
Serviciul de autentificare si autorizare al platformei DIDI. Bazat pe Keycloak, gestioneaza utilizatori, roluri, grupuri, clienti OAuth2 si token-uri JWT. Include teme custom de login si template-uri email in romana.
**Imagine**: quay.io/keycloak/keycloak:26.0 (container local `didi-keycloak`)
**Container**: didi-keycloak (activ, pe masina de deployment)
**Port**: 28080 (host) -> 8080 (container), sub `/auth`
**Realm-uri**: `didi-clients` (useri) + `didi-admins` (operatori)
**Baza de date**: PostgreSQL `didi-postgres:5432/DIDI` (`KC_DB=postgres`, user `bos_interface`)
---
## Ce face
1. **Autentificare OAuth2/OIDC** -- login, logout, refresh token, SSO
2. **Management utilizatori** -- creare, roluri, grupuri, tier-uri
3. **Emitere token-uri JWT** -- access token (10 min), refresh token, SSO session (2h)
4. **Validare JWT** -- Kong valideaza token-urile emise de Keycloak
5. **Securitate cont** -- brute force (lockout dupa 5 incercari esuate), MFA TOTP, password policy
6. **Deep linking mobil** -- redirectare catre app mobila dupa verificare email
7. **Teme custom** -- login page dark purple, emailuri in romana
---
## Structura fisierelor
```
realm-import/
didi-clients-realm.json -- Configurare completa realm (clienti, roluri, grupuri, utilizatori)
themes/
didi-clients-theme/ -- Tema principala (dark purple)
login/
theme.properties -- Configurare tema login
register.ftl -- Formular inregistrare
login-reset-password.ftl -- Resetare parola
login-verify-email.ftl -- Pagina verificare email
info.ftl -- Routing mobil/web dupa actiuni
register-commons.ftl -- Macro acceptare termeni
messages/
messages_en.properties -- Etichete UI engleza
resources/
css/login.css -- Stil dark purple (784 linii)
js/placeholders.js -- Placeholders formulare
email/
theme.properties -- Configurare tema email
html/
email-verification.ftl -- Template verificare email (romana, dark theme)
executeActions.ftl -- Template actiuni (dark purple gradient)
text/
email-verification.ftl -- Versiune text plain
didi-ai-theme/ -- Tema alternativa (white, blue accents)
login/
theme.properties
resources/
css/login.css
img/logo.png
didi-backend-theme/ -- Tema backend (white, "didi - Backend")
login/
theme.properties
resources/
css/login.css
img/logo.png
```
Zero cod custom backend. Doar configurare realm JSON + teme FreeMarker/CSS.
---
## Clienti OAuth2 (4)
| Client ID | Tip | Scop | Flow-uri | PKCE |
|-----------|-----|------|----------|------|
| didi-web-app | Public | Frontend web utilizatori | Standard + Direct Access | nu |
| admin-dashboard | Public | Dashboard admin React | Standard + Direct Access | S256 |
| orchestrator-api | Confidential | Serviciu backend orchestrator | Direct Access + Service Account | nu |
| kong-api-gateway | Bearer Only | Gateway JWT validation | Service Account only | nu |
### didi-web-app
- Redirect URIs: localhost:3001, localhost:5173, localhost:13001, localhost:33001 (+ 127.0.0.1)
- Web Origins: aceleasi + wildcard
- Scopes: web-origins, acr, profile, roles, email
### admin-dashboard
- Root URL: http://localhost:13003
- PKCE: S256 (obligatoriu)
- Redirect URIs: localhost:13003, localhost:3003, localhost:33003, localhost:33001, localhost:3001, 127.0.0.1:13003, 127.0.0.1:3003, 127.0.0.1:33003, 127.0.0.1:33001, 10.11.50.11:33003, 10.11.50.11:3003
- Post Logout: localhost:13003, localhost:3003, localhost:33003, 10.11.50.11:33003
### orchestrator-api
- Secret: nmmImrmPAcADuPh-ZTqLY7GDhCAjfXsolDOM6TxZbHg
- Service Account: activat
- Bearer Only: implicit (confidential)
### kong-api-gateway
- Secret: Fu1rJ8QsjCj4j4_qZiMXyx6Ewo3xC2ik7X5m_MvSLOE
- Bearer Only: da (nu face login, doar valideaza)
- Service Account: activat
---
## Roluri (doua realm-uri)
Operatorii (admin / moderator / senior_moderator) traiesc in realm-ul **`didi-admins`**; realm-ul **`didi-clients`** contine doar capabilitati de user si tier-uri de abonament.
### Realm `didi-admins` (operatori)
| Rol | Scop |
|-----|------|
| admin | Acces complet la platforma + admin dashboard |
| moderator | HIL moderator -- poate revendica si rezolva intrari din coada (admin dashboard /moderation) |
| senior_moderator | Senior HIL moderator -- poate escalada si forta gold atom in brain |
### Realm `didi-clients` (useri finali)
| Rol | Scop |
|-----|------|
| viewer | Poate vizualiza rezultate analize |
| analyst | Poate crea si gestiona analize |
| api_user | Poate accesa endpoint-uri API |
| free_tier | Privilegii tier gratuit |
| paid_tier | Privilegii tier platit |
| enterprise_tier | Privilegii tier enterprise |
Roluri implicite la inregistrare (didi-clients): viewer + free_tier
---
## Grupuri (6)
| Grup | Roluri | Tier | Limita zilnica | Rate limit |
|------|--------|------|----------------|------------|
| free-users | free_tier, viewer, api_user | free | 10 | 10/min |
| paid-users | paid_tier, viewer, analyst, api_user | paid | 100 | 60/min |
| enterprise-users | enterprise_tier, viewer, analyst, api_user | enterprise | nelimitat | 600/min |
| administrators | admin, analyst, viewer, api_user, enterprise_tier | admin | nelimitat | nelimitat |
| Grup | Roluri | Scop |
|------|--------|------|
| moderators-team | moderator | HIL review staff |
| senior-moderators-team | moderator + senior_moderator | Lead moderators with brain gold-promotion authority |
Atributele de grup (tier, daily_limit, rate_limit) sunt disponibile in token-ul JWT si pot fi folosite de Kong/backend pentru rate limiting.
---
## Acces admin dashboard
| Pagina admin dashboard | viewer / paid_tier / etc | moderator | senior_moderator | admin |
|---|---|---|---|---|
| /admin/* (any) | 403 (Unauthorized page -> public app) | Dashboard + History + Moderation | same + force_gold_brain | tot |
| /users, /framework, /llm-components, /providers | nu | nu | nu | da |
| /history | nu | da | da | da |
| /moderation/* | nu | da | da | da |
Note: `viewer` este rolul implicit asignat la toate signup-urile (`defaultRoles: [viewer, free_tier]`). End-userii (clientii) primesc acest rol; ei NU vad niciodata admin dashboard.
---
## Utilizatori pre-configurati (5)
| Email | Parola | Grup | Rol principal |
|-------|--------|------|---------------|
| admin@didi.local | admin123 | administrators | admin |
| demo@didi.local | Demo123! | free-users | viewer |
| free@didi.local | password123 | free-users | free_tier |
| paid@didi.local | password123 | paid-users | paid_tier |
| enterprise@didi.local | password123 | enterprise-users | enterprise_tier |
Toti au emailVerified: true. Parolele nu sunt temporare.
---
## Setari token
| Parametru | Valoare |
|-----------|---------|
| Access Token Lifespan | 600s (10 minute) |
| Access Token Implicit | 900s (15 minute) |
| SSO Session Idle | 7200s (2 ore) |
| SSO Session Max | 86400s (24 ore) |
| Algoritm semnatura | RS256 |
---
## Securitate
Aplicata pe **ambele realm-uri** (`didi-clients` + `didi-admins`).
### Brute force protection
- Activat (`bruteForceProtected: true`)
- Max incercari esuate: 5 (`failureFactor`)
- Timp asteptare: 60s (increment) / min quick-login wait 60s / quick-login check 1000ms
- Max wait: 900s (15 minute)
- Fereastra glisanta: 43200s (12 ore)
- Lockout permanent: dezactivat
### MFA / TOTP (livrabil Lot 2)
- Politica OTP: `otpPolicyType=totp` (HmacSHA1, 6 cifre, perioada 30s) — pe ambele realm-uri.
- Required action `CONFIGURE_TOTP` **enabled** pe realm-ul `didi-admins` (operatorii sunt fortati sa configureze TOTP; userii noi de admin primesc `CONFIGURE_TOTP` in `requiredActions` la prima logare, alaturi de `UPDATE_PASSWORD`).
- Realm-ul `didi-clients` are politica TOTP configurata (MFA disponibil pentru enrolment).
### Password policy (ambele realm-uri)
```
length(10) and digits(1) and upperCase(1) and lowerCase(1) and notUsername and passwordHistory(3)
```
Minim 10 caractere, cel putin o cifra, o majuscula, o minuscula, parola != username, fara reutilizarea ultimelor 3 parole.
### Setari realm
- Inregistrare: dezactivata (registrationAllowed: false)
- Login cu email: da
- Email ca username: da
- Verificare email: dezactivata (verifyEmail: false)
- Editare username: nu
- Emailuri duplicate: nu
- Remember me: da
- Reset parola: da
---
## Teme
### didi-clients-theme (principala, dark purple)
- Background: #050510 (foarte inchis)
- Accent: #A855F7 -> #7C3AED -> #6D28D9 (gradient purple)
- Card: glassmorphism (backdrop blur, border semi-transparent)
- Logo: "didi" (48px, font Outfit)
- Subtitle: "Misinformation Detection Platform"
- Font: Outfit (display) + Inter (body)
- Butoane: gradient purple cu glow la hover
- Responsive: suporta mobile (100dvh)
### didi-ai-theme (alternativa)
- Background: alb
- Accent: #0052CC (albastru)
- Subtitle: "didi - AI Platform"
### didi-backend-theme (alternativa)
- Background: alb
- Accent: #0052CC (albastru)
- Subtitle: "didi - Backend"
---
## Template-uri email
### email-verification.ftl
- Limba: romana
- Titlu: "Verifica adresa de email"
- Stil: dark purple gradient header
- URL custom: https://didi365.eu/api/auth/verify-email?key=...
- Afiseaza timpul de expirare (convertit din secunde)
- Deep link mobil: didi://email-verified, com.didi365.app://email-verified
### executeActions.ftl
- Stil: dark purple gradient
- Suporta actiuni multiple
- Deep linking mobil
### info.ftl (routing dupa actiuni)
- Detecteaza client ID (didi-mobile-app vs didi-web-app)
- Mobile: deep link cu fallback dupa 1.5-3s
- Web: redirect la /email-verified dupa 2s
- Butoane: "Deschide in aplicatie" / "Continua in browser"
---
## Fluxul de autentificare
```
Utilizator deschide aplicatia
|
v
Redirect la Keycloak login (tema didi-clients-theme)
|
v
Utilizatorul introduce email + parola
|
v
Keycloak valideaza + emite JWT (access token 10 min, refresh token)
|
v
Redirect inapoi la aplicatie cu authorization code
|
v
Aplicatia schimba codul in token-uri (PKCE pentru admin-dashboard)
|
v
Requesturi API cu Authorization: Bearer {access_token}
|
v
Kong valideaza JWT-ul (plugin jwt, consumer didi-keycloak-users, RS256, match pe iss)
|
v
Backend-ul decodeaza JWT pentru user_id/email (fara re-validare)
|
v
La fiecare 30s, aplicatia face refresh token daca expira in < 70s
```
---
## Cum comunica cu restul platformei
| Cine | Ce face | Cum |
|------|---------|-----|
| admin-dashboard | Login/logout utilizator | OAuth2 Standard Flow + PKCE |
| didi-web-app (frontend) | Login/logout utilizator | OAuth2 Standard Flow |
| Kong | Valideaza JWT pe fiecare request (RS256, match pe iss) | plugin jwt + consumer didi-keycloak-users |
| didiFramework (auth.ts) | Auto-inregistrare utilizator, Keycloak Admin API | Direct Access + Admin credentials |
| didiFramework (admin.ts) | Lista utilizatori, update emailVerified | Keycloak Admin API |
| agent-v3 | Decodeaza JWT din header (sub, email) | Doar decodare, fara validare (Kong a validat deja) |
---
## Admin API folosit de automatizari
- Admin API base: `http://localhost:28080/auth/admin/realms/{didi-clients|didi-admins}/` (Keycloak local, sub `/auth`)
- Master token via `POST /auth/realms/master/protocol/openid-connect/token` cu `client_id=admin-cli, username=admin, password=admin123`
- Folosit de fluxul de auto-inregistrare didiFramework + scripturi viitoare de automatizare.
---
## Roluri JWT in token-urile clientilor
Token-ul JWT contine acum array-ul `realm_access.roles`, parsat de agent-v3 (`req.jwtRoles`) pentru verificarile de rol pe endpoint-urile de moderare. Token-ul se reimprospateaza automat la fiecare 30s (comportament existent).
---
## Baza de date
Keycloak foloseste PostgreSQL local, aceeasi instanta ca restul platformei:
- `KC_DB=postgres`
- `KC_DB_URL=jdbc:postgresql://didi-postgres:5432/DIDI` (schema `public`)
- User: `bos_interface`
- Schema proprie Keycloak (gestionata automat)
Datele stocate: realm config, utilizatori, sesiuni, events, client sessions.
---
## Audit si evenimente
- Evenimente utilizator: activate (jboss-logging)
- Evenimente admin: activate cu detalii
- Logare: in stdout Docker (accesibil prin docker logs)
## Recent Changes
- **MFA / TOTP (livrabil Lot 2)**: `otpPolicyType=totp` pe ambele realm-uri; required action `CONFIGURE_TOTP` enabled pe `didi-admins` (operatorii sunt fortati sa configureze TOTP la prima logare, alaturi de `UPDATE_PASSWORD`).
- **Password policy** pe ambele realm-uri: `length(10) and digits(1) and upperCase(1) and lowerCase(1) and notUsername and passwordHistory(3)`.
- **Realm `didi-admins` (operatori)**: 3 roluri `admin` / `moderator` / `senior_moderator`; clienti publici `admin-dashboard` + `ai-platform-dashboard`; useri de test `moderator.test@didi.local`, `senior.moderator.test@didi.local`.
- **Realm `didi-clients` (useri finali)**: capabilitati `viewer`, `analyst`, `api_user` + tier-uri `free_tier`, `paid_tier`, `enterprise_tier`; clienti `didi-web-app`, `admin-dashboard`, `orchestrator-api`, `kong-api-gateway`.
- **Deployment local**: container unic `didi-keycloak` (`quay.io/keycloak/keycloak:26.0`, `start-dev --import-realm`), port `28080` sub `/auth`, `KC_HOSTNAME_STRICT=false`, `KC_PROXY_HEADERS=xforwarded`, DB `didi-postgres:5432/DIDI`. Fara cluster SSO / Swarm / Infinispan.

View file

@ -0,0 +1,203 @@
# Keycloak — migrat pe SSO cluster (2026-04-30)
> **TL;DR**: containerul local `keycloak` (Keycloak 22) nu mai rulează. DIDI folosește acum **SSO cluster** la `https://<sso-extern>` (Keycloak 26 HA, 3 replicas pe Dev Docker Swarm). Realm `didi-clients` migrat cu toate datele (users, clients, groups). Theme custom `didi-clients-theme` deployed pe SSO via bind mount pe nodurile Swarm.
---
## Ce era aici (înainte de 2026-04-30)
Container `keycloak` (Keycloak 22, image `quay.io/keycloak/keycloak:22.0`) definit în `backend/production/docker-compose.yml`. Single-instance pe didi12 (10.11.10.12:28000). DB pe Patroni cluster (`keycloak_db`). Hostname fix `KC_HOSTNAME_URL=https://didi365.eu/auth`.
Folosit doar de DIDI. Theme custom `didi-clients-theme` (purple gradient).
## De ce migrare
1. **Single-tenant lock-in**: Keycloak local servea doar didi365.eu. Pentru alte produse (lege365, rafai, etc.) ar fi trebuit instanțe separate sau hostname dinamic complex.
2. **Single-point-of-failure**: 1 container, 1 host. Down când didi12 down.
3. **DB password issue**: 2026-04-29 cineva a rotat parola `keycloak` user în Patroni → connection pool fail → service degraded.
4. **SSO cluster live**: 2026-04-29 Lucian a deploy-uit Keycloak 26 HA pe Dev Swarm, cu hostname public `<sso-extern>`.
## Ce e acum
### SSO Cluster
| Componentă | Detaliu |
|---|---|
| Hostname public | `<sso-extern>` (DNS public, cert Let's Encrypt valid) |
| Hostname intern | `<sso-extern-admin>` (admin URL via `KC_HOSTNAME_ADMIN`) |
| IP public | `82.79.147.181` (port-forward la Traefik intern) |
| Edge router | Traefik central (`10.11.10.171:443`) |
| Keycloak version | 26.0 (`quay.io/keycloak/keycloak:26.0`) |
| HA | 3 replicas, max 1 per node |
| Cluster | Dev Docker Swarm (`10.11.50.151-154`) |
| DB | Patroni cluster (`10.11.50.166:5000/keycloak_db`) |
| Cache | ispn (Infinispan, dns.query=tasks.keycloak) |
| Stack name | `keycloak-cluster` (`docker service ls`) |
### Hostname configuration
```yaml
KC_HOSTNAME: https://<sso-extern> # public URL (used in tokens, redirects)
KC_HOSTNAME_ADMIN: https://<sso-extern-admin> # admin endpoints (internal-only via 307 redirect)
KC_HOSTNAME_STRICT_BACKCHANNEL: false
KC_PROXY_HEADERS: xforwarded
```
Issuer in tokens: `https://<sso-extern>/realms/didi-clients`. Endpoints (no `/auth/` prefix in K26):
- `/realms/didi-clients/.well-known/openid-configuration`
- `/realms/didi-clients/protocol/openid-connect/auth`
- `/realms/didi-clients/protocol/openid-connect/token`
- `/realms/didi-clients/protocol/openid-connect/certs` (JWKS)
## Realm-uri pe SSO
- `master` — admin Keycloak (NU folosi pentru apps)
- **`didi-clients`** — DIDI customer-facing app (migrat 1:1 din local)
- `didi-admins` — DIDI admin panel (creat de Lucian, neutilizat încă)
## Theme deployment (didi-clients-theme)
Themes sunt mounted ca **bind mount** pe fiecare nod Swarm:
```yaml
mount:
type: bind
source: /var/keycloak-themes/didi-clients-theme
target: /opt/keycloak/themes/didi-clients-theme
readonly: true
```
Adăugat via `docker service update --mount-add` (nu via stack file). Pentru ca toate 3 replicas să găsească tema, fișierele trebuie pe **toate 4 nodurile** Swarm (10.11.50.151-154).
### Procedură deploy theme update
1. Pack theme local pe didi12:
```bash
cd backend/services/gateway-auth-layer/didiKeycloak/themes
tar -czf /home/admin365/didi-clients-theme.tar.gz didi-clients-theme/
```
2. Pe `dev-docker-mgr` (Swarm manager — 10.11.50.151):
```bash
scp admin365@10.11.10.12:/home/admin365/didi-clients-theme.tar.gz /tmp/
sudo tar -xzf /tmp/didi-clients-theme.tar.gz -C /var/keycloak-themes/
for n in 152 153 154; do
scp /tmp/didi-clients-theme.tar.gz admin365@10.11.50.$n:/tmp/
ssh -t admin365@10.11.50.$n 'sudo tar -xzf /tmp/didi-clients-theme.tar.gz -C /var/keycloak-themes/'
done
sudo docker service update --force keycloak-cluster_keycloak
```
3. Verify:
```bash
curl -ksm 5 https://<sso-extern>/resources/<version>/login/didi-clients-theme/css/login.css | head
```
### Theme structure (PatternFly v4 specific)
Keycloak 26 default theme (`keycloak`) folosește PatternFly v4 markup. Custom theme cu `parent=keycloak` moștenește template-urile, dar PF4 are reguli CSS specifice care necesită overrides în login.css:
- **Password input wrap** — în `<div class="pf-c-input-group">` cu eye-icon button. Necesită CSS specific pentru `.pf-c-input-group .pf-c-form-control`
- **Pseudo-element `::after` pe button** — PF4 button-uri au `<button>::after { border: ...; position: absolute }` care creează "chenarul". Trebuie killed cu `display:none !important` și `content:none !important`
Vezi `themes/didi-clients-theme/login/resources/css/login.css` secțiunile `PatternFly v4 input-group fix` și `Eye-icon button` pentru detalii.
## Cluster Kong JWT consumer
Consumer `didi-keycloak-users` în cluster Kong (`10.11.10.176:8001`) are **4 issuers acceptate** pentru tranziție smooth:
```
https://didi365.eu/auth/realms/didi-clients # legacy local Keycloak
https://<host-local>/auth/realms/didi-clients # legacy intern alias
https://<sso-extern-admin>/realms/didi-clients # SSO intern (transition)
https://<sso-extern>/realms/didi-clients # SSO public canonical (CURRENT)
```
Toate 4 au **același RSA public key** (Lucian a exportat realm-ul cu key preserved la migrare). Tokens emise acum de SSO au `iss=https://<sso-extern>/realms/didi-clients` — cluster Kong validează corect.
Vezi `didiKong/declarative/kong-cluster.yml` pentru config consumer.
## SPA configuration (didi-frontend)
`web/src/services/keycloak.service.ts`:
```typescript
const keycloakUrl =
(import.meta.env.VITE_KEYCLOAK_URL as string | undefined) ||
'https://<sso-extern>';
const keycloak = new Keycloak({
url: keycloakUrl,
realm: 'didi-clients',
clientId: 'didi-web-app',
});
```
SPA construiește toate URL-urile (auth, token, logout) relativ la `https://<sso-extern>`. Browser-ul user-ului se redirectează direct la SSO (NU prin proxy local). Cookie-urile Keycloak sunt setate pentru `<sso-extern>` domain.
## Ce a rămas local
În folder-ul ăsta (`didiKeycloak/`):
- `themes/` — sursa originală a temelor (didi-clients-theme, didi-ai-theme, didi-backend-theme). Folosită ca master pentru deployment pe SSO cluster.
- `realm-import/didi-clients-realm.json` — backup realm config (legacy, nu mai e mounted)
- `Dockerfile` — pentru imagine custom Keycloak 22 (legacy, nemai folosit)
## Volume cleanup
Volume-ul `didi-production-keycloak-data` (DB H2 local + cache) e păstrat **1 săptămână** pentru rollback safety.
```bash
# Remove după 2026-05-07:
docker volume rm didi-production-keycloak-data
```
## Rollback (în caz de probleme)
Containerul local Keycloak nu mai există. Pentru rollback:
1. **Reset DB password** (era broken pentru user `keycloak`):
```sql
ALTER USER keycloak WITH PASSWORD 'keycloak123';
```
2. **Restore docker-compose**:
```bash
git revert <commit-care-sterge-keycloak-din-compose>
cd backend/production && docker compose up -d keycloak
```
3. **Update SPA** să re-folosească local:
- În `frontend/web/src/services/keycloak.service.ts`, schimbă URL la `${origin}/auth`
- Rebuild + redeploy didi-frontend
4. **Restore /auth proxy** în `frontend/nginx-default.conf` (la cluster Kong sau local Keycloak)
## Linkuri rapide
- SSO public: <https://<sso-extern>/admin/master/console/> (admin: admin365 / parolă din credentials)
- SSO intern: `https://<sso-extern-admin>/admin/` (via /etc/hosts → 10.11.10.171, doar intern)
- Cluster setup repo: `landingzone/keycloak-sso/` (`git.finesynergy.eu/lucian/landingzone`)
- Stack file: pe `dev-docker-mgr` (Lucian) sau în repo
## Status
- ✅ Migrare aplicată: 2026-04-30
- ✅ Container local oprit + șters
- ✅ Service definition removed din docker-compose
- ✅ Theme deployed pe SSO 3 replicas
- ✅ End-to-end auth flow verificat (login, dashboard, JWT validare prin cluster Kong)
- ⏳ Volume `didi-production-keycloak-data` păstrat până 2026-05-07
## Lecții importante
1. **`KC_HOSTNAME` schimbă tot răspunsul Keycloak** — toate URL-urile generate, issuer-ul în tokens, cookie domain. Pentru aplicații web publice trebuie hostname public DNS-resolvable (NU `.local`).
2. **Keycloak 26 a renunțat la `/auth` prefix** — endpoints sunt `/realms/...`. Theme cu `parent=keycloak` moștenește templates PF4. Custom CSS trebuie să acopere PF4 markup specific (input-group, button::after pseudo-elements).
3. **`KC_HOSTNAME_ADMIN`** — separă admin de URL-ul public (security best practice). Admin via `<sso-extern-admin>` (intern), client-facing via `<sso-extern>`.
4. **Bind mount pe Swarm** cere theme files pe **toate** nodurile (constraint `max 1 per node` cu 3 replicas → cel puțin 3 din 4 noduri rulează task). Fișiere pe 4 noduri = sigur.
5. **Realm export/import** păstrează RSA signing keys — JWT-urile vechi rămân valide după migrare. Cluster Kong consumer poate avea multiple issuers cu **același** public_key.
6. **PatternFly v4 button `::after`** — chenarul "fantomă" pe button-uri vine din pseudo-element absolut poziționat. Trebuie `content: none !important` ca să-l killezi.

View file

@ -0,0 +1,446 @@
{
"realm": "didi-clients",
"enabled": true,
"displayName": "DIDI - Misinformation Detection Platform",
"displayNameHtml": "<strong>DIDI</strong> - Misinformation Detection Platform",
"registrationAllowed": false,
"registrationEmailAsUsername": true,
"rememberMe": true,
"verifyEmail": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": true,
"editUsernameAllowed": false,
"bruteForceProtected": true,
"permanentLockout": false,
"maxFailureWaitSeconds": 900,
"minimumQuickLoginWaitSeconds": 60,
"waitIncrementSeconds": 60,
"quickLoginCheckMilliSeconds": 1000,
"maxDeltaTimeSeconds": 43200,
"failureFactor": 5,
"defaultSignatureAlgorithm": "RS256",
"ssoSessionIdleTimeout": 7200,
"ssoSessionMaxLifespan": 86400,
"accessTokenLifespan": 600,
"accessTokenLifespanForImplicitFlow": 900,
"roles": {
"realm": [
{
"name": "admin",
"description": "Administrator with full access",
"composite": false
},
{
"name": "analyst",
"description": "Can create and manage analyses",
"composite": false
},
{
"name": "viewer",
"description": "Can view analysis results",
"composite": false
},
{
"name": "api_user",
"description": "Can access API endpoints",
"composite": false
},
{
"name": "free_tier",
"description": "Free tier privileges",
"composite": false
},
{
"name": "paid_tier",
"description": "Paid tier privileges",
"composite": false
},
{
"name": "enterprise_tier",
"description": "Enterprise tier privileges",
"composite": false
}
]
},
"groups": [
{
"name": "free-users",
"path": "/free-users",
"attributes": {
"tier": [
"free"
],
"daily_limit": [
"10"
],
"rate_limit": [
"10"
]
},
"realmRoles": [
"free_tier",
"viewer",
"api_user"
]
},
{
"name": "paid-users",
"path": "/paid-users",
"attributes": {
"tier": [
"paid"
],
"daily_limit": [
"100"
],
"rate_limit": [
"60"
]
},
"realmRoles": [
"paid_tier",
"viewer",
"analyst",
"api_user"
]
},
{
"name": "enterprise-users",
"path": "/enterprise-users",
"attributes": {
"tier": [
"enterprise"
],
"daily_limit": [
"unlimited"
],
"rate_limit": [
"600"
]
},
"realmRoles": [
"enterprise_tier",
"viewer",
"analyst",
"api_user"
]
},
{
"name": "administrators",
"path": "/administrators",
"attributes": {
"tier": [
"admin"
],
"daily_limit": [
"unlimited"
],
"rate_limit": [
"unlimited"
]
},
"realmRoles": [
"admin",
"analyst",
"viewer",
"api_user",
"enterprise_tier"
]
}
],
"defaultRoles": [
"viewer",
"free_tier"
],
"requiredCredentials": [
"password"
],
"clients": [
{
"clientId": "didi-web-app",
"name": "DIDI Web Application",
"description": "Main web frontend for end users",
"enabled": true,
"publicClient": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": true,
"redirectUris": [
"http://localhost:3001/*",
"http://localhost:5173/*",
"http://localhost:13001/*",
"http://localhost:33001/*",
"http://127.0.0.1:3001/*",
"http://127.0.0.1:5173/*",
"http://127.0.0.1:13001/*",
"http://127.0.0.1:33001/*"
],
"webOrigins": [
"http://localhost:3001",
"http://localhost:5173",
"http://localhost:13001",
"http://localhost:33001",
"http://127.0.0.1:3001",
"http://127.0.0.1:5173",
"http://127.0.0.1:13001",
"http://127.0.0.1:33001",
"+"
],
"defaultClientScopes": [
"web-origins",
"acr",
"profile",
"roles",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
},
{
"clientId": "admin-dashboard",
"name": "Admin Dashboard",
"description": "React admin dashboard application",
"rootUrl": "http://localhost:13003",
"adminUrl": "http://localhost:13003",
"baseUrl": "/",
"enabled": true,
"publicClient": true,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"attributes": {
"pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "http://localhost:13003/* http://localhost:3003/* http://localhost:33003/* http://10.11.50.11:33003/*"
},
"redirectUris": [
"http://localhost:13003/*",
"http://localhost:3003/*",
"http://localhost:33003/*",
"http://localhost:33001/*",
"http://localhost:3001/*",
"http://127.0.0.1:13003/*",
"http://127.0.0.1:3003/*",
"http://127.0.0.1:33003/*",
"http://127.0.0.1:33001/*",
"http://10.11.50.11:33003/*",
"http://10.11.50.11:3003/*"
],
"webOrigins": [
"http://localhost:13003",
"http://localhost:3003",
"http://localhost:33003",
"http://localhost:33001",
"http://localhost:3001",
"http://127.0.0.1:13003",
"http://127.0.0.1:3003",
"http://127.0.0.1:33003",
"http://127.0.0.1:33001",
"http://10.11.50.11:33003",
"http://10.11.50.11:3003",
"+"
]
},
{
"clientId": "orchestrator-api",
"name": "Analysis Orchestrator API",
"description": "Backend orchestrator service",
"rootUrl": "http://localhost:18000",
"enabled": true,
"publicClient": false,
"serviceAccountsEnabled": true,
"standardFlowEnabled": false,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"secret": "nmmImrmPAcADuPh-ZTqLY7GDhCAjfXsolDOM6TxZbHg",
"redirectUris": [
"http://localhost:18000/*",
"http://localhost:8000/*"
],
"webOrigins": [
"http://localhost:18000",
"http://localhost:8000"
]
},
{
"clientId": "kong-api-gateway",
"name": "Kong API Gateway",
"description": "API Gateway for JWT validation",
"rootUrl": "http://localhost:18100",
"enabled": true,
"publicClient": false,
"bearerOnly": true,
"standardFlowEnabled": false,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": true,
"secret": "Fu1rJ8QsjCj4j4_qZiMXyx6Ewo3xC2ik7X5m_MvSLOE"
}
],
"users": [
{
"username": "admin@didi.local",
"email": "admin@didi.local",
"firstName": "Admin",
"lastName": "User",
"enabled": true,
"emailVerified": true,
"credentials": [
{
"type": "password",
"value": "Admin12345",
"temporary": false
}
],
"realmRoles": [
"admin",
"analyst",
"viewer",
"api_user",
"enterprise_tier"
],
"groups": [
"/administrators"
],
"attributes": {
"tier": [
"admin"
]
}
},
{
"username": "demo@didi.local",
"email": "demo@didi.local",
"firstName": "Demo",
"lastName": "User",
"enabled": true,
"emailVerified": true,
"credentials": [
{
"type": "password",
"value": "Demo12345!",
"temporary": false
}
],
"realmRoles": [
"viewer",
"api_user",
"free_tier"
],
"groups": [
"/free-users"
],
"attributes": {
"tier": [
"free"
]
}
},
{
"username": "free@didi.local",
"email": "free@didi.local",
"firstName": "Free",
"lastName": "User",
"enabled": true,
"emailVerified": true,
"credentials": [
{
"type": "password",
"value": "Password123",
"temporary": false
}
],
"realmRoles": [
"free_tier",
"viewer",
"api_user"
],
"groups": [
"/free-users"
],
"attributes": {
"tier": [
"free"
]
}
},
{
"username": "paid@didi.local",
"email": "paid@didi.local",
"firstName": "Paid",
"lastName": "User",
"enabled": true,
"emailVerified": true,
"credentials": [
{
"type": "password",
"value": "Password123",
"temporary": false
}
],
"realmRoles": [
"paid_tier",
"viewer",
"analyst",
"api_user"
],
"groups": [
"/paid-users"
],
"attributes": {
"tier": [
"paid"
]
}
},
{
"username": "enterprise@didi.local",
"email": "enterprise@didi.local",
"firstName": "Enterprise",
"lastName": "User",
"enabled": true,
"emailVerified": true,
"credentials": [
{
"type": "password",
"value": "Password123",
"temporary": false
}
],
"realmRoles": [
"enterprise_tier",
"viewer",
"analyst",
"api_user"
],
"groups": [
"/enterprise-users"
],
"attributes": {
"tier": [
"enterprise"
]
}
}
],
"eventsEnabled": true,
"eventsListeners": [
"jboss-logging"
],
"adminEventsEnabled": true,
"adminEventsDetailsEnabled": true,
"internationalizationEnabled": true,
"supportedLocales": [
"en"
],
"defaultLocale": "en",
"passwordPolicy": "length(10) and digits(1) and upperCase(1) and lowerCase(1) and notUsername(undefined) and passwordHistory(3)",
"otpPolicyType": "totp",
"otpPolicyAlgorithm": "HmacSHA1",
"otpPolicyDigits": 6,
"otpPolicyPeriod": 30
}

View file

@ -0,0 +1,72 @@
# Keycloak Custom Theme Setup
## Automatic Setup
Run the provided script after Keycloak is running:
```bash
./docker/keycloak/set-theme.sh
```
## Manual Setup
1. Access Keycloak Admin Console:
- URL: http://localhost:8180
- Username: admin
- Password: keycloak_admin_password_123
2. Select your realm:
- Click on the realm dropdown (top left)
- Select "misinformation-analyzer"
3. Configure the theme:
- Go to "Realm Settings" in the left menu
- Click on the "Themes" tab
- In "Login theme" dropdown, select "misinformation-theme"
- Click "Save"
4. Test the theme:
- Logout from admin console
- Go to your application: http://localhost:3001
- You should see the styled login page
## Theme Customization
The theme files are located in:
- CSS: `misinformation-theme/login/resources/css/login.css`
- Logo: `misinformation-theme/login/resources/img/logo.png`
- Properties: `misinformation-theme/login/theme.properties`
### Customization Options:
1. **Colors**: Edit the CSS variables in login.css
2. **Logo**: Replace logo.png with your own
3. **Fonts**: Update font-family in the CSS
4. **Layout**: Modify the CSS selectors
### Available CSS Variables:
```css
--primary-color: #1976d2;
--primary-dark: #115293;
--primary-light: #4791db;
--secondary-color: #dc004e;
--gradient-trust: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--gradient-bloom: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
```
## Troubleshooting
If the theme doesn't appear:
1. Ensure the theme folder is mounted correctly in docker-compose.yml
2. Restart Keycloak: `docker compose restart keycloak`
3. Clear browser cache
4. Check Keycloak logs: `docker compose logs keycloak`
## Preview
The custom theme includes:
- Gradient background matching your app
- Custom logo
- Styled input fields with focus effects
- Matching button styles
- Consistent color scheme
- Responsive design

View file

@ -0,0 +1,148 @@
/* Custom theme for didi - AI Platform */
/* Remove the black striped background */
body,
.login-pf body,
.login-pf-page {
background: #FFFFFF !important;
background-image: none !important;
}
/* Hide elements we don't want */
#kc-header,
#kc-header-wrapper,
.alert-info {
display: none;
}
/* Center the entire login container */
.login-pf-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
/* Style the card container */
.card-pf {
background: #FFFFFF !important;
border-radius: 12px !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
padding: 48px !important;
max-width: 400px !important;
width: 100% !important;
margin: 20px !important;
}
/* Style the login box */
#kc-content {
text-align: center;
}
/* Form styling */
#kc-form {
text-align: left;
margin-top: 32px;
}
/* Input fields */
.pf-c-form-control,
input[type="text"],
input[type="password"] {
width: 100% !important;
padding: 12px 16px !important;
border: 1px solid #E5E7EB !important;
border-radius: 6px !important;
font-size: 16px !important;
margin-top: 8px !important;
}
.pf-c-form-control:focus,
input[type="text"]:focus,
input[type="password"]:focus {
border-color: #0052CC !important;
outline: none !important;
box-shadow: 0 0 0 3px rgba(0, 82, 204, 0.1) !important;
}
/* Labels */
label {
color: #374151 !important;
font-weight: 500 !important;
font-size: 14px !important;
display: block !important;
margin-bottom: 4px !important;
}
/* Form groups spacing */
.form-group {
margin-bottom: 20px !important;
}
/* Add logo before title */
#kc-page-title::before {
content: '';
display: block;
background-image: url('../img/logo.png');
background-repeat: no-repeat;
background-position: center;
background-size: contain;
width: 150px;
height: 150px;
margin: 0 auto 20px;
}
/* Add subtitle after title */
#kc-page-title::after {
content: 'didi - AI Platform';
display: block;
font-size: 16px;
font-weight: 400;
color: #6B7280;
margin-top: 10px;
}
/* Style the submit button */
#kc-login {
background: #0052CC !important;
border: none !important;
width: 100% !important;
padding: 12px 24px !important;
border-radius: 6px !important;
font-size: 16px !important;
font-weight: 500 !important;
margin-top: 24px !important;
transition: all 0.2s !important;
}
#kc-login:hover {
background: #003d99 !important;
transform: translateY(-1px) !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
}
/* Checkbox styling */
.checkbox {
margin: 16px 0 !important;
}
.checkbox label {
font-weight: 400 !important;
}
/* Links below form */
#kc-registration,
#kc-passwd-reset-wrapper {
margin-top: 24px !important;
}
#kc-registration a,
#kc-passwd-reset-wrapper a {
color: #0052CC !important;
text-decoration: none !important;
}
#kc-registration a:hover,
#kc-passwd-reset-wrapper a:hover {
text-decoration: underline !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,9 @@
# Inherit from keycloak to get templates
parent=keycloak
# Override styles
styles=css/login.css
# Messages
displayName=Misinformation Analysis Platform
displayNameHtml=<strong>Misinformation Analysis Platform</strong>

View file

@ -0,0 +1,148 @@
/* Custom theme for didi - Backend Platform */
/* Remove the black striped background */
body,
.login-pf body,
.login-pf-page {
background: #FFFFFF !important;
background-image: none !important;
}
/* Hide elements we don't want */
#kc-header,
#kc-header-wrapper,
.alert-info {
display: none;
}
/* Center the entire login container */
.login-pf-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
/* Style the card container */
.card-pf {
background: #FFFFFF !important;
border-radius: 12px !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
padding: 48px !important;
max-width: 400px !important;
width: 100% !important;
margin: 20px !important;
}
/* Style the login box */
#kc-content {
text-align: center;
}
/* Form styling */
#kc-form {
text-align: left;
margin-top: 32px;
}
/* Input fields */
.pf-c-form-control,
input[type="text"],
input[type="password"] {
width: 100% !important;
padding: 12px 16px !important;
border: 1px solid #E5E7EB !important;
border-radius: 6px !important;
font-size: 16px !important;
margin-top: 8px !important;
}
.pf-c-form-control:focus,
input[type="text"]:focus,
input[type="password"]:focus {
border-color: #0052CC !important;
outline: none !important;
box-shadow: 0 0 0 3px rgba(0, 82, 204, 0.1) !important;
}
/* Labels */
label {
color: #374151 !important;
font-weight: 500 !important;
font-size: 14px !important;
display: block !important;
margin-bottom: 4px !important;
}
/* Form groups spacing */
.form-group {
margin-bottom: 20px !important;
}
/* Add logo before title */
#kc-page-title::before {
content: '';
display: block;
background-image: url('../img/logo.png');
background-repeat: no-repeat;
background-position: center;
background-size: contain;
width: 150px;
height: 150px;
margin: 0 auto 20px;
}
/* Add subtitle after title */
#kc-page-title::after {
content: 'didi - Backend';
display: block;
font-size: 16px;
font-weight: 400;
color: #6B7280;
margin-top: 10px;
}
/* Style the submit button */
#kc-login {
background: #0052CC !important;
border: none !important;
width: 100% !important;
padding: 12px 24px !important;
border-radius: 6px !important;
font-size: 16px !important;
font-weight: 500 !important;
margin-top: 24px !important;
transition: all 0.2s !important;
}
#kc-login:hover {
background: #003d99 !important;
transform: translateY(-1px) !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
}
/* Checkbox styling */
.checkbox {
margin: 16px 0 !important;
}
.checkbox label {
font-weight: 400 !important;
}
/* Links below form */
#kc-registration,
#kc-passwd-reset-wrapper {
margin-top: 24px !important;
}
#kc-registration a,
#kc-passwd-reset-wrapper a {
color: #0052CC !important;
text-decoration: none !important;
}
#kc-registration a:hover,
#kc-passwd-reset-wrapper a:hover {
text-decoration: underline !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,9 @@
# Inherit from keycloak to get templates
parent=keycloak
# Override styles
styles=css/login.css
# Messages
displayName=Misinformation Analysis Platform
displayNameHtml=<strong>Misinformation Analysis Platform</strong>

View file

@ -0,0 +1,50 @@
<#assign customVerifyUrl = "https://didi365.eu/api/auth/verify-email?key=" + link?keep_after("key=")>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verifică adresa de email</title>
</head>
<body style="margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f4f4;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: #f4f4f4;">
<tr>
<td align="center" style="padding: 40px 20px;">
<table role="presentation" width="600" cellspacing="0" cellpadding="0" style="background-color: #ffffff; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
<tr>
<td style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); padding: 40px 40px 30px; border-radius: 12px 12px 0 0; text-align: center;">
<h1 style="color: #ffffff; margin: 0; font-size: 28px; font-weight: 600;">DIDI</h1>
<p style="color: #a0a0a0; margin: 8px 0 0; font-size: 14px;">Misinformation Detection Platform</p>
</td>
</tr>
<tr>
<td style="padding: 40px;">
<h2 style="color: #1a1a2e; margin: 0 0 20px; font-size: 22px; font-weight: 600;">Verifică adresa de email</h2>
<p style="color: #555555; font-size: 16px; line-height: 1.6; margin: 0 0 25px;">Salut <strong>${user.firstName!""}</strong>,</p>
<p style="color: #555555; font-size: 16px; line-height: 1.6; margin: 0 0 25px;">Mulțumim pentru înregistrare! Pentru a activa contul tău DIDI, te rugăm să confirmi adresa de email apăsând butonul de mai jos:</p>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
<tr>
<td align="center">
<a href="${customVerifyUrl}" target="_blank" style="display: inline-block; background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); color: #ffffff; text-decoration: none; padding: 16px 40px; border-radius: 8px; font-size: 16px; font-weight: 600; box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);">Verifică Email</a>
</td>
</tr>
</table>
<p style="color: #888888; font-size: 14px; line-height: 1.6; margin: 25px 0 0;">Sau copiază acest link în browser:</p>
<p style="color: #4CAF50; font-size: 13px; word-break: break-all; background-color: #f8f8f8; padding: 12px; border-radius: 6px; margin: 10px 0 25px;">${customVerifyUrl}</p>
<p style="color: #888888; font-size: 14px; line-height: 1.6; margin: 0;">Link-ul expiră în <strong>${linkExpiration}</strong>.</p>
<hr style="border: none; border-top: 1px solid #eeeeee; margin: 30px 0;">
<p style="color: #999999; font-size: 13px; line-height: 1.6; margin: 0;">Dacă nu ai creat un cont pe DIDI, poți ignora acest email.</p>
</td>
</tr>
<tr>
<td style="background-color: #f8f8f8; padding: 25px 40px; border-radius: 0 0 12px 12px; text-align: center;">
<p style="color: #999999; font-size: 12px; margin: 0 0 8px;">&copy; 2025 didi - Misinformation Detection Platform</p>
<p style="color: #bbbbbb; font-size: 11px; margin: 0;">Acest email a fost trimis automat. Te rugăm să nu răspunzi.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View file

@ -0,0 +1,50 @@
<#assign customVerifyUrl = "https://didi365.eu/api/auth/verify-email?key=" + link?keep_after("key=")>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verifică adresa de email</title>
</head>
<body style="margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #050510;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: #050510;">
<tr>
<td align="center" style="padding: 40px 20px;">
<table role="presentation" width="600" cellspacing="0" cellpadding="0" style="background-color: #0f0f1a; border-radius: 16px; box-shadow: 0 8px 32px rgba(124, 58, 237, 0.3); border: 1px solid rgba(124, 58, 237, 0.2);">
<tr>
<td style="background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%); padding: 40px 40px 30px; border-radius: 16px 16px 0 0; text-align: center;">
<h1 style="color: #ffffff; margin: 0; font-size: 36px; font-weight: 700; letter-spacing: 2px;">didi</h1>
<p style="color: rgba(255,255,255,0.7); margin: 8px 0 0; font-size: 14px; letter-spacing: 1px;">Misinformation Detection Platform</p>
</td>
</tr>
<tr>
<td style="padding: 40px;">
<h2 style="color: #E8E8E8; margin: 0 0 20px; font-size: 22px; font-weight: 600;">Verifică adresa de email</h2>
<p style="color: #D1D1D1; font-size: 16px; line-height: 1.6; margin: 0 0 25px;">Salut <strong style="color: #00d4ff;">${user.firstName!""}</strong>,</p>
<p style="color: #D1D1D1; font-size: 16px; line-height: 1.6; margin: 0 0 25px;">Pentru a-ți activa contul în platforma <strong style="color: #7c3aed;">didi</strong>, te rugăm să confirmi adresa de email apăsând butonul de mai jos:</p>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
<tr>
<td align="center">
<a href="${customVerifyUrl}" target="_blank" style="display: inline-block; background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%); color: #ffffff; text-decoration: none; padding: 16px 48px; border-radius: 12px; font-size: 16px; font-weight: 600; box-shadow: 0 4px 20px rgba(124, 58, 237, 0.4); letter-spacing: 0.5px;">Verifică Email</a>
</td>
</tr>
</table>
<p style="color: #9CA3AF; font-size: 14px; line-height: 1.6; margin: 25px 0 0;">Sau copiază acest link în browser:</p>
<p style="color: #00d4ff; font-size: 12px; word-break: break-all; background-color: rgba(124, 58, 237, 0.1); padding: 14px; border-radius: 8px; margin: 10px 0 25px; border: 1px solid rgba(124, 58, 237, 0.2);">${customVerifyUrl}</p>
<p style="color: #9CA3AF; font-size: 14px; line-height: 1.6; margin: 0;">Link-ul expiră în <strong style="color: #E8E8E8;"><#if linkExpiration?number gt 60>${(linkExpiration?number / 60)?round} minute<#else>${linkExpiration} secunde</#if></strong>.</p>
<hr style="border: none; border-top: 1px solid rgba(124, 58, 237, 0.2); margin: 30px 0;">
<p style="color: #6B7280; font-size: 13px; line-height: 1.6; margin: 0;">Dacă nu ai solicitat această acțiune, poți ignora acest email.</p>
</td>
</tr>
<tr>
<td style="background-color: rgba(124, 58, 237, 0.05); padding: 25px 40px; border-radius: 0 0 16px 16px; text-align: center; border-top: 1px solid rgba(124, 58, 237, 0.1);">
<p style="color: #9CA3AF; font-size: 12px; margin: 0 0 8px;">&copy; 2025 <span style="color: #7c3aed; font-weight: 600;">didi</span> - Misinformation Detection Platform</p>
<p style="color: #6B7280; font-size: 11px; margin: 0;">Acest email a fost trimis automat. Te rugăm să nu răspunzi.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View file

@ -0,0 +1,14 @@
Salut ${user.firstName!""},
Mulțumim pentru înregistrare pe DIDI!
Pentru a activa contul tău, te rugăm să accesezi link-ul de mai jos:
${link}
Link-ul expiră în ${linkExpiration}.
Dacă nu ai creat un cont pe DIDI, poți ignora acest email.
---
DIDI - Misinformation Detection Platform

View file

@ -0,0 +1,117 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=true; section>
<#if section = "header">
<#if messageHeader??>
${messageHeader}
<#else>
${message.summary}
</#if>
<#elseif section = "form">
<div id="kc-info-message">
<p class="instruction">${message.summary}<#if requiredActions??><#list requiredActions>: <b><#items as reqActionItem>${kcSanitize(msg("requiredAction.${reqActionItem}"))?no_esc}<#sep>, </#items></b></#list><#else></#if></p>
<#if skipLink??>
<#else>
<#-- Mobile app deep link redirect -->
<#if client?? && client.clientId?? && client.clientId == "didi-mobile-app">
<#assign mobileRedirectUrl = "didi://email-verified">
<p style="text-align: center; margin-top: 20px;">
<a href="${mobileRedirectUrl}" style="display: inline-block; background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); color: #ffffff; text-decoration: none; padding: 14px 32px; border-radius: 8px; font-size: 16px; font-weight: 600;">
Deschide aplicația DIDI
</a>
</p>
<p style="color: #666; font-size: 14px; text-align: center; margin-top: 15px;">
Vei fi redirecționat automat către aplicație...
</p>
<script>
// Try to redirect to mobile app after 1.5 seconds
setTimeout(function() {
window.location.href = "${mobileRedirectUrl}";
}, 1500);
// Fallback: if deep link fails, try alternative scheme
setTimeout(function() {
window.location.href = "com.didi365.app://email-verified";
}, 3000);
</script>
<#-- Web app redirect -->
<#elseif client?? && client.clientId?? && client.clientId == "didi-web-app">
<#assign webRedirectUrl = "/email-verified">
<p style="text-align: center; margin-top: 20px;">
<a href="${webRedirectUrl}" style="display: inline-block; background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); color: #ffffff; text-decoration: none; padding: 14px 32px; border-radius: 8px; font-size: 16px; font-weight: 600;">
Continuă către aplicație
</a>
</p>
<p style="color: #666; font-size: 14px; text-align: center; margin-top: 15px;">
Vei fi redirecționat automat...
</p>
<script>
setTimeout(function() {
window.location.href = "${webRedirectUrl}";
}, 2000);
</script>
<#-- Standard Keycloak redirects -->
<#elseif pageRedirectUri?has_content>
<p><a href="${pageRedirectUri}">${kcSanitize(msg("backToApplication"))?no_esc}</a></p>
<script>
setTimeout(function() {
window.location.href = "${pageRedirectUri}";
}, 2000);
</script>
<#elseif actionUri?has_content>
<p><a href="${actionUri}">${kcSanitize(msg("proceedWithAction"))?no_esc}</a></p>
<script>
setTimeout(function() {
window.location.href = "${actionUri}";
}, 2000);
</script>
<#elseif (client.baseUrl)?has_content>
<p><a href="${client.baseUrl}">${kcSanitize(msg("backToApplication"))?no_esc}</a></p>
<script>
setTimeout(function() {
window.location.href = "${client.baseUrl}";
}, 2000);
</script>
<#-- Fallback: detect mobile browser and redirect accordingly -->
<#else>
<div id="redirect-buttons" style="text-align: center; margin-top: 20px;">
<p style="color: #666; font-size: 14px; margin-bottom: 15px;">
Alege cum dorești să continui:
</p>
<p>
<a href="didi://email-verified" id="mobile-link" style="display: inline-block; background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); color: #ffffff; text-decoration: none; padding: 14px 32px; border-radius: 8px; font-size: 16px; font-weight: 600; margin: 5px;">
📱 Deschide în aplicație
</a>
</p>
<p>
<a href="/email-verified" id="web-link" style="display: inline-block; background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%); color: #ffffff; text-decoration: none; padding: 14px 32px; border-radius: 8px; font-size: 16px; font-weight: 600; margin: 5px;">
🌐 Continuă în browser
</a>
</p>
</div>
<script>
(function() {
var ua = navigator.userAgent || navigator.vendor || window.opera;
var isMobile = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(ua.toLowerCase());
if (isMobile) {
// On mobile, try deep link first
setTimeout(function() {
window.location.href = "didi://email-verified";
}, 1500);
// Fallback to alternative scheme
setTimeout(function() {
window.location.href = "com.didi365.app://email-verified";
}, 3000);
} else {
// On desktop, redirect to web app
setTimeout(function() {
window.location.href = "/email-verified";
}, 2000);
}
})();
</script>
</#if>
</#if>
</div>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,42 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayInfo=true displayMessage=!messagesPerField.existsError('username'); section>
<#if section = "header">
${msg("emailForgotTitle")}
<#elseif section = "form">
<form id="kc-reset-password-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="back-link">
<a href="${url.loginUrl}">${msg("backToLogin")}</a>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}">
<#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if>
</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<#if auth?has_content && auth.showUsername()>
<input type="text" id="username" name="username" class="${properties.kcInputClass!}" autofocus value="${auth.attemptedUsername}" aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"/>
<#else>
<input type="text" id="username" name="username" class="${properties.kcInputClass!}" autofocus aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"/>
</#if>
<#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('username'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}"/>
</div>
</div>
</form>
<#elseif section = "info" >
<#if realm.duplicateEmailsAllowed>
${msg("emailInstructionUsername")}
<#else>
${msg("emailInstruction")}
</#if>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,25 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayInfo=true displayMessage=false; section>
<#if section = "header">
${msg("emailVerifyTitle")}
<#elseif section = "form">
<div class="verify-email-box">
<p class="instruction">
${msg("emailVerifyInstruction1")}
</p>
<p class="instruction auto-check">
<span id="check-status">${msg("emailVerifyAutoCheck")}</span>
<span class="loader"></span>
</p>
</div>
<script>
setTimeout(function() { location.reload(); }, 5000);
</script>
<#elseif section = "info">
<p class="instruction">
${msg("emailVerifyInstruction3")}
<br/><br/>
<a href="${url.loginAction}">${msg("doClickHere")}</a> ${msg("emailVerifyInstruction2")}
</p>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,33 @@
# DIDI Custom Messages
# Forgot Password
emailForgotTitle=Reset your password
emailInstruction=Enter your email address and we will send you instructions to reset your password.
# Labels - hide them, use placeholders instead
usernameOrEmail=Email
username=Email
email=Email
password=Password
passwordConfirm=Confirm Password
firstName=First Name
lastName=Last Name
# Login
loginTitle=Sign in
doLogIn=Sign in
loginAccountTitle=Sign in to your account
# Register
registerTitle=Create account
doRegister=Create account
# Back link - keep it simple
backToLogin=\u2190 Back to sign in
# Email Verification
emailVerifyTitle=Verify your email
emailVerifyInstruction1=You need to verify your email address to activate your account.
emailVerifyInstruction2=to re-send the email.
emailVerifyInstruction3=Haven't received a verification code in your email?
emailVerifyAutoCheck=Checking verification status...

View file

@ -0,0 +1,27 @@
<#macro termsAcceptance>
<#if termsAcceptanceRequired??>
<div class="form-group">
<div class="${properties.kcInputWrapperClass!}">
${msg("termsTitle")}
<div id="kc-registration-terms-text">
${kcSanitize(msg("termsText"))?no_esc}
</div>
</div>
</div>
<div class="form-group">
<div class="${properties.kcLabelWrapperClass!}">
<input type="checkbox" id="termsAccepted" name="termsAccepted" class="${properties.kcCheckboxInputClass!}"
aria-invalid="<#if messagesPerField.existsError('termsAccepted')>true</#if>"
/>
<label for="termsAccepted" class="${properties.kcLabelClass!}">${msg("acceptTerms")}</label>
</div>
<#if messagesPerField.existsError('termsAccepted')>
<div class="${properties.kcLabelWrapperClass!}">
<span id="input-error-terms-accepted" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('termsAccepted'))?no_esc}
</span>
</div>
</#if>
</div>
</#if>
</#macro>

View file

@ -0,0 +1,135 @@
<#import "template.ftl" as layout>
<#import "register-commons.ftl" as registerCommons>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm','termsAccepted'); section>
<#if section = "header">
${msg("registerTitle")}
<#elseif section = "form">
<form id="kc-register-form" class="${properties.kcFormClass!}" action="${url.registrationAction}" method="post">
<div class="back-link">
<a href="${url.loginUrl}">${kcSanitize(msg("backToLogin"))?no_esc}</a>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="firstName" class="${properties.kcLabelClass!}">${msg("firstName")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="firstName" class="${properties.kcInputClass!}" name="firstName"
value="${(register.formData.firstName!'')}"
aria-invalid="<#if messagesPerField.existsError('firstName')>true</#if>"
/>
<#if messagesPerField.existsError('firstName')>
<span id="input-error-firstname" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('firstName'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="lastName" class="${properties.kcLabelClass!}">${msg("lastName")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="lastName" class="${properties.kcInputClass!}" name="lastName"
value="${(register.formData.lastName!'')}"
aria-invalid="<#if messagesPerField.existsError('lastName')>true</#if>"
/>
<#if messagesPerField.existsError('lastName')>
<span id="input-error-lastname" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('lastName'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="email" class="${properties.kcInputClass!}" name="email"
value="${(register.formData.email!'')}" autocomplete="email"
aria-invalid="<#if messagesPerField.existsError('email')>true</#if>"
/>
<#if messagesPerField.existsError('email')>
<span id="input-error-email" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('email'))?no_esc}
</span>
</#if>
</div>
</div>
<#if !realm.registrationEmailAsUsername>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}">${msg("username")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="username" class="${properties.kcInputClass!}" name="username"
value="${(register.formData.username!'')}" autocomplete="username"
aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"
/>
<#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('username'))?no_esc}
</span>
</#if>
</div>
</div>
</#if>
<#if passwordRequired??>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="password" id="password" class="${properties.kcInputClass!}" name="password"
autocomplete="new-password"
aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>"
/>
<#if messagesPerField.existsError('password')>
<span id="input-error-password" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('password'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="password-confirm" class="${properties.kcLabelClass!}">${msg("passwordConfirm")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="password" id="password-confirm" class="${properties.kcInputClass!}"
name="password-confirm"
aria-invalid="<#if messagesPerField.existsError('password-confirm')>true</#if>"
/>
<#if messagesPerField.existsError('password-confirm')>
<span id="input-error-password-confirm" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}
</span>
</#if>
</div>
</div>
</#if>
<@registerCommons.termsAcceptance/>
<#if recaptchaRequired??>
<div class="form-group">
<div class="${properties.kcInputWrapperClass!}">
<div class="g-recaptcha" data-size="compact" data-sitekey="${recaptchaSiteKey}"></div>
</div>
</div>
</#if>
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doRegister")}"/>
</div>
</div>
</form>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,896 @@
/* DIDI Login Theme - Dark Purple */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Outfit:wght@400;500;600;700;800&display=swap');
/* ========== ROOT VARIABLES ========== */
:root {
/* Background */
--bg-primary: #050510;
--bg-secondary: #0a0a1a;
--surface: rgba(255, 255, 255, 0.03);
--surface-hover: rgba(255, 255, 255, 0.06);
/* Borders */
--border: rgba(255, 255, 255, 0.08);
--border-hover: rgba(255, 255, 255, 0.15);
--border-focus: rgba(139, 92, 246, 0.5);
/* Text */
--text-primary: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.6);
--text-muted: rgba(255, 255, 255, 0.4);
/* Purple accent */
--purple-light: #A855F7;
--purple-main: #8B5CF6;
--purple-dark: #7C3AED;
--purple-deeper: #6D28D9;
--gradient-cta: linear-gradient(135deg, #A855F7 0%, #7C3AED 50%, #6D28D9 100%);
--glow: linear-gradient(135deg, #8B5CF6, #7C3AED);
/* Semantic */
--warning: #f59e0b;
--success: #10b981;
--error: #E63946;
/* Typography */
--font-display: 'Outfit', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
/* Spacing */
--radius-card: 20px;
--radius-normal: 16px;
--radius-button: 12px;
--radius-tag: 8px;
}
/* ========== GLOBAL RESET ========== */
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
min-height: 100vh;
}
body {
font-family: var(--font-body);
font-weight: 400;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
}
/* ========== LOGIN PAGE LAYOUT ========== */
html.login-pf,
.login-pf body {
background: var(--bg-primary) !important;
min-height: 100vh !important;
min-height: 100dvh !important; /* Dynamic viewport height for mobile */
margin: 0 !important;
padding: 0 !important;
}
#kc-header {
display: none;
}
#kc-header-wrapper {
display: none;
}
/* Force perfect centering */
.login-pf .container-fluid,
.login-pf .container {
background: var(--bg-primary) !important;
min-height: 100vh !important;
min-height: 100dvh !important;
width: 100% !important;
max-width: 100% !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
padding: 20px !important;
margin: 0 !important;
position: relative !important;
}
/* Override any row/column that might break centering */
.login-pf .row,
.login-pf .col-sm-12,
.login-pf .col-md-12,
.login-pf [class*="col-"] {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
float: none !important;
}
/* ========== LOGIN CARD ========== */
#kc-form-wrapper,
#kc-content,
#kc-content-wrapper {
background: transparent !important;
border: none !important;
box-shadow: none !important;
width: 100% !important;
}
.card-pf {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
max-width: 420px;
width: 100%;
margin: 0 auto !important;
padding: 20px;
flex-shrink: 0;
/* Prevent overflow on small screens */
overflow: hidden;
}
#kc-form,
#kc-register-form,
#kc-reset-password-form {
background: var(--surface);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--border);
border-radius: var(--radius-card);
padding: 48px 40px;
max-width: 420px;
width: 100%;
margin: 0 auto;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* ========== LOGO / HEADER - Inside card ========== */
.card-pf::before {
content: 'didi';
display: block;
text-align: center;
font-family: var(--font-display);
font-size: 48px;
font-weight: 800;
letter-spacing: -0.04em;
color: #ffffff;
margin-bottom: 4px;
order: -2;
}
.card-pf::after {
content: 'Misinformation Detection Platform';
display: block;
text-align: center;
font-family: var(--font-body);
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 24px;
letter-spacing: 0.5px;
order: -1;
}
/* Hide header outside modal */
#kc-form-wrapper::before,
#kc-form-wrapper::after,
.login-pf-header,
#kc-header,
#kc-header-wrapper {
display: none !important;
}
/* ========== FORM TITLES ========== */
#kc-page-title,
.kc-page-title,
h1#kc-page-title {
font-family: var(--font-display);
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
text-align: center;
margin: 0 0 24px 0;
letter-spacing: -0.02em;
line-height: 1.2;
}
/* ========== FORM GROUPS ========== */
.form-group {
margin-bottom: 20px;
}
/* ========== LABELS ========== */
label,
.control-label,
#kc-form-options label {
display: block;
font-family: var(--font-body);
font-size: 14px;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 8px;
}
/* ========== INPUT FIELDS ========== */
input[type="text"],
input[type="password"],
input[type="email"],
input[type="tel"],
.form-control {
width: 100%;
padding: 14px 16px;
font-family: var(--font-body);
font-size: 15px;
color: var(--text-primary);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-button);
outline: none;
transition: all 0.2s ease;
}
input[type="text"]:hover,
input[type="password"]:hover,
input[type="email"]:hover,
input[type="tel"]:hover,
.form-control:hover {
border-color: var(--border-hover);
background: var(--surface-hover);
}
input[type="text"]:focus,
input[type="password"]:focus,
input[type="email"]:focus,
input[type="tel"]:focus,
.form-control:focus {
border-color: var(--border-focus);
background: var(--surface);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
}
input::placeholder,
.form-control::placeholder {
color: var(--text-muted);
}
/* ========== PRIMARY BUTTON ========== */
input[type="submit"],
button[type="submit"],
.btn-primary,
#kc-login,
#kc-form-buttons input[type="submit"] {
width: 100%;
padding: 16px 32px;
font-family: var(--font-body);
font-size: 16px;
font-weight: 600;
color: #ffffff;
background: var(--gradient-cta);
border: none;
border-radius: var(--radius-button);
cursor: pointer;
transition: all 0.2s ease;
margin-top: 8px;
}
input[type="submit"]:hover,
button[type="submit"]:hover,
.btn-primary:hover,
#kc-login:hover {
transform: scale(1.02);
box-shadow: 0 0 30px rgba(139, 92, 246, 0.4);
}
input[type="submit"]:active,
button[type="submit"]:active,
.btn-primary:active {
transform: scale(0.98);
}
/* Spacing between Submit and Cancel buttons */
#kc-form-buttons {
display: flex;
flex-direction: column;
gap: 12px;
}
/* ========== SECONDARY / LINK BUTTONS ========== */
.btn-default,
a.btn {
display: inline-block;
padding: 12px 24px;
font-family: var(--font-body);
font-size: 14px;
font-weight: 500;
color: var(--text-secondary);
background: transparent;
border: 1px solid var(--border-hover);
border-radius: var(--radius-button);
text-decoration: none;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-default:hover,
a.btn:hover {
color: var(--text-primary);
border-color: rgba(255, 255, 255, 0.3);
background: var(--surface-hover);
}
/* ========== LINKS ========== */
a,
#kc-registration a,
#kc-form-options a,
.kc-form-options a {
color: var(--purple-light);
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
a:hover {
color: var(--purple-main);
text-decoration: underline;
}
/* ========== FORM OPTIONS (Remember me, Forgot password) ========== */
/* Both #kc-form-options and the Forgot Password div are children of .login-pf-settings.
We make the parent a flex row so they sit side by side. */
.login-pf-settings {
display: flex !important;
flex-direction: row !important;
flex-wrap: nowrap !important;
align-items: center !important;
justify-content: space-between !important;
}
#kc-form-options {
flex: 0 0 auto;
}
/* Forgot Password - the anonymous div right after #kc-form-options */
#kc-form-options + div {
flex: 0 0 auto;
text-align: right;
}
/* Back link at top of reset password form */
.back-link {
margin-bottom: 24px;
text-align: center;
}
.back-link a {
color: var(--purple-light);
font-size: 14px;
text-decoration: none;
}
.back-link a:hover {
text-decoration: underline;
}
/* Mobile responsive */
@media (max-width: 480px) {
.back-link {
text-align: center;
margin-bottom: 20px;
}
}
#kc-form-options span,
#kc-form-options .checkbox {
font-size: 14px;
color: var(--text-secondary);
}
/* ========== CHECKBOX ========== */
input[type="checkbox"] {
width: 15px;
height: 15px;
accent-color: var(--purple-main);
cursor: pointer;
}
/* Fix Remember-me alignment: cancel inherited block label + Patternfly padding,
make label a flex row so the box and the text sit on the same baseline.
Patternfly default uses `position: absolute` on the checkbox + `padding-left`
on the label both must be reset for flex layout to work. */
#kc-form-options .checkbox {
margin: 0 !important;
padding: 0 !important;
position: static !important;
}
#kc-form-options .checkbox label {
display: flex !important;
align-items: center;
gap: 8px;
margin: 0 !important;
padding: 0 !important;
cursor: pointer;
font-size: 14px;
line-height: 1;
color: var(--text-secondary);
font-weight: 500;
position: static !important;
}
#kc-form-options .checkbox input[type="checkbox"] {
position: static !important; /* override Patternfly position: absolute */
margin: 0 !important; /* override Patternfly negative margin-left */
flex-shrink: 0;
float: none !important; /* in case Patternfly floats it */
vertical-align: middle;
}
/* ========== REGISTRATION LINK ========== */
#kc-registration {
text-align: center;
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid var(--border);
}
#kc-registration span {
color: var(--text-secondary);
font-size: 14px;
}
/* ========== INFO / MESSAGES ========== */
#kc-info,
.kc-feedback-text,
#kc-info-wrapper {
text-align: center;
margin-top: 20px;
}
#kc-info-message,
.kc-feedback-text {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
}
/* ========== ALERTS ========== */
.alert {
padding: 14px 18px;
border-radius: var(--radius-button);
margin-bottom: 20px;
font-size: 14px;
line-height: 1.5;
}
.alert-error,
.alert-danger {
background: rgba(230, 57, 70, 0.1);
border: 1px solid rgba(230, 57, 70, 0.3);
color: var(--error);
}
.alert-warning {
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.3);
color: var(--warning);
}
.alert-success {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
color: var(--success);
}
.alert-info {
background: rgba(139, 92, 246, 0.1);
border: 1px solid rgba(139, 92, 246, 0.3);
color: var(--purple-light);
}
/* ========== SOCIAL PROVIDERS ========== */
#kc-social-providers {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid var(--border);
}
#kc-social-providers h4,
#kc-social-providers .kc-social-title {
text-align: center;
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 16px;
}
#kc-social-providers ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
#kc-social-providers li a,
.kc-social-provider-link {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 14px 20px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-button);
color: var(--text-primary);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
#kc-social-providers li a:hover,
.kc-social-provider-link:hover {
background: var(--surface-hover);
border-color: var(--border-hover);
}
/* ========== TERMS / RECAPTCHA ========== */
#kc-terms-text {
font-size: 13px;
color: var(--text-muted);
line-height: 1.6;
text-align: center;
margin-top: 16px;
}
/* ========== PASSWORD VISIBILITY TOGGLE ========== */
.kc-form-password-container {
position: relative;
}
.kc-form-password-container button {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 4px;
}
.kc-form-password-container button:hover {
color: var(--text-secondary);
}
/* ========== RESPONSIVE ========== */
@media (max-width: 480px) {
#kc-form,
#kc-register-form,
#kc-reset-password-form {
padding: 28px 28px;
margin: 0 auto;
border-radius: var(--radius-normal);
max-width: 100%;
}
.card-pf::before {
font-size: 36px;
}
input[type="submit"],
button[type="submit"],
.btn-primary {
padding: 14px 24px;
font-size: 15px;
}
}
/* ========== HIDE UNNECESSARY ELEMENTS ========== */
#kc-locale-wrapper,
.kc-locale-dropdown,
.pf-c-alert__icon,
#kc-header-wrapper {
display: none !important;
}
/* Hide PatternFly error icons (red circles) */
.pf-c-form-control__icon,
.pf-c-form-control__utilities,
.pf-c-form__helper-text,
input[aria-invalid="true"] + .pf-c-form-control__utilities,
.kc-feedback-text::before,
span[class*="error"]::before,
span[class*="Error"]::before {
display: none !important;
}
/* Simple red border for invalid inputs */
input[aria-invalid="true"],
input.pf-m-error,
.pf-c-form-control.pf-m-error {
border-color: var(--error) !important;
background-image: none !important;
background: var(--surface) !important;
}
/* ========== FOOTER ========== */
#kc-content::after {
content: '© 2025 didi';
display: block;
text-align: center;
font-size: 12px;
color: var(--text-muted);
margin-top: 32px;
}
/* ========== BACK TO LOGIN LINK ========== */
#kc-info {
order: 100;
margin-top: 24px !important;
}
#kc-info-wrapper {
text-align: center;
}
/* ========== PLACEHOLDERS ========== */
input#username::placeholder,
input#password::placeholder,
input#email::placeholder,
input#firstName::placeholder,
input#lastName::placeholder,
input#password-new::placeholder,
input#password-confirm::placeholder {
color: var(--text-muted);
opacity: 1;
}
/* Add placeholder text via CSS for inputs that don't have it */
input#username:placeholder-shown::placeholder { content: 'Enter your email'; }
input#password:placeholder-shown::placeholder { content: 'Enter your password'; }
/* ========== FIX FORM LAYOUT ========== */
#kc-form-login,
#kc-register-form,
#kc-reset-password-form {
display: flex !important;
flex-direction: column !important;
}
/* Form groups (inputs) - first */
.form-group {
order: 1 !important;
}
/* Remember me / Forgot password settings row - between inputs and button */
.login-pf-settings {
order: 2 !important;
}
/* Submit button - last */
#kc-form-buttons {
order: 3 !important;
}
/* ========== KC-INFO SECTION ========== */
#kc-info {
text-align: center !important;
margin-top: 20px !important;
}
#kc-info-wrapper {
text-align: center !important;
}
#kc-info a {
color: var(--purple-light) !important;
font-weight: 500 !important;
}
/* ========== EMAIL VERIFICATION PAGE ========== */
#kc-content,
#kc-content-wrapper {
text-align: center !important;
}
/* Purple box for main instruction */
.verify-email-box {
background: rgba(139, 92, 246, 0.1) !important;
border: 1px solid rgba(139, 92, 246, 0.3) !important;
border-radius: 12px !important;
padding: 28px !important;
margin: 0 0 24px 0 !important;
text-align: center !important;
}
.verify-email-box .instruction {
text-align: center !important;
margin: 0 !important;
color: var(--text-primary) !important;
}
.verify-email-box .instruction.auto-check {
margin-top: 20px !important;
font-size: 13px !important;
color: var(--text-muted) !important;
}
.instruction {
text-align: center !important;
font-size: 15px;
color: var(--text-secondary);
line-height: 1.6;
margin: 0 0 16px 0;
}
/* Haven't received section - also in purple box */
#kc-info .instruction {
margin: 0 !important;
text-align: center !important;
}
/* Loader animation for auto-check */
.loader {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(139, 92, 246, 0.3);
border-top-color: var(--purple-main);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 8px;
vertical-align: middle;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ========== FALLBACK CENTERING (for older browsers) ========== */
@supports not (min-height: 100dvh) {
.login-pf .container-fluid,
.login-pf .container {
min-height: 100vh !important;
min-height: calc(var(--vh, 1vh) * 100) !important;
}
}
/* ========== MOBILE SPECIFIC FIXES ========== */
@media screen and (max-width: 768px) {
.login-pf .container-fluid,
.login-pf .container {
padding: 12px !important;
min-height: 100vh !important;
min-height: 100dvh !important;
}
.card-pf {
max-width: 100%;
padding: 8px;
margin: 0 auto !important;
}
.card-pf::before {
font-size: 40px;
margin-bottom: 4px;
}
.card-pf::after {
font-size: 13px;
margin-bottom: 20px;
}
/* Prevent horizontal overflow */
#kc-form,
#kc-register-form {
max-width: calc(100vw - 40px);
}
}
/* ========== iOS SAFARI SPECIFIC ========== */
@supports (-webkit-touch-callout: none) {
.login-pf .container-fluid,
.login-pf .container {
min-height: -webkit-fill-available !important;
}
/* Fix iOS input zoom */
input[type="text"],
input[type="password"],
input[type="email"],
.form-control {
font-size: 16px !important;
}
}
/* ========== PatternFly v4 input-group fix (Keycloak 26) ========== */
/* Password input wrapped in .pf-c-input-group for eye-icon toggle.
PF4 specificity overrides our base input rules force theme. */
.pf-c-input-group {
background: transparent !important;
border-radius: var(--radius-button) !important;
overflow: hidden;
border: 1px solid var(--border) !important;
}
.pf-c-input-group:hover {
border-color: var(--border-hover) !important;
}
.pf-c-input-group:focus-within {
border-color: var(--border-focus) !important;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15) !important;
}
.pf-c-input-group .pf-c-form-control,
.pf-c-input-group input[type="password"],
.pf-c-input-group input[type="text"],
.pf-c-input-group input[type="email"] {
background: var(--surface) !important;
color: var(--text-primary) !important;
border: none !important;
border-radius: 0 !important;
box-shadow: none !important;
}
.pf-c-input-group .pf-c-button.pf-m-control {
background: var(--surface) !important;
color: var(--text-primary) !important;
border: none !important;
padding: 0 16px !important;
}
.pf-c-input-group .pf-c-button.pf-m-control:hover {
background: var(--surface-hover) !important;
color: var(--accent) !important;
}
.pf-c-input-group .pf-c-button.pf-m-control .fa {
color: var(--text-muted);
}
/* Eye-icon button: kill all PF4 borders/shadows */
.pf-c-input-group .pf-c-button,
.pf-c-input-group .pf-c-button.pf-m-control {
border: 0 !important;
border-left: 1px solid rgba(255, 255, 255, 0.04) !important;
outline: 0 !important;
box-shadow: none !important;
border-radius: 0 !important;
}
.pf-c-input-group .pf-c-button:focus,
.pf-c-input-group .pf-c-button:hover {
outline: 0 !important;
box-shadow: none !important;
}
/* Force ALL borders off on eye button (override previous border-left) */
.pf-c-input-group > .pf-c-button,
.pf-c-input-group > .pf-c-button.pf-m-control {
border: 0 !important;
border-left: 0 !important;
border-right: 0 !important;
border-top: 0 !important;
border-bottom: 0 !important;
outline: 0 !important;
box-shadow: none !important;
border-radius: 0 !important;
background: var(--surface) !important;
--pf-c-button--BorderColor: transparent !important;
--pf-c-button--BorderWidth: 0 !important;
--pf-c-button--m-control--BorderBottomColor: transparent !important;
}
/* THE actual chenar comes from ::after pseudo-element (PF4 pattern) */
.pf-c-input-group .pf-c-button::after,
.pf-c-input-group .pf-c-button.pf-m-control::after,
.pf-c-input-group .pf-c-button:after,
.pf-c-input-group .pf-c-button.pf-m-control:after {
display: none !important;
border: 0 !important;
content: none !important;
}

View file

@ -0,0 +1,28 @@
// Add placeholders to Keycloak forms
document.addEventListener('DOMContentLoaded', function() {
// Common fields
var username = document.getElementById('username');
if (username) username.placeholder = 'Enter your email';
var email = document.getElementById('email');
if (email) email.placeholder = 'Enter your email';
// Detect register page by presence of password-confirm
var passwordConfirm = document.getElementById('password-confirm');
var password = document.getElementById('password');
if (password) {
password.placeholder = passwordConfirm ? 'Create a password' : 'Enter your password';
}
if (passwordConfirm) {
passwordConfirm.placeholder = 'Confirm password';
}
// Register-only fields
var firstName = document.getElementById('firstName');
if (firstName) firstName.placeholder = 'First name';
var lastName = document.getElementById('lastName');
if (lastName) lastName.placeholder = 'Last name';
});

View file

@ -0,0 +1,12 @@
# Inherit from keycloak to get templates
parent=keycloak
# Override styles
styles=css/login.css
# Scripts
scripts=js/placeholders.js
# Messages
displayName=DIDI - Misinformation Detection Platform
displayNameHtml=<strong>DIDI</strong> - Detect. Investigate. Decide. Inform.

View file

@ -0,0 +1,31 @@
# Kong 3.9 — the declarative config uses fields (ai_metrics, rate-limiting
# redis block) introduced after 3.4; the running gateway is 3.9.1.
FROM kong:3.9
# Switch to root to install curl and set permissions
USER root
# Install curl for configuration import
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Copy custom entrypoint and configuration.
# kong.yml is the JWT-enforcing declarative config (RS256 via Keycloak public
# keys + jwt plugin on every protected route). Baked to the path Kong reads by
# default so `docker run` of this image is SECURE BY DEFAULT — no reliance on a
# runtime volume mount. (The previous image baked a config with `consumers: []`
# and no jwt plugin; see kong.yml.insecure-legacy for that reference.)
COPY entrypoint.sh /entrypoint.sh
COPY declarative/kong.yml /kong/declarative/kong.yml
# DBless + point Kong at the baked config. A volume mount at the same path can
# still override this for environment-specific configs (cluster vs local).
ENV KONG_DATABASE=off
ENV KONG_DECLARATIVE_CONFIG=/kong/declarative/kong.yml
# Make entrypoint executable
RUN chmod +x /entrypoint.sh
# Switch back to kong user
USER kong
ENTRYPOINT ["/entrypoint.sh"]

View file

@ -0,0 +1,208 @@
# didiKong - Index
API Gateway pentru platforma DIDI. Toate requesturile externe trec prin Kong inainte sa ajunga la serviciile backend. Gestioneaza rutare, rate limiting, CORS, SSL si headere.
**Imagine**: didi-kong:latest (Kong 3.9.1)
**Container**: didi-kong (activ, pe masina de deployment)
**Mod**: DB-less declarativ (`KONG_DATABASE=off`)
**Porturi** (doar loopback `127.0.0.1`): 18000 (proxy HTTP -> container 8000), 18001 (admin API -> 8001), 18443 (proxy TLS -> 8443 ssl)
---
## Deployment: container local DB-less (activ)
Kong ruleaza ca un singur container `didi-kong` pe masina de deployment, in mod **DB-less** (`KONG_DATABASE=off`). Nu exista Kong Cluster, Control Plane, Data Planes sau HAProxy, si nu exista PostgreSQL pentru Kong.
- Config declarativa din `declarative/kong.yml.didi11-local`, montata in container la `/kong/declarative/kong.yml`.
- Porturile sunt legate exclusiv pe loopback (`127.0.0.1`): proxy `18000`, admin API `18001`, proxy TLS `18443`. Kong nu e expus public direct; traficul extern intra prin edge/tunel catre proxy-ul local.
- Single source of truth = fisierul declarativ `kong.yml.didi11-local`. Orice modificare de rute/plugin-uri se face in acest fisier + reload.
---
## Mod de operare: DB-less (declarativ)
Configuratia vine integral din fisierul `declarative/kong.yml.didi11-local`. Kong nu are baza de date proprie.
```
KONG_DATABASE=off
KONG_DECLARATIVE_CONFIG=/kong/declarative/kong.yml
```
Nu exista mod PostgreSQL / productie separata pentru Kong: acelasi fisier declarativ este sursa unica de adevar.
---
## Servicii inregistrate
Kong ruteaza catre 2 servicii backend (conform `kong.yml.didi11-local`):
| Serviciu | Target | Status |
|----------|--------|--------|
| didi-agent-v3 | http://didi-agent-v3:24803 | activ |
| didi-framework | http://didi-framework:3005 | activ |
Nota: rutele agent-v3 au timeout mare (660s = 11 min) pentru procesarea video.
---
## Rute definite
### Agent service (activ)
| Path | Metode | Destinatie |
|------|--------|------------|
| /agent/health | GET | agent-api |
| /agent/status | GET | agent-api |
| /api/pipelines | GET | agent-api |
| /api/analyze | POST | agent-api |
| /api/sessions | GET | agent-api |
| /api/upload | POST | agent-api |
| /api/abort | POST | agent-api |
| /api/v3/* | toate | agent-api (acopera si /api/v3/moderation/*) |
### didiFramework (config + HIL moderation)
| Path | Metode | Destinatie |
|------|--------|------------|
| /api/* | toate | didiFramework (acopera /api/moderation-config, /api/sensitive-topics, /api/moderation-roles) |
Nota: rutele de moderatie (HIL) calatoresc pe regulile generale `/api/v3/*` (agent-v3) si `/api/*` (didiFramework) — nu sunt necesare reguli Kong dedicate.
> Notă: serviciile Python legacy `orchestrator-api` și `analysis-api` (rute `/api/v1/catalog|pipelines|runs/*`,
> `/analysis/*`) NU mai există în config-ul local — au fost eliminate odată cu migrarea pe agent-v3. Config-ul
> `kong.yml.didi11-local` rutează exclusiv către `didi-agent-v3` și `didi-framework`.
### Admin dashboard
| Path | Metode | Destinatie |
|------|--------|------------|
| /admin | toate | admin-dashboard |
| /admin/api | toate | admin-dashboard |
---
## Plugin-uri globale
5 plugin-uri active pe toate rutele:
### 1. CORS
- Origins: localhost:3000, localhost:3001, localhost:8100, * (wildcard)
- Metode: GET, POST, PUT, DELETE, OPTIONS, PATCH
- Headere: Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, Authorization, X-Request-ID
- Exposed headers: X-Auth-Token, X-Request-ID
- Credentials: activat
- Max age: 3600s
- Preflight continue: nu
### 2. Rate Limiting
- 100 requests/minut per consumer
- 2000 requests/ora per consumer
- 10000 requests/zi per consumer
- Politica: local (fara state distribuit)
- Fault tolerant: da
### 3. Correlation ID (Request ID Tracking)
- Header: X-Request-ID
- Generator: UUID
- Echo downstream: da (returnat in raspuns)
### 4. Request Size Limiting
- Max payload: 100MB (pentru upload media)
- Require content-length: nu
### 5. Response Transformer
- Adauga: X-Gateway: DIDI-Kong, X-API-Version: 2.0
- Sterge: Server, Via (ascunde detalii interne)
---
## Upstreams (load balancing)
Config-ul local (`kong.yml.didi11-local`) NU definește upstream-uri — rutarea se face
direct către serviciile `didi-agent-v3:24803` și `didi-framework:3005`. Scalarea orizontală
a workerilor se face la nivel de agent-v3 (`scale-workers.sh`), nu prin upstream-uri Kong.
---
## SSL/TLS
- Certificat self-signed pentru <host-local>
- Locatie: certs/server.crt, certs/server.key
- Valabil: feb 2026 - feb 2027
- Emitent: DIDI, Bucharest, RO
- Servit pe proxy-ul TLS local `127.0.0.1:18443`
---
## Integrare Keycloak (LOCAL) + enforcement JWT (Modul 4 Gateway)
Kong valideaza token-urile emise de instanta Keycloak **locala** (`didi-keycloak`, port `28080`, servita sub `/auth`). Realm-urile locale sunt `didi-clients` (useri finali) si `didi-admins` (operatori). Issuer-ele locale au forma `http://localhost:28080/auth/realms/didi-clients` si `.../didi-admins`.
Enforcement-ul se face prin pluginul Kong `jwt` (nu OIDC/introspection): validare de semnatura **RS256** cu chei publice statice, `key_claim_name: iss` (Kong potriveste tokenul dupa claim-ul `iss` cu un `jwt_secret` inregistrat pe consumer).
Consumer: **`didi-keycloak-users`** — detine `jwt_secrets` (RS256, `rsa_public_key`) pentru toate issuer-ele acceptate, cheia fiind chiar valoarea `iss`:
- local: `http://localhost:28080/realms/didi-clients`, `http://localhost:28080/realms/didi-admins`
- plus issuer-ele externe/edge folosite in fata proxy-ului: `https://didi365.eu/auth/realms/{didi-clients,didi-admins}`, `https://<sso-extern>/realms/...`, `https://<sso-extern>/realms/...`, `https://<host-local>/auth/realms/...`, `https://10.11.10.11:{3001,8443}/auth/realms/...`
Pluginul `jwt` este activ pe **16 rute protejate** de pe cele doua servicii (`didi-agent-v3` si `didi-framework`), incluzand ruta **`/framework`** (route `didi-framework-direct`, jwt adaugat 2026-07-08). Rutele publice raman fara jwt (health/status, media public, verify-email, waitlist), iar rutele de extensie folosesc autentificare separata prin `X-API-Key` + rate-limiting.
JWT validation flow:
1. Clientul (SPA/extensie) obtine token JWT de la Keycloak local (`/auth/realms/didi-clients` sau `/auth/realms/didi-admins`).
2. Trimite request cu `Authorization: Bearer {token}`.
3. Kong potriveste `iss` cu `jwt_secret`-ul consumer-ului `didi-keycloak-users` si valideaza semnatura RS256; token invalid/lipsa -> `401`.
4. Daca valid, ruteaza requestul catre serviciul backend local (`didi-agent-v3:24803` / `didi-framework`).
5. Serviciul backend decodeaza tokenul pentru `user_id`/`email`/`realm_access.roles` (fara re-validare — Kong a validat deja).
---
## Configurare performanta
```
KONG_NGINX_WORKER_PROCESSES=auto
KONG_MEM_CACHE_SIZE=256m
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE=128k
KONG_NGINX_PROXY_PROXY_BUFFERS=4 256k
KONG_NGINX_PROXY_PROXY_BUSY_BUFFERS_SIZE=256k
KONG_NGINX_HTTP_LARGE_CLIENT_HEADER_BUFFERS=4 64k
```
Buffer-urile mari sunt necesare pentru headerele JWT de la Keycloak (token-urile pot fi foarte mari).
---
## Fisiere
```
Dockerfile -- Imagine didi-kong (Kong 3.9), instaleaza curl, entrypoint
entrypoint.sh -- Pornire Kong DB-less + wait for ready + log servicii
declarative/
kong.yml.didi11-local -- Configurare declarativa activa (single source of truth, montata la /kong/declarative/kong.yml)
certs/
server.crt -- Certificat SSL self-signed
server.key -- Cheie privata SSL
```
Zero cod custom. Zero plugin-uri Lua custom. Doar configurare declarativa si certificat SSL.
---
## Health check
```
kong health (interval 30s, timeout 10s, retries 3, start period 60s)
```
---
## Consumers
Configuratia declarativa defineste un singur consumer Kong: **`didi-keycloak-users`** (username + custom_id `didi-keycloak-users`), care detine `jwt_secrets`-urile RS256 pentru toate issuer-ele Keycloak locale/edge acceptate (vezi sectiunea Integrare Keycloak). Acest consumer este cel pe care pluginul `jwt` il rezolva la validarea tokenului.
Limitele per-tier (free/paid/enterprise) provin din atributele de grup Keycloak (realm-urile locale `didi-clients` / `didi-admins`) si sunt disponibile in claim-urile JWT pentru rate limiting; rutele de extensie au propriul rate-limiting per `X-API-Key` (30/min).
## Recent Changes
- **2026-07-08 — jwt pe `/framework`**: pluginul `jwt` (RS256, `key_claim_name: iss`) adaugat pe ruta `didi-framework-direct` (path `/framework`). Enforcement JWT acum activ pe 16 rute protejate pe `didi-agent-v3` + `didi-framework`.
- **jwt_secrets pentru issuer-ele locale** pe consumer `didi-keycloak-users`: `http://localhost:28080/realms/didi-clients` + `.../didi-admins`, alaturi de issuer-ele edge (`didi365.eu/auth`, `<sso-extern>`, `<sso-extern>`, `<host-local>/auth`, `10.11.10.11:{3001,8443}/auth`) pentru realm-urile `didi-clients` si `didi-admins`.
- **Rute de extensie** (`didi-agent-extension-analyze`, `-async`, `-status`, `-upload`): autentificare `X-API-Key` + `rate-limiting` 30/min + `request-transformer`; CORS extins cu origins `chrome-extension://[a-z]+`, `moz-extension://[a-z0-9-]+` + header `X-API-Key`.
- **Single source of truth** = `declarative/kong.yml.didi11-local` (DB-less). Fara cluster, fara decK sync.

View file

@ -0,0 +1,121 @@
# Kong — migrat pe cluster shared (2026-04-28)
> **TL;DR**: containerul local `didi-kong` nu mai rulează. Tot traficul public DIDI trece prin clusterul Kong shared extern: `HAProxy 10.11.10.175``Kong DP1/DP2 (10.11.10.177/178)`. Configurația DIDI declarativă e în `declarative/kong-cluster.yml`.
---
## Ce era aici (înainte de 2026-04-28)
`didi-kong` — un container Kong 3.4.2 standalone (`image: didi-kong:latest`) definit în `backend/production/docker-compose.yml`. Mod Postgres (DB pe clusterul Patroni). Servea singur tot traficul `didi365.eu`. Avea 16 servicii + 35 rute + 5 plugin-uri globale + 1 consumer JWT.
## Ce e acum
Tot traficul DIDI a fost migrat pe **Kong cluster extern** (același folosit de `lege365`, `rafai`, `biddie`, `notify`, `firme`).
| Component cluster | IP:Port | Rol |
|---|---|---|
| HAProxy LB | `10.11.10.175:443` | edge → DP, terminare TLS pentru DIDI |
| Kong CP (Control Plane) | `10.11.10.176:8001` | configurare via Admin API (DB-attached) |
| Kong DP1 | `10.11.10.177:8000/8443` | proxy trafic |
| Kong DP2 | `10.11.10.178:8000/8443` | proxy trafic |
Cluster Kong rulează versiunea 3.14.0.1 în mod Hybrid CP/DP, cu DB Postgres pe clusterul Patroni.
## Servicii DIDI consolidate pe cluster (4)
| Service cluster | Tag | Upstream | Routes |
|---|---|---|---|
| `didi-agent-v3` | `product:didi,env:prod,kind:api` | `10.11.10.12:24803` | 16 (15 API + 1 prefix /agent-v3) |
| `didi-framework` | `product:didi,env:prod,kind:api` | `10.11.10.12:3005` | 6 |
| `didi-keycloak` | `product:didi,env:prod,kind:auth` | `10.11.10.12:28000` | 2 |
> `didi-admin` NU e migrat — accesibil intern via VPN, păstrează Docker network DNS.
Toate rutele matchează pe `Host: didi365.eu, www.didi365.eu, <host-local>`.
## Servicii droppate complet
- `analysis-api`, `orchestrator-api` (legacy, 0 routes)
- `agent-v3`, `keycloak` (alias-uri orfane)
- `minio-storage` (deja în cluster MinIO 4-node, vezi `didiStorage/MIGRATION.md`)
- `pgadmin`, `redis-commander`, `rabbitmq-management`, `minio-console` (tooling în dispariție)
## Configurația declarativă
```
declarative/
├── kong-cluster.yml ← config-ul curent al DIDI pe cluster (sursa de adevăr)
├── kong-local-backup.yml ← snapshot complet al Kong-ului local înainte de migrare
└── kong.yml ← config-ul declarativ legacy (DB-less staging)
```
Apply / sync:
```bash
docker run --rm --network host -v $(pwd)/declarative:/cfg kong/deck:latest \
gateway sync /cfg/kong-cluster.yml --kong-addr http://10.11.10.176:8001
```
Validare fără modificări:
```bash
docker run --rm --network host -v $(pwd)/declarative:/cfg kong/deck:latest \
gateway diff /cfg/kong-cluster.yml --kong-addr http://10.11.10.176:8001
```
## Edge nginx (Contabo)
Edge nginx pe Contabo (`213.136.83.198`, `/srv/edge-nginx/conf.d/didi365.conf`) a fost actualizat să trimită `/auth/`, `/api/`, `/agent-v3/` la `https://10.11.10.175` (în loc de `https://<host-local>` care era kong local). Backup: `didi365.conf.bak.pre-kong-cluster-20260428`.
## Admin nginx
`backend/admin-dashboard/nginx-ssl.conf``location /api/` proxy_pass schimbat de la `https://kong:8443` (Docker DNS local) la `https://10.11.10.175` cu `Host: didi365.eu` overridden. Restul (`/auth/`, `/agent-v3/`, `/framework/`) bypass-ează Kong, merg direct la backend.
## Cum verifici că merge prin cluster
Răspunsurile prin cluster Kong au header-ul `X-Gateway: DIDI-Kong-Cluster`. Răspunsurile (vechile) prin Kong local aveau `X-Gateway: DIDI-Kong-Staging`.
```bash
curl -sI https://didi365.eu/auth/realms/didi-clients | grep -i x-gateway
# expect: x-gateway: DIDI-Kong-Cluster
```
## Rollback (dacă apar probleme)
Kong local nu mai există ca container, dar imaginea `didi-kong:latest` e intactă local. Pași rollback:
1. **Restore docker-compose**: revert commitul care a scos `kong:` din `production/docker-compose.yml`
2. **Repornește kong**: `cd backend/production && docker compose up -d kong`
3. **Rollback edge Contabo**:
```
cp /srv/edge-nginx/conf.d/didi365.conf.bak.pre-kong-cluster-20260428 \
/srv/edge-nginx/conf.d/didi365.conf
nginx -s reload
```
4. **Rollback admin nginx**: revert commit-ul nginx-ssl.conf, rebuild `didi-admin:latest`
DB-ul Kong-ului local (`KONG_PG_DATABASE`) era pe clusterul Patroni — datele sunt încă acolo (nu s-au șters).
## Lecții importante (capcane confirmate)
1. **decK default `protocols: [https]` rupe HAProxy passthrough.** Cluster Kong returna `426 Please use HTTPS` deși edge termina TLS. HAProxy LB forwardează HTTP intern la DP — Kong vede HTTP. Fix: `_info.defaults.route.protocols: [http, https]`.
2. **Port binding upstream.** Original `didi-agent-v3` era `127.0.0.1:24803` și `didi-framework` deloc expus. Modificat în docker-compose la `10.11.10.12:24803`/`3005` ca DPs cluster să ajungă (LAN VLAN 10).
3. **strip_path nu e uniform.** Match local: `/agent/health`, `/auth`, `/framework`, `/agent-v3` au `strip_path: true`. Restul `false`. Verifică înainte să copiezi.
4. **Host header obligatoriu.** Cluster shared filtrează rute pe Host. Edge nginx și admin nginx fac `proxy_set_header Host didi365.eu`. Fără asta — 404.
5. **JWT plugin per-rută.** Cluster shared, alți tenanți nu vor JWT didi-clients. Plugin aplicat explicit pe 13 rute.
## Linkuri rapide
- Cluster Admin API: `http://10.11.10.176:8001`
- Cluster ghid onboarding: `landingzone/kong-api/README.md` (repo `git.finesynergy.eu/lucian/landingzone`)
- Documentație decK: <https://docs.konghq.com/deck/>
## Status
- ✅ Migrare aplicată: 2026-04-28
- ✅ Kong local oprit și șters din docker-compose
- ✅ Edge nginx Contabo actualizat
- ✅ Admin nginx actualizat
- ⏳ Soak 24-48h în desfășurare; eliminarea volumelor Kong după validare

View file

@ -0,0 +1,537 @@
_format_version: "3.0"
_info:
select_tags:
- product:didi
- env:prod
defaults:
route:
# Routes accept both HTTP and HTTPS (matches cluster pattern for lege365/rafai/etc).
# HAProxy LB terminates TLS at edge, forwards HTTP to Kong DP — Kong must accept HTTP
# internally or it returns 426 "Please use HTTPS protocol".
protocols: [http, https]
# ============================================================
# DIDI tenant configuration for shared Kong cluster
# Cluster CP: 10.11.10.176:8001 | DP1: 10.11.10.177 | DP2: 10.11.10.178 | LB: 10.11.10.175
# Hosts: didi365.eu (public) + www.didi365.eu + localhost (internal alias)
# Upstreams: 10.11.10.12 (DIDI host) on exposed ports
# Plugins are applied per-service (NOT global) — cluster shared with lege365/rafai/biddie/notify
# ============================================================
# ============================================================
# CONSUMERS — JWT issuers preserved from local Kong (Keycloak realm didi-clients)
# ============================================================
consumers:
- username: didi-keycloak-users
custom_id: didi-keycloak-users
tags: [product:didi, env:prod]
jwt_secrets:
- algorithm: RS256
key: https://localhost/auth/realms/didi-clients
secret: 1qG51hpfTte1TGQzcAhKWjVkXsMWNIun
rsa_public_key: |-
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlpv0lfjAFPGV4hLK6jp6
EsVlxax2nPA9I2IzGwNUIK8AsKwC9qu+737jarsjewx3ya/0s1uDP0ilbdh+wEzW
Do/8jjWd0DxgTxMxCTS7DU07UZKXJWGc/Z+ansUPUcjqJ+uLTdu331z7ajK2FIZF
7yYH2WgjzApF6YSMx/dqybp/bdmBrvsPDGv1EJK4a72jV3P86WCW4ZDax2Qayw1t
iKnO3+o6xvyoVSMeVJbs9ArjpAldueMLfTZqYBSmWe/rlBSIMWYkKTSgS+pdakez
G71qs2RcSkI+GxlfJw0DJA8TfSjol6zc+EIUtAYG0pwwAqPB2PvkxMoDbL2UDkZE
/wIDAQAB
-----END PUBLIC KEY-----
- algorithm: RS256
key: https://didi365.eu/auth/realms/didi-clients
secret: IS8AJNbvC3taNROALc4bHslsZnSKELQD
rsa_public_key: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlpv0lfjAFPGV4hLK6jp6
EsVlxax2nPA9I2IzGwNUIK8AsKwC9qu+737jarsjewx3ya/0s1uDP0ilbdh+wEzW
Do/8jjWd0DxgTxMxCTS7DU07UZKXJWGc/Z+ansUPUcjqJ+uLTdu331z7ajK2FIZF
7yYH2WgjzApF6YSMx/dqybp/bdmBrvsPDGv1EJK4a72jV3P86WCW4ZDax2Qayw1t
iKnO3+o6xvyoVSMeVJbs9ArjpAldueMLfTZqYBSmWe/rlBSIMWYkKTSgS+pdakez
G71qs2RcSkI+GxlfJw0DJA8TfSjol6zc+EIUtAYG0pwwAqPB2PvkxMoDbL2UDkZE
/wIDAQAB
-----END PUBLIC KEY-----
# SSO cluster (sso.local) — added 2026-04-29 after migration la Keycloak SSO public.
# Same RSA public key (realm exported/imported from local with key preserved).
- algorithm: RS256
key: https://sso.local/realms/didi-clients
secret: lEyVoR2eXLgWmQQ5q6fZRbCkVJhSaTuB
rsa_public_key: |-
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlpv0lfjAFPGV4hLK6jp6
EsVlxax2nPA9I2IzGwNUIK8AsKwC9qu+737jarsjewx3ya/0s1uDP0ilbdh+wEzW
Do/8jjWd0DxgTxMxCTS7DU07UZKXJWGc/Z+ansUPUcjqJ+uLTdu331z7ajK2FIZF
7yYH2WgjzApF6YSMx/dqybp/bdmBrvsPDGv1EJK4a72jV3P86WCW4ZDax2Qayw1t
iKnO3+o6xvyoVSMeVJbs9ArjpAldueMLfTZqYBSmWe/rlBSIMWYkKTSgS+pdakez
G71qs2RcSkI+GxlfJw0DJA8TfSjol6zc+EIUtAYG0pwwAqPB2PvkxMoDbL2UDkZE
/wIDAQAB
-----END PUBLIC KEY-----
# Internal SSO host (used during transition; also kept for proxy fallback paths)
- algorithm: RS256
key: https://sso-admin.local/realms/didi-clients
secret: kPwQjA4nFsUzMxRyV8tDeBcGhJlOvIuS
rsa_public_key: |-
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlpv0lfjAFPGV4hLK6jp6
EsVlxax2nPA9I2IzGwNUIK8AsKwC9qu+737jarsjewx3ya/0s1uDP0ilbdh+wEzW
Do/8jjWd0DxgTxMxCTS7DU07UZKXJWGc/Z+ansUPUcjqJ+uLTdu331z7ajK2FIZF
7yYH2WgjzApF6YSMx/dqybp/bdmBrvsPDGv1EJK4a72jV3P86WCW4ZDax2Qayw1t
iKnO3+o6xvyoVSMeVJbs9ArjpAldueMLfTZqYBSmWe/rlBSIMWYkKTSgS+pdakez
G71qs2RcSkI+GxlfJw0DJA8TfSjol6zc+EIUtAYG0pwwAqPB2PvkxMoDbL2UDkZE
/wIDAQAB
-----END PUBLIC KEY-----
# didi-admins realm — admin-dashboard SPA auth (added 2026-05-04)
# Different RSA key than didi-clients (separate realm).
- algorithm: RS256
key: https://didi365.eu/auth/realms/didi-admins
secret: aDmInS001PuBlIcEdge2026May04PaSsKonGvErY
rsa_public_key: |-
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkcx2UE88P4j4XzWQ/bsd
0C69xZuxo/VXvhyKPj/t7u6ILIpD3/KL0sI0Ei4ZOlfjZ8PHmmEhWVwOYRCyhxgN
aq5EMk4MKmUJ0KZd6pixvufKp8ddnI/xKdUROLbdzEHSxP251uHUUCYzpsKYisZ7
t6UidWgDBDrcU42YsV4OggvPlvemtQKFawv0CpQ7BLhyTm2WUD4iq7H17OEI975i
ocC7Zqrk9itl2o0w8x9fSMOyMgsEafkVQ/KIuZA1/kKugtpG9eyYE49RFH7iQZd9
KmQow/XDxtKHfaLi1CEU4X2fHCThMWKdRQeK8N1n6qS+yanygcYJlUf8FKybgRxZ
NwIDAQAB
-----END PUBLIC KEY-----
- algorithm: RS256
key: https://sso.local/realms/didi-admins
secret: aDmInS002SsOcLuStEr2026May04PaSsKonGvErY
rsa_public_key: |-
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkcx2UE88P4j4XzWQ/bsd
0C69xZuxo/VXvhyKPj/t7u6ILIpD3/KL0sI0Ei4ZOlfjZ8PHmmEhWVwOYRCyhxgN
aq5EMk4MKmUJ0KZd6pixvufKp8ddnI/xKdUROLbdzEHSxP251uHUUCYzpsKYisZ7
t6UidWgDBDrcU42YsV4OggvPlvemtQKFawv0CpQ7BLhyTm2WUD4iq7H17OEI975i
ocC7Zqrk9itl2o0w8x9fSMOyMgsEafkVQ/KIuZA1/kKugtpG9eyYE49RFH7iQZd9
KmQow/XDxtKHfaLi1CEU4X2fHCThMWKdRQeK8N1n6qS+yanygcYJlUf8FKybgRxZ
NwIDAQAB
-----END PUBLIC KEY-----
- algorithm: RS256
key: https://sso-admin.local/realms/didi-admins
secret: aDmInS003LoCaLcLuStEr2026May04PaSsKonGv
rsa_public_key: |-
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkcx2UE88P4j4XzWQ/bsd
0C69xZuxo/VXvhyKPj/t7u6ILIpD3/KL0sI0Ei4ZOlfjZ8PHmmEhWVwOYRCyhxgN
aq5EMk4MKmUJ0KZd6pixvufKp8ddnI/xKdUROLbdzEHSxP251uHUUCYzpsKYisZ7
t6UidWgDBDrcU42YsV4OggvPlvemtQKFawv0CpQ7BLhyTm2WUD4iq7H17OEI975i
ocC7Zqrk9itl2o0w8x9fSMOyMgsEafkVQ/KIuZA1/kKugtpG9eyYE49RFH7iQZd9
KmQow/XDxtKHfaLi1CEU4X2fHCThMWKdRQeK8N1n6qS+yanygcYJlUf8FKybgRxZ
NwIDAQAB
-----END PUBLIC KEY-----
- algorithm: RS256
key: https://localhost/auth/realms/didi-admins
secret: aDmInS004DiDi11LoCaL2026May04PaSsKonGvE
rsa_public_key: |-
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkcx2UE88P4j4XzWQ/bsd
0C69xZuxo/VXvhyKPj/t7u6ILIpD3/KL0sI0Ei4ZOlfjZ8PHmmEhWVwOYRCyhxgN
aq5EMk4MKmUJ0KZd6pixvufKp8ddnI/xKdUROLbdzEHSxP251uHUUCYzpsKYisZ7
t6UidWgDBDrcU42YsV4OggvPlvemtQKFawv0CpQ7BLhyTm2WUD4iq7H17OEI975i
ocC7Zqrk9itl2o0w8x9fSMOyMgsEafkVQ/KIuZA1/kKugtpG9eyYE49RFH7iQZd9
KmQow/XDxtKHfaLi1CEU4X2fHCThMWKdRQeK8N1n6qS+yanygcYJlUf8FKybgRxZ
NwIDAQAB
-----END PUBLIC KEY-----
# ============================================================
# SERVICES (4 consolidated from 9 local) + ROUTES + PER-SERVICE PLUGINS
# ============================================================
services:
# ----------------------------------------------------------
# SERVICE 1: didi-agent-v3 (was: agent-api + agent-v3 + agent-v3-api)
# ----------------------------------------------------------
- name: didi-agent-v3
protocol: http
host: 10.11.10.12
port: 24803
retries: 5
connect_timeout: 60000
write_timeout: 660000
read_timeout: 660000
tags: [product:didi, env:prod, kind:api]
plugins:
- name: cors
tags: [product:didi, env:prod]
config:
# chrome-extension://* and moz-extension://* required for browser extension API
origins: [https://didi365.eu, https://www.didi365.eu, "chrome-extension://[a-z]+", "moz-extension://[a-z0-9-]+"]
methods: [GET, POST, PUT, DELETE, OPTIONS, PATCH]
# X-API-Key header required for extension auth
headers: [Accept, Authorization, Content-Type, X-Request-ID, X-API-Key]
exposed_headers: [X-Request-ID]
credentials: true
max_age: 3600
preflight_continue: false
- name: request-size-limiting
tags: [product:didi, env:prod]
config:
allowed_payload_size: 104857600
size_unit: bytes
require_content_length: false
- name: response-transformer
tags: [product:didi, env:prod]
config:
add:
headers: ["X-Gateway:DIDI-Kong-Cluster", "X-API-Version:2.0"]
remove:
headers: [Server, Via]
- name: correlation-id
tags: [product:didi, env:prod]
config:
header_name: X-Request-ID
generator: uuid
echo_downstream: true
routes:
# JWT-protected endpoints
- name: didi-agent-abort
paths: [/api/abort]
methods: [POST, OPTIONS]
hosts: &didi-hosts [didi365.eu, www.didi365.eu, localhost]
strip_path: false
preserve_host: true
tags: [product:didi, env:prod]
plugins:
- name: jwt
config: &jwt-config
key_claim_name: iss
claims_to_verify: [exp]
header_names: [authorization]
uri_param_names: [jwt]
run_on_preflight: true
secret_is_base64: false
maximum_expiration: 0
- name: didi-agent-admin-services
paths: [/api/v1/admin/services]
methods: [GET, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod]
plugins:
- name: jwt
config: *jwt-config
- name: didi-agent-analysis
paths: [/api/analysis]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod]
plugins:
- name: jwt
config: *jwt-config
- name: didi-agent-analyze
paths: [/api/analyze]
methods: [POST, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod]
plugins:
- name: jwt
config: *jwt-config
- name: didi-agent-jobs
paths: [/api/v1/jobs]
methods: [GET, POST, DELETE, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod]
plugins:
- name: jwt
config: *jwt-config
- name: didi-agent-pipelines
paths: [/api/pipelines]
methods: [GET, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod]
plugins:
- name: jwt
config: *jwt-config
- name: didi-agent-progress
paths: [/api/progress]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod]
plugins:
- name: jwt
config: *jwt-config
- name: didi-agent-sessions
paths: [/api/sessions]
methods: [GET, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod]
plugins:
- name: jwt
config: *jwt-config
- name: didi-agent-storage
paths: [/api/storage]
methods: [GET, POST, DELETE, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod]
plugins:
- name: jwt
config: *jwt-config
- name: didi-agent-subscriptions-v1
# Legacy v1 endpoint on agent-v3 (current v2 is on framework-api)
paths: [/api/v1/subscriptions]
methods: [GET, POST, DELETE, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod, legacy:v1]
plugins:
- name: jwt
config: *jwt-config
- name: didi-agent-upload
paths: [/api/upload]
methods: [POST, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod]
plugins:
- name: jwt
config: *jwt-config
# Public endpoints (no JWT)
- name: didi-agent-health
# Local had strip_path=true: /agent/health -> "/" on agent-v3 (root, returns 200)
paths: [/agent/health]
methods: [GET, OPTIONS]
hosts: *didi-hosts
strip_path: true
preserve_host: true
tags: [product:didi, env:prod, public:true]
- name: didi-agent-status
paths: [/api/status]
methods: [GET, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod, public:true]
- name: didi-agent-media-public
# Public media playback proxy (range-supported, served by agent-v3 itself)
paths: [/api/v3/media/file]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod, public:true]
- name: didi-agent-v3-prefix
# Edge nginx forwards /agent-v3/* here. Local Kong stripped the prefix
# (e.g. /agent-v3/api/v3/health -> /api/v3/health on agent-v3).
# Local had JWT plugin on this route — preserved here to match security model.
paths: [/agent-v3]
hosts: *didi-hosts
strip_path: true
preserve_host: true
tags: [product:didi, env:prod]
plugins:
- name: jwt
config: *jwt-config
# Browser extension API endpoints — NO JWT (X-API-Key validated in agent-v3).
# All four routes share the same request-transformer pattern + rate-limit.
# Regex paths (~ prefix) with `$` anchor prevent /analyze matching /analyze-async.
- name: didi-agent-extension-analyze
# Sync analyze (legacy — small text only, hits Cloudflare 100s timeout otherwise)
paths: [~/agent-v3/api/v3/pipeline/extension/analyze$]
methods: [POST, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod, public:true, auth:api-key]
plugins:
- name: request-transformer
config:
replace:
uri: /api/v3/pipeline/extension/analyze
- name: rate-limiting
config: &ext-rate-limit
minute: 30
hour: 500
policy: local
limit_by: header
header_name: X-API-Key
fault_tolerant: true
hide_client_headers: false
error_code: 429
error_message: "API rate limit exceeded"
- name: didi-agent-extension-analyze-async
# Async dispatch — returns 202 + session_id (used by extension for all flows)
paths: [~/agent-v3/api/v3/pipeline/extension/analyze-async$]
methods: [POST, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod, public:true, auth:api-key]
plugins:
- name: request-transformer
config:
replace:
uri: /api/v3/pipeline/extension/analyze-async
- name: rate-limiting
config: *ext-rate-limit
- name: didi-agent-extension-upload
# Multipart media upload (screenshot/video) — multer expects "file" field
paths: [~/agent-v3/api/v3/pipeline/extension/upload$]
methods: [POST, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod, public:true, auth:api-key]
plugins:
- name: request-transformer
config:
replace:
uri: /api/v3/pipeline/extension/upload
- name: rate-limiting
config: *ext-rate-limit
- name: didi-agent-extension-status
# Polling endpoint with sessionId capture — rewrite preserves the UUID
paths: ['~/agent-v3/api/v3/pipeline/extension/status/(?<sid>[\w-]+)$']
methods: [GET, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod, public:true, auth:api-key]
plugins:
- name: request-transformer
config:
replace:
uri: /api/v3/pipeline/extension/status/$(uri_captures.sid)
- name: rate-limiting
config:
# Polling can hit this every 2s — bump per-minute limit
minute: 120
hour: 2000
policy: local
limit_by: header
header_name: X-API-Key
fault_tolerant: true
hide_client_headers: false
error_code: 429
error_message: "Polling rate limit exceeded"
# ----------------------------------------------------------
# SERVICE 2: didi-framework (was: framework-api + didi-framework)
# ----------------------------------------------------------
- name: didi-framework
protocol: http
host: 10.11.10.12
port: 3005
retries: 5
connect_timeout: 60000
write_timeout: 60000
read_timeout: 60000
tags: [product:didi, env:prod, kind:api]
plugins:
- name: cors
tags: [product:didi, env:prod]
config:
origins: [https://didi365.eu, https://www.didi365.eu]
methods: [GET, POST, PUT, DELETE, OPTIONS, PATCH]
headers: [Accept, Authorization, Content-Type, X-Request-ID]
exposed_headers: [X-Request-ID]
credentials: true
max_age: 3600
preflight_continue: false
- name: request-size-limiting
tags: [product:didi, env:prod]
config:
allowed_payload_size: 104857600
size_unit: bytes
require_content_length: false
- name: response-transformer
tags: [product:didi, env:prod]
config:
add:
headers: ["X-Gateway:DIDI-Kong-Cluster", "X-API-Version:2.0"]
remove:
headers: [Server, Via]
- name: correlation-id
tags: [product:didi, env:prod]
config:
header_name: X-Request-ID
generator: uuid
echo_downstream: true
routes:
- name: didi-framework-auth
paths: [/api/auth]
methods: [GET, POST, PUT, DELETE, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod]
plugins:
- name: jwt
config: *jwt-config
- name: didi-framework-auth-verify-email
# PUBLIC route — must beat /api/auth (JWT) on priority
paths: [/api/auth/verify-email]
methods: [GET, POST]
hosts: *didi-hosts
strip_path: false
preserve_host: true
regex_priority: 100
tags: [product:didi, env:prod, public:true]
- name: didi-framework-history
paths: [/api/history]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod]
plugins:
- name: jwt
config: *jwt-config
- name: didi-framework-subscriptions
# Current /api/subscriptions endpoint (v2). Legacy /api/v1/subscriptions is on agent-v3.
paths: [/api/subscriptions]
methods: [GET, POST, DELETE, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod]
plugins:
- name: jwt
config: *jwt-config
- name: didi-framework-waitlist
# PUBLIC waitlist signup. Local Kong had this duplicated (one with JWT, one without).
# Keeping no-JWT version — public signup is correct behavior.
paths: [/api/waitlist]
methods: [GET, POST, DELETE, OPTIONS]
hosts: *didi-hosts
strip_path: false
preserve_host: true
tags: [product:didi, env:prod, public:true]
- name: didi-framework-direct
# Legacy /framework prefix — local had strip_path=true so /framework/api/X -> /api/X on framework
paths: [/framework]
hosts: *didi-hosts
strip_path: true
preserve_host: true
tags: [product:didi, env:prod]
# ----------------------------------------------------------
# NOTE: didi-admin NOT migrated — internal-only (VPN access to 10.11.10.12 directly).
# Will be revisited after admin nginx is replaced with simpler setup.
# ----------------------------------------------------------
# ----------------------------------------------------------
# NOTE: didi-keycloak service REMOVED 2026-04-29.
# DIDI now uses the external SSO cluster (https://sso.local/realms/didi-clients) directly
# — SPA goes browser→sso.local, no proxy through cluster Kong needed.
# JWT consumer didi-keycloak-users still validates tokens for SSO issuer.
# ----------------------------------------------------------

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,392 @@
_format_version: "3.0"
_transform: true
# Service definitions for new architecture
services:
# Agent Service (NEW - replaces orchestration layer)
- name: agent-api
url: http://didi-agent:18789
retries: 3
connect_timeout: 120000
write_timeout: 660000
read_timeout: 660000
tags:
- agent
- analysis
# Legacy: Orchestrator API Service (to be removed after migration)
- name: orchestrator-api
url: http://orchestrationLayer-orchestrator:8000
retries: 5
connect_timeout: 60000
write_timeout: 60000
read_timeout: 60000
tags:
- orchestrator
- legacy
# Legacy: Analysis Service (to be removed after migration)
- name: analysis-api
url: http://orchestrationLayer-analysis:8004
retries: 3
connect_timeout: 120000
write_timeout: 120000
read_timeout: 120000
tags:
- analysis
- legacy
# didiAI Platform Gateway (external) - disabled, configure via DIDIAI_GATEWAY_URL env var
# - name: didiai-platform
# url: ${DIDIAI_GATEWAY_URL}
# retries: 3
# connect_timeout: 60000
# write_timeout: 60000
# read_timeout: 60000
# tags:
# - didiai
# - external
# Admin Dashboard
- name: admin-dashboard
url: http://didi-admin:80
retries: 3
connect_timeout: 30000
write_timeout: 30000
read_timeout: 30000
tags:
- ui
- dashboard
# Routes
routes:
# ============================================
# Agent Service Routes (NEW)
# ============================================
# Agent Health Check
- name: agent-health
service: agent-api
paths:
- /agent/health
strip_path: true
methods:
- GET
# Agent Status (detailed)
- name: agent-status
service: agent-api
paths:
- /agent/status
strip_path: false
methods:
- GET
# Agent Pipelines List
- name: agent-pipelines
service: agent-api
paths:
- /api/pipelines
strip_path: false
methods:
- GET
# Agent Analysis (non-streaming)
- name: agent-analyze
service: agent-api
paths:
- /api/analyze
strip_path: false
methods:
- POST
# Agent Session Status
- name: agent-sessions
service: agent-api
paths:
- /api/sessions
strip_path: false
methods:
- GET
# Agent File Upload
- name: agent-upload
service: agent-api
paths:
- /api/upload
strip_path: false
methods:
- POST
# Agent Abort Session
- name: agent-abort
service: agent-api
paths:
- /api/abort
strip_path: false
methods:
- POST
# ============================================
# Legacy Orchestrator Routes (to be removed)
# ============================================
# Orchestrator Routes
- name: orchestrator-catalog
service: orchestrator-api
paths:
- /api/v1/catalog
strip_path: false
methods:
- GET
- POST
- PUT
- DELETE
- name: orchestrator-pipelines
service: orchestrator-api
paths:
- /api/v1/pipelines
strip_path: false
methods:
- GET
- POST
- PUT
- DELETE
- PATCH
- name: orchestrator-runs
service: orchestrator-api
paths:
- /api/v1/runs
strip_path: false
methods:
- GET
- POST
- name: orchestrator-health
service: orchestrator-api
paths:
- /orchestrator/health
strip_path: true
methods:
- GET
# Analysis Service Routes
- name: analysis-health
service: analysis-api
paths:
- /analysis/health
strip_path: true
methods:
- GET
- name: analysis-stats
service: analysis-api
paths:
- /analysis/stats
strip_path: true
methods:
- GET
# didiAI Platform Routes (proxied) - disabled, enable when DIDIAI_GATEWAY_URL is configured
# - name: didiai-extractors
# service: didiai-platform
# paths:
# - /didiai/extractors
# strip_path: true
# methods:
# - POST
# - GET
#
# - name: didiai-models
# service: didiai-platform
# paths:
# - /didiai/models
# strip_path: true
# methods:
# - POST
# - GET
#
# - name: didiai-discovery
# service: didiai-platform
# paths:
# - /didiai/discovery
# strip_path: true
# methods:
# - GET
# Admin Dashboard Routes
- name: admin-ui
service: admin-dashboard
paths:
- /admin
strip_path: false
preserve_host: true
- name: admin-api
service: admin-dashboard
paths:
- /admin/api
strip_path: false
# Global Plugins
plugins:
# CORS Configuration
- name: cors
config:
origins:
- "http://localhost:3000"
- "http://localhost:3001"
- "http://localhost:8100"
- "*"
methods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
- PATCH
headers:
- Accept
- Accept-Version
- Content-Length
- Content-MD5
- Content-Type
- Date
- Authorization
- X-Request-ID
exposed_headers:
- X-Auth-Token
- X-Request-ID
credentials: true
max_age: 3600
preflight_continue: false
# Rate Limiting (Global)
- name: rate-limiting
config:
minute: 100
hour: 2000
day: 10000
policy: local
fault_tolerant: true
hide_client_headers: false
limit_by: consumer
# Request ID Tracking
- name: correlation-id
config:
header_name: X-Request-ID
generator: uuid
echo_downstream: true
# Request Size Limiting (100MB for media files)
- name: request-size-limiting
config:
allowed_payload_size: 104857600
size_unit: bytes
require_content_length: false
# Response Headers
- name: response-transformer
config:
add:
headers:
- X-Gateway:DIDI-Kong
- X-API-Version:2.0
remove:
headers:
- Server
- Via
# Consumers for future authentication (consumer_groups not supported in Kong 3.4)
consumers: []
# Upstreams for load balancing (prepared for scaling)
upstreams:
# Agent Service Upstream (NEW)
- name: agent-upstream
algorithm: round-robin
slots: 10000
healthchecks:
active:
concurrency: 5
healthy:
http_statuses:
- 200
interval: 10
successes: 2
http_path: /health
timeout: 10
type: http
unhealthy:
http_failures: 3
http_statuses:
- 429
- 500
- 503
interval: 10
tcp_failures: 3
timeouts: 3
targets:
- target: didi-agent:18789
weight: 100
tags:
- agent
# Legacy: Orchestrator Upstream
- name: orchestrator-upstream
algorithm: round-robin
slots: 10000
healthchecks:
active:
concurrency: 10
healthy:
http_statuses:
- 200
- 302
interval: 5
successes: 3
http_path: /health
timeout: 5
type: http
unhealthy:
http_failures: 3
http_statuses:
- 429
- 500
- 503
interval: 5
tcp_failures: 3
timeouts: 3
targets:
- target: orchestrationLayer-orchestrator:8000
weight: 100
tags:
- orchestrator
- legacy
- name: analysis-upstream
algorithm: least-connections
slots: 10000
healthchecks:
active:
concurrency: 5
healthy:
http_statuses:
- 200
interval: 10
successes: 2
http_path: /health
timeout: 10
type: http
unhealthy:
http_failures: 5
interval: 10
timeouts: 5
targets:
- target: orchestrationLayer-analysis:8004
weight: 100
tags:
- analysis

View file

@ -0,0 +1,21 @@
#!/bin/bash
# Start Kong in the background
/docker-entrypoint.sh kong docker-start &
KONG_PID=$!
# Wait for Kong to be ready
echo "Waiting for Kong to start..."
until kong health; do
sleep 2
done
echo "Kong is ready!"
echo "Configuration should already be imported by migrations container."
# Check current services count
SERVICE_COUNT=$(curl -s http://localhost:8001/services 2>/dev/null | grep -o '"id"' | wc -l)
echo "Current services in Kong: $SERVICE_COUNT"
# Keep Kong running in foreground
wait $KONG_PID