Skip to content

API Oversikt

GolfChart eksponerer et sett med Cloud Functions for å håndtere backend-logikk som krever server-side validering, transaksjoner, og integrasjoner med eksterne tjenester.

Tilgjengelige endpoints

Funksjon Beskrivelse Autentisering Roller
createRental Opprett ny utleie staff, clubAdmin, superadmin
endRental Avslutt pågående utleie staff, clubAdmin, superadmin
updateCartStatus Endre cart status staff, clubAdmin, superadmin
sendNotification Send e-post/SMS staff, clubAdmin, superadmin
generateReport Generer rapport (scheduled) N/A (cron)
setUserRole Sett brukerrolle superadmin

Base URL

Produksjon:

https://europe-west1-golfbilkontroll-skigk.cloudfunctions.net

Emulator (lokal):

http://localhost:5001/golfbilkontroll-skigk/europe-west1

Autentisering

Alle callable functions krever Firebase Auth ID token:

// admin/src/api/client.ts
import { getFunctions, httpsCallable } from 'firebase/functions';
import { getAuth } from 'firebase/auth';

const functions = getFunctions(app, 'europe-west1');

export async function callFunction<T>(name: string, data: any): Promise<T> {
  const auth = getAuth();
  const user = auth.currentUser;

  if (!user) {
    throw new Error('Ikke innlogget');
  }

  const fn = httpsCallable<any, T>(functions, name);
  const result = await fn(data);
  return result.data;
}

Feilhåndtering

Functions returnerer standard Firebase HttpsError ved feil:

// Eksempel respons ved feil
{
  "code": "permission-denied",
  "message": "Ikke tilgang til denne klubben",
  "details": {
    "clubId": "ski-gk",
    "userClubs": ["holmenkollen-gk"]
  }
}

Feilkoder:

Kode Betydning
unauthenticated Bruker er ikke innlogget
permission-denied Mangler nødvendig rolle eller klubbtilgang
invalid-argument Ugyldig input-data
not-found Ressurs ikke funnet
already-exists Ressurs eksisterer allerede
resource-exhausted Rate limit overskredet
internal Intern serverfeil

Rate limiting

App Check er aktivert for alle functions:

export const myFunction = onCall(
  {
    enforceAppCheck: true,
    consumeAppCheckToken: true
  },
  async (data, context) => {
    // Handler
  }
);

Grenser:

  • Standard: 100 requests/min per IP
  • Authenticated: 1000 requests/min per bruker

Eksempel: createRental

Request

import { callFunction } from '@/api/client';

const result = await callFunction<{ rentalId: string; price: number }>('createRental', {
  clubId: 'ski-gk',
  cartId: 'cart123',
  renterName: 'Ola Nordmann',
  membershipNumber: '73-12345',
  hasDoctorsNote: false,
  holes: 18,
  paymentMethod: 'Kort',
  notificationMethod: 'email',
  contactInfo: 'ola@example.com',
  notes: 'Forhåndsbooket'
});

console.log(`Rental ${result.rentalId} opprettet, pris: ${result.price} kr`);

Response

{
  "rentalId": "rental_20251203_001",
  "price": 350,
  "startTime": "2025-12-03T10:00:00.000Z"
}

Flow

sequenceDiagram
    participant UI as Admin UI
    participant CF as createRental
    participant Auth as Firebase Auth
    participant FS as Firestore
    participant Email as SendGrid

    UI->>CF: createRental(data)
    CF->>Auth: Verify ID token
    Auth-->>CF: Valid + Custom Claims
    CF->>CF: Validate clubId access
    CF->>FS: Transaksjon START
    CF->>FS: Les cart (sjekk available)
    CF->>FS: Les pricingRules
    CF->>CF: Beregn pris
    CF->>FS: Opprett rental
    CF->>FS: Oppdater cart.status = 'rented'
    CF->>FS: Transaksjon COMMIT
    CF->>Email: Send notifikasjon (async)
    CF-->>UI: { rentalId, price }

Testing

Lokal testing (Emulator)

// admin/src/firebase.ts
import { connectFunctionsEmulator } from 'firebase/functions';

if (import.meta.env.VITE_USE_EMULATOR === 'true') {
  connectFunctionsEmulator(functions, 'localhost', 5001);
}

Integration tests

// tests/functions.test.ts
import { wrap } from 'firebase-functions-test';

const testEnv = wrap();

describe('createRental', () => {
  it('should create rental and update cart', async () => {
    const data = {
      clubId: 'ski-gk',
      cartId: 'cart123',
      renterName: 'Test User',
      holes: 18
    };

    const context = {
      auth: {
        uid: 'user123',
        token: {
          role: 'staff',
          clubs: ['ski-gk']
        }
      }
    };

    const result = await createRental(data, context);

    expect(result.rentalId).toBeDefined();
    expect(result.price).toBe(350);
  });
});

Best practices

✅ Do:

  • Alltid valider input i functions
  • Bruk transaksjoner for multi-step operasjoner
  • Logg alle kritiske operasjoner
  • Håndter race conditions (f.eks. to utleier på samme cart samtidig)
  • Bruk structured logging

❌ Don't:

  • Ikke stol på frontend-validering alene
  • Ikke gjør tunge operasjoner i functions (cold start)
  • Ikke returner sensitive data i error messages
  • Ikke glem å validere clubId-tilgang

Neste steg