Golden path & rekommenderad arkitektur

Följer ni det här mönstret blir integrationen både snabbare och stabilare — och ni behöver nästan aldrig kontakta supporten.

Sekvensen#

  1. Vid uppstart / på schema: logga in server-side en gång. Spara access- och refresh-token i delad lagring.
  2. På schema: förnya token proaktivt var 36:e timme, inte när något gått sönder.
  3. På schema eller vid behov: hämta enhetslistan och skriv om mappningen. Ett dygn (24 h) är en rimlig utgångspunkt.
  4. I öppningsögonblicket: slå upp {deviceId} i mappningen och gör ett unlock-anrop.

Ansvarsfördelningen#

LagerAnsvarFår inte
Er klient / appVisa dörrar, ta emot ett tryckAldrig hålla tokens eller anropa KiiOn direkt
Er backendBehörighetsregler, tokenhantering, mappning, auditLogga in per öppning
KiiOn-API:tUtföra upplåsningenAvgöra om användaren borde få öppna

1. Tokenlagret#

En singleton per API-konto som äger inloggning, förnyelse och 401-hantering. All annan kod ber om en giltig token, aldrig om en inloggning. Detaljerna finns i Autentisering & tokens.

Node / TypeScript — getValidAccessToken
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
}

2. Enhetsmappningen#

Enhetslistan ändras sällan; unlock-anropen sker ofta. Hämta listan på schema och spara minst Device.Id och Device.Name — gärna även plats och företag så att felsökning går att göra utan att slå i API:t.

Node / TypeScript — syncDevices
/** Runs on a schedule, not on the unlock path. */
export async function syncDevices() {
  const res = await apiCall("/user/device");
  const devices: Device[] = await res.json();

  await db.deviceMapping.replaceAll(
    devices.map((d) => ({
      kiionDeviceId: d.id,      // fills {deviceId} in the unlock call
      kiionName: d.name,
      locationId: d.locationId,
      locationName: d.locationName,
      syncedAt: Date.now(),
    })),
  );
}
  • Uppdatera mappningen var 24:e timme, samt när ni får 404 på ett känt {deviceId}.
  • Låt mappningen vara nyckeln mellan er interna dörridentitet och KiiOns id — hårdkoda aldrig id:t.
  • Spara syncedAt så att ni kan se hur gammal mappningen är när något ser fel ut.

3. Öppningsögonblicket#

Här ska det bara finnas ett HTTP-anrop. Behörighetskontrollen är er, uppslaget är lokalt, token är redan giltig.

Node / TypeScript — the unlock path
/** Everything expensive already happened. This is one call. */
export async function openDoor(internalDoorId: string, actor: User) {
  // 1. YOUR rules decide. KiiOn does not.
  if (!(await mayOpen(actor, internalDoorId))) throw new Forbidden();

  // 2. Cached mapping — no device-list call here.
  const deviceId = await db.deviceMapping.kiionIdFor(internalDoorId);
  if (!deviceId) throw new NotMapped(internalDoorId);

  // 3. One call, with a token that is already valid.
  const res = await apiCall(`/device/${deviceId}/unlock`, { method: "PUT" });
  await audit.record({ actor, internalDoorId, deviceId, status: res.status });
  return res.ok;
}

Checklista#

  • Inloggning sker server-side och återanvänds
  • Proaktiv token-förnyelse med single-flight-lås
  • 401-fallback som gör om anropet en gång
  • Roterad refresh-token sparas atomiskt
  • Enhetslistan cachelagras och hårdkodas inte
  • Öppningsögonblicket innehåller exakt ett API-anrop
  • Varje upplåsning skrivs till er egen audit-logg

Vidare

Kopiera sidan som Markdown: Visa som Markdown