> ## Documentation Index
> Fetch the complete documentation index at: https://docs.diyanet.pro/llms.txt
> Use this file to discover all available pages before exploring further.

# Personel Girişi

> Admin SPA → Keycloak (OIDC + PKCE) → JwtBearer doğrulama → davet bazlı provisioning → permission policy → GetAdminMeQuery.

Personel (yönetim paneli) girişi vatandaş akışından farklıdır: OTP/SMS yoktur, kimlik doğrulama **Keycloak** üzerinden OIDC ile yapılır. API token'ı kendisi üretmez; Keycloak'ın imzaladığı access token'ı JWKS ile doğrular. İlk girişte kullanıcı **davet bazlı** olarak lokal DB'ye eşlenir — daveti olmayan kişi `403 not_invited` alır.

<Info>
  İlgili dosyalar:
  `src/DiyanetCleanArchitecture.API/Controllers/AuthController.cs` (Keycloak akışları) ·
  `Application/Features/Authentication/Admin/Commands/ProvisionKeycloakUser/*` ·
  `API/SeedWork/Authorization/ActiveAccountAuthorizationHandler.cs` ·
  `Application/Features/Users/Admin/Queries/GetAdminMe/*`
</Info>

## Realm ve client

|                    | Realm (dev)                  | Client            | Token süresi |
| ------------------ | ---------------------------- | ----------------- | ------------ |
| Personel (yönetim) | `diyanet-yonetim-dev-realm`  | `diyanet-admin`   | **5 dk**     |
| Vatandaş (website) | `diyanet-vatandas-dev-realm` | `diyanet-website` | 2 saat       |

Admin SPA, `keycloak-js` ile **Authorization Code + PKCE (S256)** akışını kullanır. 5 dakikalık kısa access token, `keycloak-js`'in sessiz (silent) refresh mekanizmasıyla yenilenir.

## Sequence — ilk personel girişi

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant U as Personel
    participant A as Admin SPA (keycloak-js)
    participant KC as Keycloak (yonetim realm)
    participant C as AuthController
    participant V as IKeycloakTokenValidator (JWKS)
    participant P as ProvisionKeycloakUserCommandHandler
    participant DB as PostgreSQL

    U->>A: Giriş
    A->>KC: Authorization Code + PKCE (S256)
    KC-->>A: access_token (5 dk) + refresh_token
    A->>C: POST /api/auth/keycloak/session { accessToken, ... }
    C->>V: ValidateAsync(accessToken)  (JWKS, offline)
    V-->>C: KeycloakTokenPayload (sub, iss, email, ...)
    C->>C: HttpOnly cookie kc_token + refresh_token set
    C->>P: _mediator.Send(ProvisionKeycloakUserCommand)
    P->>P: ResolveRealmKind(iss) == Personel ?
    P->>DB: sub ile UserByIdentityProviderSpecification
    alt sub linkli kullanıcı var
        P->>DB: AddOrUpdateKeycloakLogin + ActivateOnExternalLogin
    else email ile davet eşleşmesi
        P->>DB: byEmail.AddOrUpdateKeycloakLogin + Activate
    else eşleşme yok
        P-->>C: ExternalLoginNotAllowedException("not_invited")
    end
    P-->>C: wasFirstLogin
    alt not_invited / blocked
        C->>C: cookie temizle
        C-->>A: 403 { reason }
    else
        C-->>A: 200 { wasFirstLogin }
    end

    Note over A,C: Sonraki istekler kc_token cookie ile
    A->>C: GET /api/admin/me  (Cookie: kc_token)
    C->>C: JwtBearer "Personel" şeması: imza + claims
    C->>C: ActiveAccountAuthorizationHandler + permission policy
    C-->>A: 200 GetAdminMeQuery sonucu
```

## Adım adım

<Steps>
  <Step title="SPA → Keycloak (PKCE)">
    Admin SPA `keycloak-js` ile Authorization Code + PKCE akışını başlatır. Kullanıcı Keycloak login formunda kimlik doğrular; SPA `access_token` (5 dk) ve `refresh_token` alır. API bu adımda devrede değildir.
  </Step>

  <Step title="Session — token doğrulama + cookie">
    SPA token'ı `keycloak/session` endpoint'ine gönderir. API token'ı **JWKS ile offline** doğrular (Keycloak'a gidilmez) ve HTTP-only cookie set eder:

    ```csharp theme={null}
    [AllowAnonymous]
    [HttpPost("keycloak/session")]
    public async Task<IActionResult> KeycloakSession([FromBody] KeycloakSessionRequest request, CancellationToken ct)
    {
        var payload = await _keycloakValidator.ValidateAsync(request.AccessToken, ct); // JWKS
        Response.AppendKcToken(request.AccessToken, accessExpiry, SecureCookie);
        if (!string.IsNullOrEmpty(request.RefreshToken))
            Response.AppendRefreshToken(request.RefreshToken, refreshExpiry, SecureCookie);
        // → provisioning (sonraki adım)
    }
    ```

    <Note>
      Raw token cookie'ye gömülür; React bir daha token'ı header'a koymaz, tüm istekler `kc_token` cookie'siyle gider (BFF deseni). `SecureCookie` dev'de `false`, prod'da `true`'dur.
    </Note>
  </Step>

  <Step title="Provisioning — davet bazlı eşleme">
    Doğrulanan payload `ProvisionKeycloakUserCommand`'a dönüştürülür. Handler **yalnızca Personel realm** için çalışır (`ResolveRealmKind(iss)`); başka realm gelirse `unknown_realm` ile reddeder. Sonra üç olasılık denenir, hepsi tek transaction içinde:

    ```csharp theme={null}
    // 1) sub ile mevcut Keycloak linki var mı?
    var bySub = await _userRepository.FirstOrDefaultAsync(
        new UserByIdentityProviderSpecification(AuthProviderType.Keycloak, request.Subject), ct);
    if (bySub is not null) {
        if (bySub.IsBlocked) throw new ExternalLoginNotAllowedException("...", "account_blocked");
        bySub.AddOrUpdateKeycloakLogin(request.Subject, displayName);
        wasFirstLogin = bySub.ActivateOnExternalLogin();   // Draft/Pending → Active
        // ... save + commit
    }
    // 2) email ile önceden davet edilmiş (pre-created) kullanıcı?
    //    byEmail.AddOrUpdateKeycloakLogin + ActivateOnExternalLogin
    // 3) hiçbir eşleşme yok → reddet
    throw new ExternalLoginNotAllowedException(
        "Yönetim paneli erişim yetkiniz bulunmuyor. ...", "not_invited");
    ```

    `User.ActivateOnExternalLogin()` idempotenttir: `Draft`/`Pending` ise `Active`'e geçirip `true` döner, zaten `Active` ise `false`. `Banned`/`Suspended` kullanıcılara dokunmaz; login zaten reddedilir.
  </Step>

  <Step title="403 senaryoları — cookie temizliği">
    Provisioning bir `ExternalLoginNotAllowedException` (`not_invited` / `account_blocked` / `unknown_realm`) ya da beklenmeyen hata fırlatırsa controller **cookie'leri temizler ve `403` döner** — daveti olmayan kullanıcının dashboard'a düşmesi engellenir:

    ```csharp theme={null}
    catch (ExternalLoginNotAllowedException ex)
    {
        Response.DeleteKcToken();
        Response.DeleteRefreshToken();
        return StatusCode(StatusCodes.Status403Forbidden, new { message = ex.Message, reason = ex.Reason });
    }
    ```

    Frontend bu `reason` koduna göre `AccessDeniedScreen` gösterir.

    <Warning>
      Concurrency notu: SPA aynı login'de iki paralel provision tetikleyebilir (`keycloak/session`'ın `mediator.Send`'i + `JwtBearer.OnTokenValidated` hook'u). İkisi de aynı `(provider_type_id, provider_user_id)` için INSERT dener; biri unique constraint (Postgres `23505`) alır. Handler bu durumu **idempotent başarı** sayar ve `false` döner — UI `403` görmez.
    </Warning>
  </Step>

  <Step title="Yetkili istekler — JwtBearer Personel şeması">
    Bundan sonraki tüm istekler `kc_token` cookie'siyle gelir. API'de JwtBearer "Personel" şeması token'ı doğrular ve claim'leri okur: `permissions` (multivalued), `organization_id` / `tenant_id`, `accountStatus`. Otorizasyon kararı **her zaman lokal DB'deki rol + tenant'a** dayanır; Keycloak'ın `ClientRoles`'u yetkiye dönüşmez (yalnızca audit/debug için loglanır).
  </Step>

  <Step title="Authorization — aktif hesap + permission policy">
    İki kademe çalışır. Önce `ActiveAccountAuthorizationHandler` hesabın `accountStatus`'unun aktif olduğunu kontrol eder; sonra endpoint'in `[Authorize(Policy = "...")]` permission policy'si `permissions` claim'iyle eşleştirilir. Pipeline'da Application tarafında ayrıca `PermissionPipelineBehavior` çalışır. Bkz. [Authorization](/security/authorization).
  </Step>

  <Step title="Profil — GetAdminMeQuery">
    SPA oturum sonrası kullanıcı profilini çeker:

    ```text theme={null}
    GET /api/admin/me  →  GetAdminMeQuery  →  GetAdminMeQueryHandler
    ```

    Handler oturum açan kullanıcının kimliğini, rollerini ve izinlerini döner; SPA menüyü/yetkileri buna göre çizer.
  </Step>
</Steps>

## Token yenileme ve çıkış

* **Refresh:** 5 dakikalık kısa token nedeniyle `keycloak/refresh` sık çağrılır. API refresh token'la Keycloak'tan yeni token alır, cookie'leri günceller ve `IUserContextProvider.InvalidateAsync(sub)` ile permission cache'ini düşürür (yetkiler değişmiş olabilir).
* **Logout:** `POST /api/auth/logout` cookie'leri siler. Keycloak oturumunu da bitirmek için frontend ayrıca Keycloak `end_session` URL'ini açmalıdır.

## Hata senaryoları

| Durum                                | reason            | HTTP                                          |
| ------------------------------------ | ----------------- | --------------------------------------------- |
| Davet yok (eşleşme bulunamadı)       | `not_invited`     | `403`                                         |
| Banned / Suspended hesap             | `account_blocked` | `403`                                         |
| Bilinmeyen / yanlış realm            | `unknown_realm`   | `403`                                         |
| Provision sırasında beklenmeyen hata | `provision_error` | `403` (güvenli varsayılan: erişim reddedilir) |
| Geçersiz Keycloak token              | —                 | `401`                                         |

## İlgili

<CardGroup cols={2}>
  <Card title="Keycloak Provisioning" icon="user-gear" href="/keycloak/provisioning">
    Realm/client/user otomatik kurulumu (RunOnce).
  </Card>

  <Card title="Authorization" icon="lock" href="/security/authorization">
    Permission policy ve aktif hesap kontrolü.
  </Card>

  <Card title="Keycloak Ortamları" icon="server" href="/keycloak/environments">
    Dev/stage/prod realm farkları.
  </Card>

  <Card title="OTP / TOTP Akışı" icon="key" href="/playbooks/otp-flow">
    Vatandaş tarafı yerel kimlik akışı.
  </Card>
</CardGroup>
