Skip to content

Dokumentasjon: GKIT Cart-Rental App

Sist oppdatert: 26. April 2026
Forfatter: GKIT / Owe Stangeland
Infrastruktur: .NET 9 Web API + Classic ASP (ClubSite-4) "Zero-Touch Widget"
Database: SQL Server (BO-BOOKING)

Dette dokumentet samler hele systemarkitekturen, sikkerhetsmodellen, multi-tenant strategien og driftsveiledningen for applikasjonen. Det er formatert for bruk med verktøy som MkDocs og GitHub/DevOps wikier.


Kapittel 1: Systemarkitektur og Konsept

Prosjektet ble opprinnelig planlagt på Firebase, men er nå portert og bygget som en moderne .NET 9 (Backend) og React (Frontend) løsning. Formålet er å levere en utleie-app for Golfbiler til 55 norske golfklubber uten å måtte skrive store mengder ny infrastruktur i hver klubbs legacy CMS-system.

1.1 Zero-Touch Widget Modellen

Istedenfor å kode inn applikasjonen i alle de 55 gamle legacy-mappene (eks. CS4-ARENDAL, SKI), kjører det nye .NET 9 APIet på én sentral instans (Single Deployment).

I det eksisterende Classic ASP-systemet ("ClubSite-4") injecter vi frontenden via en enkel nettleser-bundle (et <script> tag eller en <iframe>) som sender oppkallet med et tenant-alias, for eksempel:

https://www.clubsite.no/api/cart-rental?tenant=ski-golfklubb

1.2 Dataflyt Oversikt (Mermaid)

graph TB
    subgraph "Legacy CMS (ClubSite-4)"
        A[Classic ASP Frontend<br/>clubsite.no/ski/]
        B[cmsadmin Cookie<br/>Authentication]
    end

    subgraph "GKIT Cart Rental (Modern Stack)"
        C[Widget Injector<br/>React/JS]
        D[.NET 9 Web API<br/>Tenant Resolver & Dapper]
        E[appsettings.Tenants.json<br/>OfficialClubId Mapping]
    end

    subgraph "Nye Legacy Databaser"
        F[(BO-BOOKING<br/>Central Booking DB)]
        G[(ClubsiteMaster<br/>Site ID Mapping)]
    end

    A -->|1. Laster iframe / script| C
    B -.->|2. Sender ukryptert Session Cookie| D
    C -->|3. API Kall med ?tenant=ski| D
    D -->|4. Slår opp Tenant ID i JSON| E
    E -.->|Returnerer eks: LegacyBookingId 1146| D
    D -->|5. SQL Queries filtrert på ID| F
    G -.->|6. Original Site Info for Admin| D


Kapittel 2: Multi-Tenancy & ID-Mapping

Da vi undersøkte de gamle databasene fra ClubSite-4 og BO-BOOKING, oppdaget vi et betydelig ID-avvik:

  • ClubsiteMaster bruker én ID-serie (Eks: Arendal = 6).
  • BO-BOOKING bruker en annen ID-serie (Eks: Arendal = 1146).

2.1 The Single Source of Truth

Applikasjonen bruker nå Norges Golfforbund (NGF) sin offisielle klubbID i sentrum for all nyutvikling. Filen appsettings.Tenants.json lades inn i RAM ved oppstart av .NET-appen for å koble aliaset fra Widgeten rett over til korrekt SQL ID.

Utdrag appsettings.Tenants.json:

{
  "Tenants": [
    { "Alias": "arendalgk", "OfficialClubId": 11, "LegacyBookingId": 1146, "ClubName": "Arendal & Omegn Golfklubb" },
    { "Alias": "ski-golfklubb", "OfficialClubId": 73, "LegacyBookingId": 1152, "ClubName": "Ski Golfklubb" }
  ]
}

2.2 Routing i .NET (.TenantResolverMiddleware)

Hver request fanges opp av en .NET Middleware som leser ?tenant=XX parameteren i HTTP requesten. Den slår opp i RAM-dictionaryen og injiserer LegacyBookingId statisk inn i HttpContext.Items["TenantId"]. Dermed kan alle klasser nedover i pipelinen utelukkende stole på Contexten og aldri på bruker-input.


Kapittel 3: Dapper & Databasesikkerhet (SQL Isolasjon)

Sikkerhetsansvarlig & DB-Admin MERK: For å garantere at ingen data krysser mellom klubbene i den delte MS SQL-databasen BO-BOOKING-WORK (dev) / BO-BOOKING (prod), benytter vi oss av strikt Parameterisering via et custom BaseRepository.

3.1 BaseTenantRepository

Alle repositories (f.eks ChartCartRepository) arver fra BaseTenantRepository. Før en query får lov til å bygge en Dapper Query, roper koden på funksjonen GetTenantId(). Denne leser HTTP Contexten satt av ruteren vår. Hvis den mangler vil kallet krasje umiddelbart (Throw Exception) for å beskytte databasen.

3.2 Eksempel på Trygg Query-implementasjon

Alle SQL operasjoner ha klausulen WHERE ClientID = @TenantId som vist under.

public async Task<IEnumerable<ChartCartModel>> GetAllCartsAsync()
{
    // OBLIGATORISK FØRSTE STEG
    int tenantId = GetTenantId(); 

    using var connection = GetConnection();

    // OBLIGATORISK: "WHERE ClientID = @TenantId" i ALLE queries!
    var sql = "SELECT * FROM dbo.Chart_Cart WHERE ClientID = @TenantId AND Active = 1";

    // Injiser parametern trygt ned i Dapper. Aldri string-concatinering.
    return await connection.QueryAsync<ChartCartModel>(sql, new { TenantId = tenantId });
}

````r

**Brudd på regel:** Det er ikke tillatt i Code Review / merge til master å ha Data-Reads mot Chart_Bookings tabellene uten en `ClientID`-filtrering.

---

## Kapittel 4: Autentisering og Tilgangskontroll

Siden vi bygger en moderne mikrotjeneste for et legacy system, manglet vi i utgangspunktet Identity Server integrasjon. 

### 4.1 CmsAdmin Cookie Handler

I ClubSite-4 logger administrasjonen og ProShop-ansatte seg inn via gammelt skjema som setter en ukryptert Session ID-cookie kalt `cmsadmin`.

1. Når en forespørsel kommer fra en iFrame mot APIet vårt, følger `cmsadmin` cookien med.
2. Vår .NET klasse `CmsAdminCookieAuthenticationHandler` fanger opp dette.
3. Koden kjører `HttpUtility.ParseQueryString()` og tvinger ut parametere som f.eks `admid=5&admname=Owe&super=True`.
4. Den oversetter "Owe" til en trygg .NET `ClaimsPrincipal` identitet i RAM, og tildeler rollen `ProShop` (og ev. `SuperAdmin` ved super=True).

### 4.2 Autorisasjon Pipelinen (`[Authorize]`)

Dette gjør at alle moderne .NET attributter på endepunktene våre, som `[Authorize(Roles = "ProShop")]`, fungerer feilfritt. Hvis cookien er slettet eller hacket/feil-formatert, nekter APIet tilgang (`401 Unauthorized`). Brukere trenger altså ikke logge inn enda en gang i Cart-Rental systemet; "Single Sign-On" er løst i bakgrunnen.

---

## Kapittel 5: Gjenstående Implementasjonsoppgaver (TODO)

Per 26. April 2026 er fundamentet ferdig. Dette dekker:

* [x] Setup av Konfigurasjon & Tenants-dict
* [x] Middleware resolver for Tenants
* [x] Cookie Authenticator for Legacy Single Sign-on
* [x] BaseRepository for Dapper MS-SQL sikkerhet.

**Neste og siste steg for fullføring av integrasjonen:**

1. Utvikle "Widget-injektoren" (f.eks en 5 linjers `cart-department.asp` fil) for å laste inn Frontend fra den sentrale serveren.
2. Skrive utvidede CRUD Queries i Repositoriene for bookings (henting, reservasjon og endring) over Dapper.
3. Publisere (`dotnet publish`) til en IIS Webserver som har nettverkstilgang til BO-BOOKING.

---

## Kapittel 6: Deployment Instructions for CMS (Zero-Touch Widget)

For å sikre at Cart-Rental applikasjonen integreres sømløst i det eksisterende ClubSite-4 miljøet **uten** å bryte design, navigasjon eller adgangskontroll, skal widgeten inkluderes via en standard ASP-fil, og **ikke** som en frittstående `index.html`-applikasjon.

### 6.1 Fallgruver ved frittstående filer

Eventuelle forsøk (som f.eks. i mappen `SKI/golfbil`) på å legge ut en frittstående React/Vite-app (`index.html`) direkte på webserveren må slettes. Dette bryter ut av ClubSite-designet (mangler menyer/footer) og mister integrasjonen med IIS/Classic ASP session-håndtering, som er kritisk for at `cmsadmin`-cookien skal fungere.

### 6.2 Korrekt Installasjon (Oppskrift for Server-admin)

Opprett en ny fil i klubbens rotmappe (f.eks. `/SKI/golfbiler.asp`) med følgende standard ClubSite-4 struktur:

```asp
<!-- #include file="common-front/declarations.asp" -->
<!-- #include file="common-front/html_header.asp" -->

<div id="wrapper">
    <!-- #include virtual="/common-front/page_header.asp" -->

    <section id="subtitle">
        <div class="inner">
            <div class="subtitle">Golfbil Utleie</div>
        </div>
    </section>

    <section id="content">
        <div class="inner">
            <div class="page">
                <!-- ========================================== -->
                <!-- GKIT CART RENTAL WIDGET INJECTION          -->
                <!-- ========================================== -->
                <script type="text/javascript" src="https://www.clubsite.no/api/widget/cart-rental.js"></script>
                <div id="gkit-cart-rental-root" data-tenant="ski-golfklubb"></div>
                <!-- ========================================== -->
            </div>
        </div>
    </section>

    <!-- #include virtual="/common-front/page_footer.asp" -->
</div>
<!-- #include file="common-front/html_footer.asp" -->

Viktige punkter for Frontend-utvikler:

  • data-tenant-attributtet: Dette er det eneste som er unikt per klubb. Det forteller React-appen hvilken klubb som skal lastes.
  • Credentials/CORS: Siden widgeten kjører på klubbens domene (f.eks. www.skigk.no) men snakker med det delte APIet (f.eks. api.clubsite.no), alle API-kall (fetch/axios) konfigureres med credentials: 'include' (cors) for at cmsadmin-cookien skal sendes med.