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:
ClubsiteMasterbruker én ID-serie (Eks: Arendal = 6).BO-BOOKINGbruker 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 MÅ 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), må alle API-kall (fetch/axios) konfigureres medcredentials: 'include'(cors) for atcmsadmin-cookien skal sendes med.