livrare lot 2
This commit is contained in:
commit
8ecc78e729
763 changed files with 164593 additions and 0 deletions
263
backend/services/gateway-auth-layer/README.md
Normal file
263
backend/services/gateway-auth-layer/README.md
Normal 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`
|
||||
362
backend/services/gateway-auth-layer/didiKeycloak/INDEX.md
Normal file
362
backend/services/gateway-auth-layer/didiKeycloak/INDEX.md
Normal 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.
|
||||
203
backend/services/gateway-auth-layer/didiKeycloak/MIGRATION.md
Normal file
203
backend/services/gateway-auth-layer/didiKeycloak/MIGRATION.md
Normal 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.
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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;">© 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>
|
||||
|
|
@ -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;">© 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>
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
parent=base
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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...
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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';
|
||||
});
|
||||
|
|
@ -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.
|
||||
31
backend/services/gateway-auth-layer/didiKong/Dockerfile
Normal file
31
backend/services/gateway-auth-layer/didiKong/Dockerfile
Normal 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"]
|
||||
208
backend/services/gateway-auth-layer/didiKong/INDEX.md
Normal file
208
backend/services/gateway-auth-layer/didiKong/INDEX.md
Normal 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.
|
||||
121
backend/services/gateway-auth-layer/didiKong/MIGRATION.md
Normal file
121
backend/services/gateway-auth-layer/didiKong/MIGRATION.md
Normal 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
|
||||
|
|
@ -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
1829
backend/services/gateway-auth-layer/didiKong/declarative/kong.yml
Normal file
1829
backend/services/gateway-auth-layer/didiKong/declarative/kong.yml
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
21
backend/services/gateway-auth-layer/didiKong/entrypoint.sh
Normal file
21
backend/services/gateway-auth-layer/didiKong/entrypoint.sh
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue