Del 3: Slik bygget jeg sameiets nye nettsider. Autentisering og private ruter i Gatsby

Client-only routes i Gatsby brukes for de delene av nettsiden du ikke vil skal bygges som statiske HTML-sider.

I del 1 og 2 av denne serien har jeg gått gjennom hvilke teknologivalg jeg gjorde da jeg skulle bygge nye nettsider for boligsameiet, og hvordan jeg 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. 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.

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. 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 kun skulle være tilgjengelig for de 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 kun er tilgjengelige for innloggede brukere.

Gatsby har støtte for såkalte client-only routes. Dette gjør det mulig å lage sider som eksisterer kun på klienten (i nettleseren) og der det altså ikke opprettes statiske HTML-sider som havner i /public-mappen 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 innebygget i Gatsby kan du håndtere de ulike rutene som kun innloggede brukere skal se.

I tillegg til det du har innebygget 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 senere 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. 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 <Privateroute>-komponent som bruker en higher order-komponent i auth0-react kalt withAutenthicationRequired til å sjekke om en bruker er logget inn eller 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 kun styret i sameiet som kan opprette nye brukere i Auth0, dermed har vi kontroll på hvem som har brukerkonto til enhver tid.

En <Privateroute>-komponent fungerer som en slags "portvakt" som slipper gjennom kun innloggede brukere.
En <Privateroute>-komponent fungerer som en slags "portvakt" som slipper gjennom kun innloggede brukere.

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:

1// ./gatsby-config.js
2
3plugins: [
4{
5      resolve: `gatsby-plugin-create-client-paths`,
6      options: { prefixes: [`/informasjon/*`, `/min-side/*`] },
7},
8]

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 ). 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 <Referater>-komponenten som skal vises. Jeg har valgt å legge alle komponenter som kun skal vises til innloggede brukere i mappen /src/components/private-components i stedet for å bare hive alt sammen i en svær haug sammen med de andre komponentene. 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. 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):

1// ./src/pages/informasjon.tsx
2
3import * as React from 'react';
4import { useAuth0 } from '@auth0/auth0-react';
5import { Router } from '@reach/router';
6import PrivateRoute from '../utils/privateRoute';
7
8import InfoPage from '../components/private-components/informasjon';
9import Referater from '../components/private-components/referater';
10
11import LoadingSpinner from '../components/loading-spinner';
12import NotLoggedIn from '../components/private-components/notLoggedIn';
13
14const Informasjon = () => {
15  const { isLoading, isAuthenticated, error } = useAuth0();
16
17  if (isLoading) {
18    return (
19      <Box>
20        <LoadingSpinner spinnerMessage='Autentiserer bruker' />
21      </Box>
22    );
23  }
24
25  if (error) {
26    return <div>Det har oppstått en feil... {error.message}</div>;
27  }
28
29  if (!isAuthenticated) {
30    return <NotLoggedIn />;
31  }
32
33  return (
34    <Router>
35      <PrivateRoute path='/informasjon' component={InfoPage} />
36      <PrivateRoute
37        path='/informasjon/referater/'
38        component={Referater}
39        title='Referater fra årsmøter'
40        excerpt='På denne siden finner du referater fra alle tidligere årsmøter. Er det noe du savner, ta kontakt med styret.'
41      />
42    </Router>
43  );
44};
45
46export 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.

1// ./src/utils/privateRoute.tsx
2
3import * as React from 'react';
4import { withAuthenticationRequired } from '@auth0/auth0-react';
5
6interface IPrivateroute {
7  component: any;
8  location?: string;
9  path: string;
10  postData?: any;
11  title?: string;
12  excerpt?: string;
13}
14
15function PrivateRoute({ component: Component, ...rest }: IPrivateroute) {
16  return <Component {...rest} />;
17}
18
19export 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. 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. 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. Da kan du senere, 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. F.eks. slik: const { isAuthenticated } = useAuth0() for å senere kunne sjekke om brukeren er innlogget ved å gjøre noe sånt som if("isAuthenticated){ return <NotLoggedIn /> }

I Gatsby kan du wrappe root-elementet til nettsiden med en annen komponent (f.eks. Auth0Provider) ved å eksportere wrapRootElement fra filen gatsby-browser.js. Les mer om det i Gatsby-dokumentasjonen.

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

1// ./gatsby-browser.js
2
3import * as React from 'react';
4import { wrapPageElement as wrap } from './src/chakra-wrapper';
5import { Auth0Provider } from '@auth0/auth0-react';
6import { navigate } from 'gatsby';
7
8const onRedirectCallback = (appState) => {
9  // Use Gatsby's navigate method to replace the url
10  navigate(appState?.returnTo || '/', { replace: true });
11};
12
13export const wrapRootElement = ({ element }) => (
14  <Auth0Provider
15    domain={process.env.GATSBY_AUTH0_DOMAIN}
16    clientId={process.env.GATSBY_AUTH0_CLIENT_ID}
17    redirectUri={window.location.origin}
18    onRedirectCallback={onRedirectCallback}
19  >
20    {element}
21  </Auth0Provider>
22);
23
24export 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.

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.

"Min Side" dukker opp under innloggingsknappen når brukeren er logget inn, og gir tilgang til å bytte passord.
"Min Side" dukker opp under innloggingsknappen når brukeren er logget inn, og gir tilgang til å bytte passord.

Sikring av "det hemmelige" innholdet

I det opprinnelige prosjektet brukte jeg Gatsbys GraphQL -datalag for å hente innhold for de beskyttede rutene, ved hjelp av Gatsbys useStaticQuery -hook. Det betydde at alt innholdet ble hentet ved bygging av nettsiden - til og med innholdet som bare skulle være tilgjengelig for innloggede brukere. Brukerne kunne ikke få tilgang til disse beskyttede rutene uten å være innlogget, men tekniske brukere kunne likevel finne innhold via nettverksfanen i nettleserens utviklingsverktøy.

For å forhindre dette måtte jeg skrive om komponentene som ble brukt i client-only-routes til å bruke Apollo Client i stedet for Gatsbys GraphQL-datalag for å hente data. Data som skal være tilgjengelig kun på klienten, hentes fra Contentful GraphQL Content API ved hjelp av Apollo Client, og ikke build-time via gatsby-source-contentful-pluginen.

For å få dette til å fungere måtte jeg gjøre endringer i både hvordan rich-text ble håndtert (siden det var forskjellig avhengig av om jeg brukte gatsby-source-contentful eller hentet innholdet dynamisk fra Contentfuls GraphQL content-API). Jeg måtte også bygge en tilpasset komponent for håndtering av bilder levert fra Contentfuls Image-API, siden Gatsby Image ikke fungerer med Contentful sitt eget API. Jeg ønsket den samme ytelsen som med Gatsby Image, og at bildene leveres i "riktige" størrelser avhengig av skjermbredde. Jeg skal ikke gå inn på alle detaljene, men du kan finne den komplette kildekoden på min Github her, og min tilpassede bildekomponent her.

I den neste delen av denne serien vil jeg gå gjennom hvordan jeg publiserer det ferdige nettstedet til Netlify, ved hjelp av kontinuerlig utrulling.

I de to siste delene av serien vil jeg vise hvordan jeg bygde dashbordet for brukeradmin som lar administratorer opprette eller oppdatere brukerne som skal ha tilgang til de beskyttede rutene til nettsiden vår.

Publisert: 02. juni 2021 (oppdatert: 15. juli 2022)
#gatsby#react#utvikling#netlify