# N'oluyo ile giriş

N'oluyo ile giriş (OAuth2), bir kişinin kendi hesabıyla senin web sitene, masaüstü programına ya da mobil uygulamana açık rızasıyla giriş yapmasını sağlar. Şifresini seninle paylaşmaz; N'oluyo ekranında hangi bilgilerin paylaşılacağını görür, onaylar ve senin uygulamana tek kullanımlık bir kodla döner.

## Bu nedir ve ne zaman gerekir

Geliştirici portalındaki tek bir uygulama hem bot hem de **N'oluyo ile giriş** barındırabilir; ama ikisi tamamen farklı işler yapar:

- **Bir sunucuya bot eklemek istiyorsan** bu sayfaya ihtiyacın yok. Bot ekleme akışı `/bot-ekle/<uygulama_id>` ekranından yürür; ayrıntısı [Bot uygulamaları](https://noluyo.net/gelistirici/bot-uygulamalari.md) rehberinde.
- **Kendi sitene veya uygulamana kullanıcı girişi koymak istiyorsan** doğru yer burasıdır. Bot kullanıcısı oluşturman bile gerekmez; yalnız OAuth2 ayarlarını açman yeterlidir.

## Kişinin gördüğü izin ekranı

Kullanıcıyı `https://noluyo.net/oauth2/authorize` adresine yönlendirdiğinde karşısına N'oluyo'nun izin ekranı çıkar. Bu ekranda:

- Uygulamanın adı, avatarı, doğrulanmış rozeti (doğrulanmamışsa açık uyarı etiketi), gizlilik politikası (`privacy_url`) ve kullanım koşulları (`terms_url`) bağlantıları yer alır.
- İstenen her kapsam (`identify`, `email`, `guilds`) sade cümlelerle listelenir.
- Ekranın altında **"Uygulama şunları asla göremez"** kutusu sabit olarak gösterilir: şifren, iki aşamalı doğrulama kodların, özel mesajların, sunucu mesajların, sesli sohbetlerin ve ödeme bilgilerin paylaşılmaz; uygulama senin adına mesaj atamaz veya sunucuya katılamaz.

Misafir (kayıtsız) hesaplar OAuth2 izni veremez (`403 guest_not_allowed`). Doğrulanmamış bir uygulamaya en çok **100 farklı kullanıcı** giriş yapabilir; 101. kişi `403 oauth_user_cap` uyarısı alır. Sınırı kaldırmak için [Doğrulama](https://noluyo.net/gelistirici/dogrulama.md) başvurusu yapılır.

> **Not:** Kullanıcı dilediği an **Ayarlar → Bağlı uygulamalar** ekranından uygulamanın erişimini tek tıkla kaldırabilir.

## Portal ayarları ve yönlendirme adresleri

`/gelistirici/uygulamalar` sayfasında uygulamanı açıp **N'oluyo ile Giriş (OAuth2)** bölümünü yapılandır:

1. **Yönlendirme adresleri (`redirect_uris`):** En çok **10** adres eklenebilir (her biri en çok 512 karakter). Yetkilendirme isteğindeki `redirect_uri` parametresi buradaki kayıtlardan biriyle **birebir** eşleşmelidir. Joker karakter (`*`), `#fragment`, `kullanici@` bilgisi veya `noluyo.net` / `*.noluyo.net` alan adları kabul edilmez. İzin verilen üç biçim vardır:
   - **Web siteleri:** `https://` ile başlayan tam adres (`https://ornek.site/giris/geri-donus`).
   - **Yerel geliştirme ve masaüstü geri dönüşü:** `http://localhost`, `http://127.0.0.1` veya `http://[::1]`. Port numarası çalışma anında esnektir: portala `http://127.0.0.1/geri-donus` kaydettiğinde çalışma anında `http://127.0.0.1:5173/geri-donus` veya rastgele seçilmiş yerel bir port kabul edilir.
   - **Mobil ve masaüstü özel şema:** Nokta içeren ters alan adı (`com.ornek.uygulama:/oauth/geri-donus`; `net.noluyo` ile başlayamaz).
2. **İstemci türü (`oauth_public`):**
   - **Gizli istemci (`oauth_public: false`, varsayılan):** Kendi arka uç sunucusu olan web siteleri içindir. Portaldan **Secret oluştur / yenile** düğmesiyle (`POST /api/v1/applications/{application_id}/oauth-secret`) bir `client_secret` üret ve sunucundaki `.env` dosyasında sakla. Secret yalnızca üretildiği anda tek sefer gösterilir.
   - **Açık istemci (`oauth_public: true`):** Arka ucu olmayan tek sayfa uygulamalar (SPA), masaüstü uygulamaları ve mobil uygulamalar içindir. Kodu kullanıcının cihazında çalıştığı için `client_secret` kullanılmaz; güvenliği PKCE ve tam yönlendirme adresi sağlar.
3. **Yasal bağlantılar:** İzin ekranında görünmesi için `privacy_url` ve `terms_url` alanlarına `https://` ile başlayan gizlilik politikası ve kullanım koşulları adreslerini ekle.

## Yetkilendirme adresi ve parametreler

Kullanıcıyı tarayıcıda şu adrese yönlendirirsin:

```text
https://noluyo.net/oauth2/authorize?client_id=123456789012345678&redirect_uri=https%3A%2F%2Fornek.site%2Fgiris%2Fgeri-donus&response_type=code&scope=identify+email+guilds&state=RastgeleDurumDegeri&code_challenge=PKCEOzetDegeri&code_challenge_method=S256
```

Her parametrenin tam karşılığı:

| Parametre | Zorunlu | Açıklama |
|---|---|---|
| `client_id` | evet | Uygulamanın snowflake kimliği (`application.id`) |
| `redirect_uri` | evet | Portalda kayıtlı yönlendirme adreslerinden biri (URL kodlu) |
| `response_type` | evet | Her zaman `code` olmalıdır |
| `scope` | evet | Boşlukla ayrılmış kapsam listesi; `identify` zorunludur (`identify`, `identify email`, `identify email guilds`) |
| `state` | evet | Oturumuna bağladığın rastgele tahmin edilemez metin (en az 16 bayt rastgele veri önerilir) |
| `code_challenge` | evet | `code_verifier` değerinin SHA-256 özeti (base64url, `=` dolgusuz) |
| `code_challenge_method` | evet | Her zaman `S256` olmalıdır; `plain` reddedilir |

> **İpucu:** Geliştirici portalındaki **N'oluyo ile Giriş** bölümünde yer alan bağlantı oluşturucu, seçtiğin kapsamlara ve yönlendirme adresine göre hazır test adresini tek tıkla üretir.

## PKCE ve state ne işe yarar

Hem gizli hem de açık istemcilerde **PKCE (`S256`)** ve **`state`** zorunludur:

- **`code_verifier` ve `code_challenge` (PKCE):** Kullanıcıyı yönlendirmeden hemen önce 43–128 karakter uzunluğunda rastgele bir metin (`code_verifier`) üretip kullanıcının oturumunda saklarsın. Bunun SHA-256 özetinin base64url karşılığını (`code_challenge`) yetkilendirme adresine koyarsın. Kullanıcı kodla döndüğünde asıl `code_verifier` değerini `/api/v1/oauth2/token` ucuna gönderirsin. Yolda kodu ele geçiren biri `code_verifier` değerini bilmediği için token alamaz.
- **`state`:** Başka birinin kendi yetkilendirme kodunu senin kullanıcının tarayıcısına enjekte etmesini (CSRF) engeller. Yönlendirmeden önce ürettiğin rastgele `state` değerini HTTP-only çerezde ya da sunucu oturumunda sakla; geri dönüşte gelen `state` birebir eşleşmiyorsa işlemi hemen reddet.

## Beş adımda giriş akışı

Tam çalışan web sitesi örneklerini depodaki `sdks/examples/js/oauth-site` ve `sdks/examples/python/oauth_site` klasörlerinde bulabilirsin.

### 1. PKCE değerlerini üret ve kullanıcıyı yönlendir

```bash tab=curl
# PKCE ve state üretimi (OpenSSL ile örnek):
VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
STATE=$(openssl rand -hex 16)

echo "Oturuma kaydet -> verifier=$VERIFIER state=$STATE"
echo "Tarayıcıda aç -> https://noluyo.net/oauth2/authorize?client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fcb&response_type=code&scope=identify+email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"
```
```js tab=JavaScript
import { createHash, randomBytes } from "node:crypto";

const codeVerifier = randomBytes(32).toString("base64url");
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
const state = randomBytes(16).toString("hex");

// codeVerifier ve state değerlerini kullanıcının sunucu oturumuna kaydet.
const url = new URL("https://noluyo.net/oauth2/authorize");
url.searchParams.set("client_id", process.env.CLIENT_ID);
url.searchParams.set("redirect_uri", "http://127.0.0.1:3000/cb");
url.searchParams.set("response_type", "code");
url.searchParams.set("scope", "identify email");
url.searchParams.set("state", state);
url.searchParams.set("code_challenge", codeChallenge);
url.searchParams.set("code_challenge_method", "S256");
```
```py tab=Python
import base64, hashlib, os, secrets
from urllib.parse import urlencode

code_verifier = secrets.token_urlsafe(32)
digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
state = secrets.token_hex(16)

# code_verifier ve state değerlerini kullanıcının sunucu oturumuna kaydet.
params = urlencode({
    "client_id": os.environ["CLIENT_ID"],
    "redirect_uri": "http://127.0.0.1:3000/cb",
    "response_type": "code",
    "scope": "identify email",
    "state": state,
    "code_challenge": code_challenge,
    "code_challenge_method": "S256",
})
authorize_url = f"https://noluyo.net/oauth2/authorize?{params}"
```

### 2. Geri dönüşü karşıla ve state kontrolü yap

Kullanıcı **İzin ver** düğmesine bastığında tarayıcı `redirect_uri?code=...&state=...` adresine, **Vazgeç** düğmesine bastığında ise `redirect_uri?error=access_denied&state=...` adresine döner. Yetkilendirme kodu **2 dakika** geçerlidir ve **tek kullanımlıktır**.

```bash tab=curl
# Tarayıcının döndüğü adres sorgusundan gelen değerler:
# ?code=nly_ac_...&state=...
# Önce gelen state değerinin 1. adımdaki $STATE ile birebir aynı olduğunu doğrula.
```
```js tab=JavaScript
import { timingSafeEqual } from "node:crypto";

function checkCallback(query, session) {
  const got = Buffer.from(String(query.state ?? ""));
  const expected = Buffer.from(String(session.state ?? ""));
  if (got.length === 0 || got.length !== expected.length || !timingSafeEqual(got, expected)) {
    throw new Error("Geçersiz state");
  }
  if (query.error) {
    throw new Error(`Kullanıcı vazgeçti: ${query.error}`);
  }
  return String(query.code);
}
```
```py tab=Python
import hmac

def check_callback(query: dict[str, str], session: dict[str, str]) -> str:
    got = query.get("state", "")
    expected = session.get("state", "")
    if not got or not hmac.compare_digest(got, expected):
        raise ValueError("Geçersiz state")
    if "error" in query:
        raise ValueError(f"Kullanıcı vazgeçti: {query['error']}")
    return query["code"]
```

### 3. Kodu access token ile değiştir

`POST https://noluyo.net/api/v1/oauth2/token` ucuna **`application/x-www-form-urlencoded`** gövde gönderilir (en çok 8 KiB; JSON gövde gönderirsen `400 invalid_request` alırsın). Gizli istemciler `client_secret` alanını form içinde ya da HTTP Basic başlığında iletir; açık istemciler (`oauth_public: true`) `client_secret` göndermez. Hız sınırı `client_id` ve IP başına dakikada **20** istektir.

```bash tab=curl
curl -X POST "https://noluyo.net/api/v1/oauth2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "code=$CODE" \
  --data-urlencode "redirect_uri=http://127.0.0.1:3000/cb" \
  --data-urlencode "client_id=$CLIENT_ID" \
  --data-urlencode "client_secret=$CLIENT_SECRET" \
  --data-urlencode "code_verifier=$VERIFIER"
```
```js tab=JavaScript
const body = new URLSearchParams({
  grant_type: "authorization_code",
  code,
  redirect_uri: "http://127.0.0.1:3000/cb",
  client_id: process.env.CLIENT_ID,
  client_secret: process.env.CLIENT_SECRET, // Açık istemcide bu satırı çıkar
  code_verifier: session.codeVerifier,
});

const tokenRes = await fetch("https://noluyo.net/api/v1/oauth2/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body,
});
const tokens = await tokenRes.json();
if (!tokenRes.ok) throw new Error(`${tokens.error}: ${tokens.error_description}`);
// tokens: { access_token, token_type: "Bearer", expires_in: 3600, refresh_token, scope }
```
```py tab=Python
import os, aiohttp

async with aiohttp.ClientSession() as http:
    async with http.post(
        "https://noluyo.net/api/v1/oauth2/token",
        data={
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": "http://127.0.0.1:3000/cb",
            "client_id": os.environ["CLIENT_ID"],
            "client_secret": os.environ["CLIENT_SECRET"],  # Açık istemcide çıkar
            "code_verifier": session["code_verifier"],
        },
    ) as resp:
        tokens = await resp.json()
        if resp.status != 200:
            raise RuntimeError(f"{tokens['error']}: {tokens.get('error_description')}")
```

### 4. Kullanıcı profilini ve sunucu listesini oku

`access_token` **1 saat** (`expires_in: 3600`) geçerlidir ve `Authorization: Bearer <access_token>` başlığıyla kullanılır (`Bot` önekiyle değil!). Okuma uçlarının hız sınırı 5 saniyede **30** istektir (`oauth_read`).

```bash tab=curl
curl "https://noluyo.net/api/v1/oauth2/@me" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# "guilds" kapsamı istendiyse kullanıcının üye olduğu sunucular:
curl "https://noluyo.net/api/v1/oauth2/@me/guilds" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```
```js tab=JavaScript
const meRes = await fetch("https://noluyo.net/api/v1/oauth2/@me", {
  headers: { Authorization: `Bearer ${tokens.access_token}` },
});
const me = await meRes.json();
if (!meRes.ok) throw new Error(me.code);
// me.user.id -> kendi veritabanındaki tekil anahtar!
```
```py tab=Python
async with aiohttp.ClientSession() as http:
    async with http.get(
        "https://noluyo.net/api/v1/oauth2/@me",
        headers={"Authorization": f"Bearer {tokens['access_token']}"},
    ) as resp:
        me = await resp.json()
        if resp.status != 200:
            raise RuntimeError(me["code"])
        user_id = me["user"]["id"]
```

### 5. Token yenile (rotasyon) ve çıkışta iptal et

`refresh_token` **30 gün** geçerlidir ve **her yenilemede döner (rotate olur)**: yanıt sana yeni bir `access_token` ve yeni bir `refresh_token` verir; hemen ikisini birden veritabanında güncelle. Eşzamanlı ağ isteklerinin birbirini kırmaması için eski `refresh_token` **30 saniyelik** bir geçiş penceresi boyunca geçerli kalır; 30 saniye geçtikten sonra eski bir `refresh_token` tekrar gönderilirse çalıntı şüphesiyle o iznin **tüm** açık token'ları anında iptal edilir ve `400 invalid_grant` döner.

Kullanıcı senin sitenden çıkış yaptığında ya da hesabını ayırdığında `POST /api/v1/oauth2/revoke` çağrısıyla token'ı iptal et (her zaman `200` döner):

```bash tab=curl
# Yenileme:
curl -X POST "https://noluyo.net/api/v1/oauth2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=refresh_token" \
  --data-urlencode "refresh_token=$REFRESH_TOKEN" \
  --data-urlencode "client_id=$CLIENT_ID" \
  --data-urlencode "client_secret=$CLIENT_SECRET"

# İptal etme:
curl -X POST "https://noluyo.net/api/v1/oauth2/revoke" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "token=$REFRESH_TOKEN" \
  --data-urlencode "client_id=$CLIENT_ID" \
  --data-urlencode "client_secret=$CLIENT_SECRET"
```
```js tab=JavaScript
// Yenileme:
const refreshed = await fetch("https://noluyo.net/api/v1/oauth2/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "refresh_token",
    refresh_token: storedRefreshToken,
    client_id: process.env.CLIENT_ID,
    client_secret: process.env.CLIENT_SECRET,
  }),
}).then((r) => r.json());

// Çıkışta iptal:
await fetch("https://noluyo.net/api/v1/oauth2/revoke", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    token: refreshed.refresh_token,
    client_id: process.env.CLIENT_ID,
    client_secret: process.env.CLIENT_SECRET,
  }),
});
```
```py tab=Python
async with aiohttp.ClientSession() as http:
    # Yenileme:
    async with http.post(
        "https://noluyo.net/api/v1/oauth2/token",
        data={
            "grant_type": "refresh_token",
            "refresh_token": stored_refresh_token,
            "client_id": os.environ["CLIENT_ID"],
            "client_secret": os.environ["CLIENT_SECRET"],
        },
    ) as resp:
        refreshed = await resp.json()

    # Çıkışta iptal:
    await http.post(
        "https://noluyo.net/api/v1/oauth2/revoke",
        data={
            "token": refreshed["refresh_token"],
            "client_id": os.environ["CLIENT_ID"],
            "client_secret": os.environ["CLIENT_SECRET"],
        },
    )
```

## Kapsamlar ve dönen alanlar

N'oluyo kasıtlı olarak yalnızca üç salt-okunur kapsam sunar; mesaj okuma, mesaj yazma ya da sunucuya sokma kapsamı yoktur:

| Kapsam | Zorunlu | Döndürdüğü uç ve alanlar |
|---|---|---|
| `identify` | evet | `GET /api/v1/oauth2/@me` içinde `application: {id, name}`, `scopes`, `expires_at` ve `user: {id, username, display_name, avatar}` |
| `email` | hayır | `GET /api/v1/oauth2/@me` içindeki `user` nesnesine `email` (`string` veya `null`) ve `email_verified` (`boolean`) ekler |
| `guilds` | hayır | `GET /api/v1/oauth2/@me/guilds` ucunu açar; her sunucu için `{id, name, icon, owner, permissions}` dizisi döndürür (`permissions` dizgi olarak izin bit maskesidir) |

> **Uyarı:** Kendi veritabanında kullanıcıyı **her zaman `user.id` (snowflake)** ile eşleştir; asla `username` ya da `email` alanını birincil anahtar yapma. Kullanıcı adını veya e-postasını değiştirebilir. Ayrıca `email` kapsamı verilmiş olsa bile kullanıcının e-postası doğrulanmamışsa (`email_verified: false`), `user.email` alanı **`null`** gelir! E-postası doğrulanmamış iki hesabı otomatik birleştirme.

## Açık istemciler ve CORS

Tarayıcıda çalışan tek sayfa uygulamalar (SPA), Electron/Tauri masaüstü programları ve mobil uygulamalar kaynak kodunda gizli anahtar saklayamaz. Bu uygulamalarda:

1. Portalda **Açık istemci (`oauth_public`)** seçeneğini aç ve istemci koduna hiçbir zaman `client_secret` gömme.
2. `/api/v1/oauth2/token`, `/api/v1/oauth2/revoke`, `/api/v1/oauth2/@me` ve `/api/v1/oauth2/@me/guilds` uçları `Access-Control-Allow-Origin: *` (çerezsiz CORS) başlığıyla tarayıcıdan doğrudan çağrılabilir.
3. Tarayıcı uygulamasında `access_token` değerini `localStorage` yerine bellek içinde tut; masaüstü ve mobilde işletim sisteminin güvenli anahtar zincirini (Keychain / Keystore / Credential Locker) kullan.

## Hata tablosu

`/api/v1/oauth2/token` ve `/revoke` uçları RFC 6749 biçiminde `{"error": "...", "error_description": "..."}` döndürür; diğer tüm uçlar standart `{"code": "...", "message": "..."}` zarfını kullanır:

| Durum | Kod / `error` | Neden ve çözüm |
|---|---|---|
| 400 | `invalid_request` | Form kodlaması (`application/x-www-form-urlencoded`) kullanılmadı, PKCE `S256` eksik ya da zorunlu alan boş. |
| 401 | `invalid_client` | `client_id` veya `client_secret` yanlış, ya da gizli istemcide secret gönderilmedi. |
| 400 | `invalid_grant` | Yetkilendirme kodunun 2 dakikalık süresi doldu, kod ikinci kez kullanıldı, `redirect_uri` veya `code_verifier` eşleşmedi ya da 30 saniyeyi geçmiş eski bir `refresh_token` tekrar gönderildi. Kullanıcıdan yeniden giriş iste. |
| 400 | `unsupported_grant_type` | `grant_type` alanı `authorization_code` veya `refresh_token` dışında bir değer. |
| 400 | `invalid_scope` | İstenen kapsam tanınmıyor veya `identify` eklenmemiş. |
| 400 | `oauth_unknown_client` | Yetkilendirme ekranındaki `client_id` bulunamadı veya uygulama durdurulmuş. |
| 400 | `oauth_bad_redirect` | `redirect_uri` portalda kayıtlı adreslerle eşleşmiyor. Portaldaki listeyi kontrol et. |
| 400 | `oauth_bad_request` | `response_type=code`, `state`, `code_challenge` veya `code_challenge_method=S256` eksik/geçersiz. |
| 403 | `oauth_user_cap` | Doğrulanmamış uygulamanın 100 kullanıcı sınırı doldu. [Doğrulama](https://noluyo.net/gelistirici/dogrulama.md) başvurusu yap. |
| 403 | `guest_not_allowed` | Misafir hesapla giriş denendi; kullanıcının kalıcı hesap açması gerekir. |
| 401 | `invalid_oauth_token` | `access_token` süresi doldu, iptal edildi ya da kullanıcı şifresini değiştirdi. `refresh_token` ile yenile; o da reddedilirse yeniden giriş ekranına yönlendir. |
| 403 | `insufficient_scope` | `/oauth2/@me/guilds` çağrıldı ama izin alınırken `guilds` kapsamı istenmemişti. |

## Oturum iptali, şifre değişimi ve hesap silme

Token yaşam döngüsü kullanıcının hesap güvenliği hareketlerini anında izler:

- **Kullanıcı erişimi kaldırdığında:** Kullanıcı **Ayarlar → Bağlı uygulamalar** ekranından uygulamanı kaldırdığında tüm `access_token` ve `refresh_token` kayıtları anında silinir; bir sonraki `@me` çağrısı `401 invalid_oauth_token` döndürür.
- **Şifre değişimi veya sıfırlama:** Kullanıcı şifresini değiştirdiğinde ya da sıfırladığında açık `access_token` ve `refresh_token` değerleri iptal olur (uygulama Bağlı uygulamalar listesinde kalır; kullanıcı sitene geldiğinde yeniden giriş yapar). "Diğer cihazlardan çık" düğmesi ve iki aşamalı doğrulama değişiklikleri OAuth2 token'larını düşürmez.
- **Hesap veya uygulama kapanışı:** Kullanıcı N'oluyo hesabını sildiğinde/dondurduğunda ya da sen uygulamayı sildiğinde/durdurduğunda tüm token'lar derhal geçersiz kalır.

## Güvenlik listesi ve yasal yükümlülüklerin

Yayına çıkmadan önce şu maddeleri doğrula:

- `client_secret` hiçbir zaman tarayıcı koduna, mobil pakete veya Git deposuna girmez; yalnız arka uç `.env` dosyasında durur.
- Her giriş isteğinde yeni bir `state` ve `code_verifier` üretilir ve geri dönüşte `state` doğrulanmadan kod takas edilmez.
- Portalda yalnızca gerçekten kullandığın tam `redirect_uri` adresleri kayıtlıdır.
- Kullanıcı kimliği yalnızca `user.id` üzerinden tutulur ve `user.email === null` durumu hatasız ele alınır.
- **Yasal sorumluluk:** N'oluyo ile giriş üzerinden kendi sistemine çektiğin veriler için kendi kullanıcılarına karşı bağımsız veri sorumlusun. Portalda doğru bir gizlilik politikası (`privacy_url`) yayınlamalı ve sitende hesabını silen kişinin senden tutulmasını istemediği OAuth2 token'larını `/oauth2/revoke` ile iptal edip verilerini silmelisin.

## Bu sayfayı yapay zekaya verirsen

> **İpucu:** Bir yapay zeka aracına "N'oluyo ile giriş" özelliği yazdırırken bu sayfayı ver ve şu kuralları özellikle vurgula:
>
> - Yetkilendirme ekranı `https://noluyo.net/oauth2/authorize`, API uçları ise `https://noluyo.net/api/v1/oauth2/token`, `/revoke`, `/@me` ve `/@me/guilds` adresindedir.
> - PKCE (`code_challenge_method=S256`) ve `state` hem gizli hem açık istemcilerde zorunludur.
> - `/api/v1/oauth2/token` ve `/revoke` uçları JSON değil, yalnızca `application/x-www-form-urlencoded` kabul eder.
> - `@me` çağrılarında `Authorization: Bearer <access_token>` kullanılır (`Bot` değil).
> - Kullanıcılar veritabanında `user.id` ile tutulur; `user.email` alanı `null` dönebilir.

## Sırada ne var

- [Doğrulama](https://noluyo.net/gelistirici/dogrulama.md): 100 kullanıcı sınırını kaldırmak için doğrulama başvurusu
- [Güvenlik](https://noluyo.net/gelistirici/guvenlik.md): secret ve token saklama kuralları
- [Hatalar](https://noluyo.net/gelistirici/hatalar.md): tüm HTTP durum ve hata kodlarının tam listesi
