Skip to main content
Projede iki bağımsız React SPA vardır. İkisi de aynı stack üzerine kuruludur ama ayrı Node.js projeleri, ayrı Dockerfile’lar ve ayrı Keycloak realm’leridir.
Frontend React’tır, Vue değil. Her iki SPA da React 19 + Vite 6 + TypeScript 5.8 kullanır.
SPAKlasörDev portKeycloak realmClient
Website (vatandaş)src/DiyanetCleanArchitecture.Presentation.Website3000diyanet-vatandas-dev-realmdiyanet-website
Admin (backoffice)src/DiyanetCleanArchitecture.Presentation.Admin3001diyanet-yonetim-dev-realmdiyanet-admin

Teknoloji yığını

  • React 19 + react-dom 19, Vite 6, TypeScript 5.8
  • react-router-dom 7 — routing (Admin’de basename import.meta.env.BASE_URL’den okunur)
  • zustand 5 — global state (store/)
  • @tanstack/react-query 5 — server state / cache
  • axios 1.8 — HTTP client (lib/axios.ts)
  • keycloak-js 26 — SSO / OAuth2 + PKCE
  • Radix UI primitive’leri + Tailwind CSS 3 + class-variance-authority + tailwind-merge
  • lucide-react (ikon), sonner (toast), recharts (grafik, Admin), @tanstack/react-table (tablo, Admin)

Klasör yapısı

src/
├── pages/        # route bileşenleri
├── components/   # paylaşılan UI (Radix + Tailwind)
├── lib/          # axios.ts, keycloak.ts, api-client.ts (NSwag üretimi)
├── store/        # zustand store'ları (auth.store.ts, me.store.ts)
├── router/       # react-router yapılandırması (basename)
└── api/          # endpoint sarmalayıcıları

lib/axios.ts — merkezi HTTP client

Tek bir Axios instance (baseURL = import.meta.env.VITE_API_BASE_URL, timeout: 30s) iki interceptor ile gelir. Request interceptor — her isteğe X-App-Id header’ı (VITE_APP_ID) ve Keycloak Bearer token’ı ekler. Token 30 saniyeden kısa sürede expire olacaksa otomatik yeniler:
if (keycloak.authenticated) {
  await keycloak.updateToken(30)              // <30s kaldıysa yenile
  config.headers.Authorization = `Bearer ${keycloak.token}`
}
Refresh başarısız olursa safeKeycloakLogin() çağrılır — bu, 10 saniyelik throttle ile redirect-loop’u önler (yanlış audience/issuer durumunda login → SSO → 401 → login döngüsüne girmeyi engeller). Response interceptor — RFC 7807 (application/problem+json) hatalarını yakalar, status’a göre toast gösterir ve ProblemDetailsError fırlatır:
  • 401 — token reddedildi. Otomatik redirect YOK (loop riski); kullanıcıya “Giriş Yap” action butonlu toast.
  • 403 — ProblemDetails’taki reason alanı not_invited veya unknown_realm ise (kullanıcı Keycloak’a girdi ama bu app’te davetli/tanımlı değil) “Çıkış Yap” önerili özel mesaj gösterilir. Diğer 403’ler “yetkiniz yok” toast’ı.
  • 422errors alanı varsa toast atılmaz (component inline gösterir).
  • 400 / 404 / 409 / 429 / 5xx — duruma özel toast’lar.
Tüm hatalar bir diagnostic buffer’a (supportContext) kaydedilir — destek modalı snapshot’ında traceId ile görünür.

store/auth.store.ts — Keycloak oturum yönetimi

zustand store, uygulama açılırken bir kez initialize() çalıştırır:
const authenticated = await keycloak.init({
  onLoad: 'check-sso',                                       // mevcut SSO oturumunu kontrol et
  silentCheckSsoRedirectUri: `${APP_BASE_URL}silent-check-sso.html`,
  pkceMethod: 'S256',
  checkLoginIframe: false,                                   // token'lar memory'de
})
Authenticated ise:
  1. provisionKeycloakSession(keycloak.token) — backend’e token gönderilir; kullanıcı local DB’ye eşlenir (davetli ise Draft/Pending → Active). Bu çağrı olmadan korumalı her endpoint 403 döner (ActiveAccountAuthorizationHandler).
  2. useMeStore.getState().bootstrap()/me çekilir, tüm sayfalar (avatar, profil, role-bazlı menü) buradan okur.
Provision 403 dönerse accessDeniedReason ayarlanır: not_invited / account_blocked / unknown_realm / provision_error / unknown. Roller, hem realm_access.roles hem resource_access[clientId].roles’ten birleştirilir (SuperAdmin/Admin/Staff client-level gelir).

VITE_* ortam değişkenleri

Vite, VITE_ öneki ile başlayan env’leri build sırasında bundle’a gömer:
DeğişkenAçıklama
VITE_API_BASE_URLAPI adresi (dev: http://localhost:5005; stage/prod: boş — aynı origin /api/)
VITE_KEYCLOAK_URLTarayıcının ulaşacağı Keycloak URL’i (external)
VITE_KEYCLOAK_REALMRealm adı
VITE_KEYCLOAK_CLIENT_IDClient ID
VITE_APP_IDX-App-Id header’ı (audit/proje izleme)
VITE_ADMIN_ROLESAdmin erişimi için izinli roller (Admin SPA, örn. Admin,SuperAdmin)
VITE_BASE_PATHSPA base path (Admin) — dev /, stage/prod /admin/

Dockerfile (iki aşamalı)

node:22-alpine ile build, nginx:1.27-alpine ile serve:
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ARG VITE_API_BASE_URL
ARG VITE_BASE_PATH=/          # Admin: stage/prod compose '/admin/' geçer
RUN npm run build

FROM nginx:1.27-alpine AS runtime
COPY --from=builder /app/dist /usr/share/nginx/html
# Admin: nginx.conf.template runtime'da BASE_PATH ile envsubst'lanır
EXPOSE 80
Admin SPA prod’da /admin/ altında serve edilir. VITE_BASE_PATH=/admin/ ile build edilir; vite.config.ts bunu base olarak okur ve nginx BASE_PATH env’iyle template’i render eder. Tüm Keycloak redirect URI’ları origin + base path kullanır — aksi halde logout sonrası kullanıcı website’a düşer ve silent-check-sso.html 404 olur.

npm script’leri

npm run dev           # Vite dev server (Website 3000 / Admin 3001), /api proxy → localhost:5005
npm run build         # tsc -b && vite build → dist/
npm run preview       # build çıktısını serve et
npm run generate-api  # NSwag ile typed Axios client üret (API çalışırken)

Sonraki adımlar

OpenAPI / client üretimi

npm run generate-api detayları.

Controller'lar

SPA’ların çağırdığı endpoint aileleri.

Keycloak ortamları

Realm ve client yapılandırması.

Dev Docker

SPA’ları container’da çalıştırma.