Slik bygde Kurt Gatsby-siden til sameiet

Del 3 av Kurts guide.

I del 1 og 2 av denne serien har Kurt Lekanger gått gjennom hvilke teknologivalg jeg gjorde da han skulle bygge nye nettsider for boligsameiet, og hvordan han satte opp løsningen med Gatsby som frontend og Contentful som backend.

Gatsby omtales gjerne som en "static site generator", som betyr at i det du taster inn kommandoen gatsby build så begynner Gatsby å hente innhold fra et CMS, et API eller kanskje markdownfiler på disken.

«Gatsby rendrer statiske HTML-filer og pakker alt pent og pyntelig sammen»

Innhold og data fra ulike kilder kombineres, og Gatsby rendrer statiske HTML-filer og pakker alt pent og pyntelig sammen – uten at du behøver å kunne noe som helst om Webpack-konfigurering, code splitting eller andre ting som ofte kan være litt komplisert å sette opp.

Statiske nettsider gir høy ytelse

Det er mange fordeler med statiske nettsider, hvor kanskje høy ytelse er noe av det første i hvert fall jeg tenker på.

Høy sikkerhet er også et argument.

Ettersom nettsidene lages i det du bygger siden, og brukeren får servert statiske HTML-sider, er angrepsflaten betydelig redusert. Det er for eksempel ikke mulig for en angriper å få tilgang til annet innhold fra databaser eller CMS-er du bruker, utover det innholdet Gatsby hentet da de statiske sidene ble generert.

«Ønsket å ha et eget område på sameiets nettsider som kun skulle være tilgjengelig for de som bor i sameiet.»

Det har vært mange eksempler opp igjennom på Wordpress-plugins fulle av sikkerhetshull, ofte med fatale konsekvenser. Sånt slipper du med Gatsby.

Gatsby trenger ikke være bare statiske sider

Som nevnt i de første delene av denne serien, ønsket jeg å ha et eget område på sameiets nettsider som bare skulle være tilgjengelig for dem som bor i sameiet.

Det betyr at jeg ikke ønsker at disse sidene skal være statiske sider, men også ha mulighet til å hente innhold dynamisk etter behov – i mitt tilfelle avhengig av om brukeren er innlogget eller ikke.

Før jeg går inn på hvordan jeg har satt opp selve innloggingsfunksjonen, må jeg si litt om hvordan Gatsby kan håndtere sider som bare er tilgjengelige for innloggede brukere.

Client-only routes i Gatsby

Gatsby har støtte for såkalte client-only routes. Dette gjør det mulig å lage sider som eksisterer bare på klienten (i nettleseren) og der det altså ikke opprettes statiske HTML-sider som havner i /public-mappa når du kjører gatsby build-kommandoen.

Client-only-ruter fungerer mer som en tradisjonell single page-app i React, og ved å bruke Reach Router som er innebygd i Gatsby kan du håndtere de ulike rutene som bare innloggede brukere skal se.

«Ved hjelp av Auth0 kan jeg beskytte tilgangen til alle client-only-ruter.»

I tillegg til det du har innebygd i Gatsby, trenger du en autentiseringsløsning. Jeg havnet til slutt på Auth0, siden det er en utbredt og velprøvd løsning som har muligheter jeg trenger seinere for å bygge en løsning for brukeradministrasjon. Ved hjelp av Auth0 kan jeg beskytte tilgangen til alle client-only-ruter.

Under er et forenklet diagram som viser hvordan det fungerer hos meg.

Enten logget inn, eller ikke

De blå boksene er statiske sider som lages ved bygging av Gatsby-siten.

For ruten /informasjon lages det også en statisk side som hvis brukeren ikke er logget inn viser en tekstplakat med beskjed om at du må logge inn for å se innholdet.

Hvis brukeren er logget inn, brukes Reach Router for å vise den riktige React-komponenten avhengig av hvilken rute brukeren prøver å nå. Dette pakkes inn i en -komponent som bruker en higher order-komponent i auth0-react kalt withAutenthicationRequired til å sjekke om en bruker er logget inn eller ikke.

«Enten er man logget inn og har tilgang — eller så er man det ikke.»

Jeg har ikke noe mer avansert autentisering enn det, dvs. enten er man logget inn og har tilgang – eller så er man det ikke.

Det er bare styret i sameiet som kan opprette nye brukere i Auth0, dermed har vi kontroll på hvem som har brukerkonto til enhver tid.

image: Slik bygde Kurt Gatsby-siden til sameiet

For å gjøre det enklere å lage client-only-ruter, bruker jeg en offisiell plugin kalt gatsby-plugin-create-client-paths.

Når den er installert, kan du i gatsby-config.js konfigurere hvilke ruter du vil skal være private, og som altså Gatsby ikke skal lage statiske sider ut av når du kjører gatsby build:

// ./gatsby-config.js

plugins: [
{
      resolve: `gatsby-plugin-create-client-paths`,
      options: { prefixes: [`/informasjon/*`, `/min-side/*`] },
},
]

Her vil alt som ligger under /informasjon og /min-side være ikke-statiske sider.

På sameiets nettsider er det et menyelement på navigasjonslinjen som heter For beboere som ligger under ruten /informasjon (altså https://gartnerihagen-askim.no/informasjon).

Slik gjør Kurt kodebasen enklere:

Jeg opprettet derfor filen informasjon.tsx under /src/pages, og bruker i denne Reach Router for å vise ulike React-komponenter avhengig av rute.

Går du for eksempel til /informasjon/referater, er det -komponenten som skal vises.

Jeg har valgt å legge alle komponenter som bare skal vises til innloggede brukere i mappa /src/components/private-components i stedet for å bare hive alt sammen i en svær haug sammen med de andre komponentene.

«Jeg har en Min side-komponent hvor innloggede brukere kan få informasjon om brukeren, bytte passord, osv.»

Mest for å gjøre kodebasen litt enklere å forholde seg til og øke lesbarheten, men også som en liten påminnelse til meg selv om at dette er komponenter der jeg kanskje kan gjøre en ekstra sjekk av om brukeren er logget inn, kanskje vise et brukernavn, osv.

Min side-komponent

For eksempel har jeg en Min side-komponent hvor innloggede brukere kan få informasjon om brukeren, bytte passord, osv.

Slik ser informasjon.tsx-siden ut hos meg, og slik er rutingen satt opp (forkortet, se fullstendig kildekode på https://github.com/klekanger/gartnerihagen):

// ./src/pages/informasjon.tsx import * as React from 'react'; import { useAuth0 } from '@auth0/auth0-react'; import { Router } from '@reach/router'; import PrivateRoute from '../utils/privateRoute'; import InfoPage from '../components/private-components/informasjon'; import Referater from '../components/private-components/referater'; import LoadingSpinner from '../components/loading-spinner'; import NotLoggedIn from '../components/private-components/notLoggedIn'; const Informasjon = () => { const { isLoading, isAuthenticated, error } = useAuth0(); if (isLoading) { return ( ); } if (error) { return
Det har oppstått en feil... {error.message}
; } if (!isAuthenticated) { return ; } return ( ); }; export default Informasjon;

<PrivateRoute>-komponenten min ser du nedenfor. Den sørger for at du må være innlogget for å få tilgang.

Hvis ikke sendes brukeren til en innloggings-dialogboks levert fra Auth0.

// ./src/utils/privateRoute.tsx

import * as React from 'react';
import { withAuthenticationRequired } from '@auth0/auth0-react';

interface IPrivateroute {
  component: any;
  location?: string;
  path?: string;
  postData?: any;
  title?: string;
  excerpt?: string;
}

function PrivateRoute({
  component: Component,
  location,
  ...rest
}: IPrivateroute) {
  return <Component {...rest} />;
}

export default withAuthenticationRequired(PrivateRoute);

Navbar med innlogging

Private ruter er vel og bra – men som nevnt trenger vi en autentiseringsløsning for å finne ut hvem som skal ha tilgang og ikke.

Første versjon av sameiets nettsider ble satt opp med Netlify Identity og Netlify Identity Widget, på grunn av at jeg allerede brukte Netlify til å hoste nettsidene – og at løsningen var veldig enkel å sette opp.

Det viste seg imidlertid fort at det var en del begrensninger med Netlify Identity.

«Det var en del begrensninger med Netlify Identity.»

Den ene var at innloggingsboksen ikke var på norsk (jeg oversatte og åpnet en pull request, men orket ikke å vente på at den skulle gå igjennom …). I tillegg begynte jeg å jobbe med et litt mer avansert frontend-dashbord for brukerkontoadministrasjon (som ikke er ferdig bygget ennå) hvor jeg ville trenge noe mer funksjonalitet enn det jeg fant i Netlify Identity Widget.

Endte opp med Auth0

Etter litt research, endte jeg opp med å velge Auth0.

Etter at jeg hadde registrert meg og satt opp alt hos Auth0.com, installerte jeg Auth0 React SDK slik: npm install @auth0/auth0-react

Auth0 React SDK bruker React Context, slik at du kan wrappe hele applikasjonen din i en Auth0Provider som sørger for at Auth0 vet om brukeren er logget inn eller ikke, uansett hvor i applikasjonen brukeren befinner seg.

«Etter litt research, endte jeg opp med å velge Auth0.»

Se om brukeren er autentisert

Da kan du seinere, hvor som helst i applikasjonen, importere useAuth-hooken slik: import { useAuth0 } from '@auth0/auth0-react' og fra useAuth hente ut ulike metoder eller egenskaper som har med innlogging å gjøre, for eksempel sjekke om brukeren er autentisert, få opp en innloggingsboks, osv.

For eksempel slik: const { isAuthenticated } = useAuth0 () for å seinere kunne sjekke om brukeren er innlogget ved å gjøre noe sånt som if ("isAuthenticated){ return }

«I Gatsby kan du wrappe root-elementet til nettsiden med en annen komponent»

I Gatsby kan du wrappe root-elementet til nettsida med en annen komponent (f.eks. Auth0Provider) ved å eksportere wrapRootElement fra filen gatsby-browser.js.

Les mer om det i Gatsby-dokumentasjonen.

Gatsby-browser med Auth0Provider

Slik ser min gatsby-browser.js-fil ut, med Auth0Provider satt opp for at alle sider på nettsida skal ha tilgang til informasjon om hvorvidt brukeren er logget inn eller ikke:

// ./gatsby-browser.js

import * as React from 'react';
import { wrapPageElement as wrap } from './src/chakra-wrapper';
import { Auth0Provider } from '@auth0/auth0-react';
import { navigate } from 'gatsby';

const onRedirectCallback = (appState) => {
  // Use Gatsby's navigate method to replace the url
  navigate(appState?.returnTo || '/', { replace: true });
};

export const wrapRootElement = ({ element }) => (
  <Auth0Provider
    domain={process.env.GATSBY_AUTH0_DOMAIN}
    clientId={process.env.GATSBY_AUTH0_CLIENT_ID}
    redirectUri={window.location.origin}
    onRedirectCallback={onRedirectCallback}
  >
    {element}
  </Auth0Provider>
);

export const wrapPageElement = wrap;

Jeg laget en innloggingsknapp i navigasjonslinjen øverst på skjermen.

Når brukeren prøver å logge inn, blir vedkommende sendt til Auth0 sin innloggingsside – og tilbake igjen til sameiets nettside hvis brukernavn og passord er riktig.

image: Slik bygde Kurt Gatsby-siden til sameiet

Under innloggingsknappen har jeg også laget en Min side der brukeren får opp informasjon om hvem som er logget inn, og har mulighet til å bytte passord.

Av sikkerhetsmessige årsaker endres ikke passordet direkte, men i stedet vil Bytt passord-knappen sende en POST-request til Auth0s autentiserings-API med forespørsel om å bytte passord.

Auth0 har en beskrivelse av hvordan dette fungerer her.

«Vil kanskje finne et potensielt sikkerhetsproblem i min løsning.»

Sikkert som banken? Noen betraktninger til slutt

De mest kunnskapsrike og observante leserne som har kikket på kildekoden til noen av de "private" komponentene mine, vil kanskje finne et potensielt sikkerhetsproblem i min løsning.

Jeg har imidlertid valgt å se gjennom fingrene med dette i første versjon av nettstedet siden det ikke er sensitiv informasjon som ligger tilgjengelig for sameiets beboere (det er mer at det ikke er interessant for andre enn de som bor her).

Sikkerhetsproblemet er som følger: Selv om jeg har laget nettsidene for innloggede brukere som client only routes i Gatsby, og man må være innlogget for å komme dit, har jeg brukt Gatsbys GraphQL-datalag for å hente informasjonen som skal være tilgjengelig bare for innloggede brukere.

«En annen mulighet er å bygge et backend-API som tar seg av henting av "hemmelige" data.»

Kun tilgjengelig ved build time

Dette er bare tilgjengelig ved build time, noe som betyr at dataene fra spørringen over faktisk blir hentet ved build time (når du bruker useStaticQuery-hooken f.eks.), og at de blir tilgjengelig i det som til slutt blir levert til klienten.

For eksempel i Dokumenter-komponenten som brukes for å gi innloggede brukere en oversikt over referater etc. fra årsmøter brukes denne GraphQL-queryen for å hente ut informasjon:

// ./src/components/private-components/dokumenter.tsx

export default function Dokumenter({ title, excerpt, ...props }: IDokumenter) {
  const { menu }: IMenu = useStaticQuery(graphql`
    {
      menu: contentfulServiceMenu {
        files: menu6Files {
          contentful_id
          title
          file {
            url
            fileName
          }
          createdAt(formatString: "DD.MM.YYYY")
          updatedAt(formatString: "DD.MM.YYYY")
        }
      }
    }
  `);

  const content = menu?.files || [];

Men det kreves litt kunnskap om hvordan du bruker utviklerverktøyene og network-taben i nettleseren for å finne igjen data fra disse spørringene.

Det er mange måter å løse dette på, der én av måtene jeg har vurdert er å bruke en tredjeparts GraphQL-klient (Apollo f.eks.) til å hente data ved run-time, etter at jeg har sjekket at brukeren er logget inn.

En annen mulighet er å bygge et backend-API som tar seg av henting av "hemmelige" data, etter en sjekk av om brukeren har rettigheter til dette eller ikke.

«Brukeradmin-API-et (backend) verifiserer deretter access-tokenet.»

Brukeradmin-panel basert på Netlify

Jeg holder uansett på å lage et brukeradmin-panel som vil være basert på Netlify Functions (som igjen er basert på AWS Lambda), hvor jeg på frontend henter et access token for brukeren som er innlogget og sender med dette når jeg gjør et kall til brukeradmin-API-et som ligger som en serverless function hos Netlify.

Brukeradmin-API-et (backend) verifiserer deretter dette access-tokenet og sjekker at brukeren har de nødvendige rettigheter før API-et gjennomfører operasjoner som f.eks. å endre brukerdata, slette eller opprette brukere, og så videre.

En tilsvarende løsning kan brukes til å returnere "hemmelige" data fra backend til bare autoriserte brukere. Les mer om rollebasert aksesskontroll (RBAC) hos Auth0.

image: Slik bygde Kurt Gatsby-siden til sameiet

Her er hvordan brukeradmin-dashbordet vil se ut, og en skjematisk framstilling av hvordan autentiseringen kan foregå. Som sagt, "work in progress" …

Hvordan jeg løser dette, vil jeg komme tilbake til i en eventuell seinere artikkel.

Brukeradmin-panelet er et litt større prosjekt som jeg holder på å kikke på i de få ledige stundene jeg har.

Dette er et fritidsprosjekt, og jeg sitter jo ikke og koder hele tiden på fritida (selv om kona påstår det :-)).

Gode tips mottas gjerne!