# Kodexempel

Fyra funktioner räcker för en komplett integration. Alla värden nedan är platshållare — byt ut dem mot era egna via miljövariabler, aldrig i koden.

**Konfiguration**

```bash
BASE_URL=https://staging.zesec.com/api
KIION_PHONE_NUMBER={{phoneNumber}}
KIION_PASSWORD={{password}}
```

> **Inga uppgifter i kod** — `{{phoneNumber}}` och `{{password}}` hör hemma i er hemlighetshantering. Checka aldrig in dem, och logga aldrig tokens.

## 1. login

Körs en gång vid uppstart, och som fallback när en refresh misslyckats. Aldrig per öppning.

_cURL_

```bash
curl -X POST https://staging.zesec.com/api/auth/login-by-phone \
  -H "Content-Type: application/json" \
  -d '{"phoneNumber": "{{phoneNumber}}", "password": "{{password}}"}'
```

_Node / TypeScript_

```typescript
const res = await fetch(`${BASE_URL}/auth/login-by-phone`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ phoneNumber: PHONE_NUMBER, password: PASSWORD }),
});
if (!res.ok) throw new Error(`login failed: ${res.status}`);
const { AccessToken, RefreshToken } = await res.json();
```

_Python_

```python
import requests

res = requests.post(
    f"{BASE_URL}/auth/login-by-phone",
    json={"phoneNumber": PHONE_NUMBER, "password": PASSWORD},
    timeout=10,
)
res.raise_for_status()
tokens = res.json()  # {"AccessToken": "...", "RefreshToken": "..."}
```

## 2. getValidAccessToken

Proaktiv förnyelse var 36:e timme, single-flight-lås och fallback till ominloggning. All annan kod anropar den här, aldrig `login` direkt.

_Node / TypeScript — getValidAccessToken_

```typescript
const PROACTIVE_REFRESH_MS = 36 * 60 * 60 * 1000;

let inFlight: Promise<string> | null = null;

/** Single active session per account: one process, one token pair, one refresh at a time. */
export async function getValidAccessToken(): Promise<string> {
  const current = await session.read();

  if (current && Date.now() - current.rotatedAt < PROACTIVE_REFRESH_MS) {
    return current.accessToken;
  }

  // Single-flight guard: concurrent callers must not each burn a single-use refresh token.
  inFlight ??= (async () => {
    try {
      return current ? await rotate(current.refreshToken) : await login();
    } catch (err) {
      if (err instanceof RefreshFailed) return await login(); // refresh token spent or expired
      throw err;
    } finally {
      inFlight = null;
    }
  })();

  return inFlight;
}

/** Wrap every API call: one refresh, one replay, then surface the failure. */
export async function apiCall(path: string, init: RequestInit = {}) {
  let token = await getValidAccessToken();
  let res = await fetch(`${BASE_URL}${path}`, withAuth(init, token));

  if (res.status === 401) {
    await session.invalidateAccessToken();
    token = await getValidAccessToken();
    res = await fetch(`${BASE_URL}${path}`, withAuth(init, token));
  }
  return res; // still 401 -> stop and alert an operator; do not loop
}
```

_Python — get_valid_access_token_

```python
import threading, time

_lock = threading.Lock()
PROACTIVE_REFRESH_S = 36 * 3600


def get_valid_access_token() -> str:
    """One session per account: one lock, one refresh at a time."""
    with _lock:
        current = session.read()
        if current and time.time() - current["rotated_at"] < PROACTIVE_REFRESH_S:
            return current["access_token"]
        try:
            return rotate(current["refresh_token"]) if current else login()
        except RefreshFailed:
            return login()  # refresh token spent or expired -> full re-login
```

**Rotationen som sparar båda tokens**

```typescript
async function rotate(currentRefreshToken: string) {
  const res = await fetch(`${BASE_URL}/auth/refresh`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ refreshToken: currentRefreshToken }),
  });
  if (!res.ok) throw new RefreshFailed(res.status);

  const { AccessToken, RefreshToken } = await res.json();
  // One write. Never persist the access token without the refresh token that came with it:
  // the token you sent is already dead, so a partial save loses the session for good.
  await session.replace({ accessToken: AccessToken, refreshToken: RefreshToken, rotatedAt: Date.now() });
  return AccessToken;
}
```

**Refresh-anropet i cURL**

```bash
curl -X POST https://staging.zesec.com/api/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refreshToken": "<RefreshToken>"}'
```

## 3. syncDevices

Hämtar enhetslistan och skriver om mappningen. Körs på schema — inte i öppningsögonblicket.

_cURL_

```bash
curl https://staging.zesec.com/api/user/device \
  -H "Authorization: Bearer <AccessToken>"
```

_Node / TypeScript_

```typescript
const res = await fetch(`${BASE_URL}/user/device`, {
  headers: { Authorization: `Bearer ${accessToken}` },
});
const devices = await res.json();
// Persist the mapping: your internal door key -> device.id
const mapping = new Map(devices.map((d) => [d.name, d.id]));
```

_Python_

```python
res = requests.get(
    f"{BASE_URL}/user/device",
    headers={"Authorization": f"Bearer {access_token}"},
    timeout=10,
)
res.raise_for_status()
mapping = {d["name"]: d["id"] for d in res.json()}
```

## 4. unlockDoor

Ett anrop, med ett `{deviceId}` från den cachelagrade mappningen.

_cURL_

```bash
curl -X PUT https://staging.zesec.com/api/device/{deviceId}/unlock \
  -H "Authorization: Bearer <AccessToken>"
```

_Node / TypeScript_

```typescript
const res = await fetch(`${BASE_URL}/device/${deviceId}/unlock`, {
  method: "PUT",
  headers: { Authorization: `Bearer ${accessToken}` },
});
// 200 means the unlock command was accepted and forwarded to the device.
if (!res.ok) throw new UnlockFailed(res.status);
```

_Python_

```python
res = requests.put(
    f"{BASE_URL}/device/{device_id}/unlock",
    headers={"Authorization": f"Bearer {access_token}"},
    timeout=10,
)
res.raise_for_status()  # do not blind-retry: see the limits page
```

_Python — unlock_door_

```python
def unlock_door(device_id: int) -> bool:
    res = api_call("PUT", f"/device/{device_id}/unlock")
    if res.status_code == 403:
        raise NoKeyForDevice(device_id)
    if res.status_code == 404:
        raise UnknownDevice(device_id)   # re-sync the mapping
    res.raise_for_status()
    return True  # command accepted; never retried blindly
```

## Postman-samling

Ladda ner [KiiOn-samlingen för Postman](/kiion-postman-collection.json). Den innehåller de fyra anropen, en `BASE_URL`-variabel och ett testskript som sparar tokenparet efter login och refresh.

- Samlingen är skriven av KiiOn för den publika API-ytan — den innehåller inga partner- eller kundspecifika anrop.
- Variablerna `phoneNumber`, `password`, `AccessToken`, `RefreshToken` och `deviceId` levereras tomma.
- Lägg era uppgifter i en Postman-**miljö** med tomt *initial value*, så följer de inte med vid export.

**Vidare**
- [Golden path — var funktionerna hör hemma i arkitekturen](/docs/golden-path)
- [Statuskoder & felsvar](/docs/errors)
- [Bygga en klient — generera en typad klient ur specen](/docs/libraries)
