Brukeradmin-dashbord med Gatsby Functions: Oppdatere, opprette eller slette brukere

Laptop med VS Code på skjermen.

Dette er del 2 om hvordan jeg har laget et brukeradmin-dashbord med Auth0 og Gatsby Functions. Har du ikke allerede gjort det, bør du lese del 1 først. Få også gjerne med deg starten på hele prosjektet: Slik bygget jeg nye nettsider til sameiet med Gatsby og Chakra UI

I første del gikk jeg gjennom hvordan jeg bygget et brukeradmin-dashbord med Gatsby Functions, og hvordan det hele ble satt opp hos Auth0 for at jeg skulle kunne bruke rollebasert aksesskontroll og Auth0s Management API til å presentere en oversikt over alle som er registrert som brukere på sameiets nettsider.

Funksjonaliteten for å vise alle brukerne, inkludert hvilke roller de har (user, editor eller admin) er altså på plass. Det som gjenstår er det som skal skje når du trykker knappene for å opprette en ny bruker, oppdatere en bruker eller slette en bruker.

Opprette ny brukere

Når brukeren klikker knappen Opprett ny bruker, bruker jeg Gatsbys navigate-funksjon for å sende brukeren til /user-admin/create-user.

Skjermbilde som viser tittel "Brukeradministrasjon", søkefelt og en knapp for å opprette nye brukere.

Ruten /user-admin/ og alt under der er definert som en client only-rute i konfigurasjonen til pluginen gatsby-plugin-create-client-paths i filen gatsby-config.js, slik at det ikke lages statiske sider når Gatsby-prosjektet bygges. Under /src/pages har jeg opprettet filen user-admin.tsx som inneholder den nødvendige koden for å rute brukerne til sidene for å henholdsvis opprette brukere, oppdatere brukere, eller gå til hovedsiden for brukeradministrasjon.<Privateroute>-komponenten i kodesnutten under bruker en higher order-komponent i auth0-react kalt withAutenthicationRequired til å sjekke om en bruker er logget inn eller ikke.

1// src/pages/user-admin.tsx
2
3import * as React from 'react';
4import { useAuth0 } from '@auth0/auth0-react';
5import { Router } from '@reach/router';
6import PrivateRoute from '../utils/privateRoute';
7import NotLoggedIn from '../components/notLoggedIn';
8import LoadingSpinner from '../components/loading-spinner';
9import UserAdminPage from '../components/private-components/user-admin/userAdminPage';
10import CreateUserPage from '../components/private-components/user-admin/createUserPage';
11import UpdateUserPage from '../components/private-components/user-admin/updateUserPage';
12
13function UserAdmin() {
14  const { isLoading, isAuthenticated, error } = useAuth0();
15
16  if (isLoading) {
17    return <LoadingSpinner />;
18  }
19
20  if (error) {
21    return <div>Det har oppstått en feil... {error.message}</div>;
22  }
23
24  if (!isAuthenticated) {
25    return (
26      <NotLoggedIn
27        title='Logg inn for brukeradministrasjon'
28        description='Du må logge inn for å administrere brukerkontoer for Boligsameiet Gartnerihagen. 
29      Du vil da kunne legge til, slette eller endre brukere, samt gi brukere admin-tilgang.
30      Ta kontakt med styret.'
31        redirectUser='/user-admin'
32      />
33    );
34  }
35
36  return (
37    <Router>
38      <PrivateRoute path='/user-admin/create-user' component={CreateUserPage} />
39      <PrivateRoute path='/user-admin/update-user' component={UpdateUserPage} />
40      <PrivateRoute path='/user-admin' component={UserAdminPage} />
41    </Router>
42  );
43}
44
45export default UserAdmin;

Hvis brukeren er logget inn, vises komponenten <CreateUserPage> (createUserPage.tsx) og dette skjermbildet dukker opp på skjermen:

Skjermbilde: Opprett ny bruker-dialogboks

Med Chakra UI er det enkelt å lage et skjema som ser fint ut. Reacts useState-hook brukes til å lagre alle dataene som tastes inn i skjemaet i variabelen formData, som et objekt med key/value-par for epost, navn, osv.:

1const [formData, setFormData] = useState({
2    email: '',
3    name: '',
4    password: '',
5    repeatPassword: '',
6    roles: [],
7});

Når noen endrer informasjon i ett av feltene, bruker jeg setFormData til å oppdatere tilstand/state for skjemaet. For eksempel slik for Fornavn og etternavn-feltet:

1<FormControl id='name' isRequired>
2  <FormLabel>Fornavn og etternavn</FormLabel>
3  <Input
4    value={formData.name}
5    placeholder='Fornavn Etternavn'
6    onChange={(e) =>
7      setFormData((prevState) => {
8        return {
9          ...prevState,
10          name: e.target.value,
11        };
12      })
13    }
14  />
15</FormControl>

FormControl kommer fra Chakra UI og gir litt ekstra kontroll på feltene i skjemaer, du kan lese mer om det her.

Ved endringer i feltet (onChange) bruker vi spread-operatoren for å fylle inn alle eksisterende data i formData, og så endrer vi formData.name til det som til enhver tid er tastet inn i feltet.

Når brukeren trykker Opprett-knappen, kjøres funksjonen handleSubmit. Vi starter med å validere informasjonen som er tastet inn i feltene. Her sjekker jeg om begge passordfeltene er identiske, samt at passordet som er tastet inn inneholder både tall og store og små bokstaver, og at passordet er minst 8 tegn langt:

1const handleSubmit = async (event) => {
2    event.preventDefault();
3
4    if (formData.password !== formData.repeatPassword) {
5      toast({
6        title: 'Passordene er ikke like',
7        description:
8          'Pass på at du har skrevet passordet helt likt i de to feltene.',
9        status: 'error',
10        duration: 3000,
11        isClosable: true,
12      });
13      return;
14    }
15
16    if (!formData.password.match(/((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})/)) {
17      toast({
18        title: 'Ikke sterkt nok passord',
19        description:
20          'Passordet må inneholde både tall og store og små bokstaver, og være minst 8 tegn langt.',
21        status: 'warning',
22        duration: 3000,
23        isClosable: true,
24      });
25      return;
26    }
27
28// ...resten av handleSubmit-funksjonen

Hvis passordene ikke er like, eller ikke er sterkt nok, bruker jeg Toast-komponenten til Chakra UI for å vise en liten advarsel som popper opp på skjermen noen sekunder, før den forsvinner igjen.

I skjemaet for nye brukere er det også sjekkbokser for hvilke roller den nye brukeren skal ha. Jeg lagrer hvorvidt sjekkboksene er krysset av eller ikke i variablene isAdminChecked og isEditorChecked. Dermed kan jeg gjøre følgende i handleSubmit-funksjonen for å oppdatere roles-arrayet i formData med alle rollene vi vil brukeren skal få:

1formData.roles = ['user'];
2if (isAdminChecked) {
3  formData.roles.push('admin');
4}
5if (isEditorChecked) {
6  formData.roles.push('editor');
7}

Alle brukere skal ha rollen 'user', derfor trenger vi bare å sjekke for admin og editor (og legge til 'user' for alle).

Kontakte serverless-funksjonen for å opprette brukeren

Vi har nå oppdatert formData så det inneholder et JavaScript-objekt med alle nødvendige data om brukeren. Det kan for eksempel se slik ut:

1{
2    email: 'ola@nordmann.no',
3    name: 'Ola Nordmann',
4    password: 'InnmariBraPassord123',
5    repeatPassword: 'InnmariBraPassord123',
6    roles: ['user', 'admin'],
7}

Vi kan nå kalle en Gatsby Function (serverless function) vi har kalt create-user for å opprette brukeren. Den ligger i likhet med alle Gatsby Functions under /src/api/. Her er kodesnutten som gjør dette (vi er fortsatt i handleSubmit-funksjonen i <CreateUserPage>-komponenten på klienten:

1try {
2      const accessToken = await getAccessTokenSilently(opts);
3      const api = await fetch(`/api/admin-users/create-user`, {
4        method: 'POST',
5        headers: {
6          'Content-Type': 'application/json',
7          Authorization: `Bearer ${accessToken}`,
8        },
9
10        body: JSON.stringify(formData),
11      });
12
13      if (api?.status !== 200) {
14        throw new Error(`${api.statusText} (${api.status})`);
15      }
16
17      const isJson = api.headers
18        .get('content-type')
19        ?.includes('application/json');
20
21      const data = isJson && (await api.json());
22
23      if (!data) {
24        throw new Error('no_data');
25      }
26
27      if (data.error) {
28        const { error_description } = JSON.parse(data?.error_description);
29        throw new Error(`${data.error} : ${JSON.stringify(error_description)}`);
30      }
31
32      // Store the API response (e.g. the user data for the newly created user)
33      setResponse(data?.body?.user);
34
35      setShowLoadingButton(false);
36    } catch (error) {
37      if (
38        error.message.includes(
39          'Consent required' || 'Forbidden (403)' || 'access_denied'
40        )
41      ) {
42        getToken();
43      }
44
45      if (error.message === 'Conflict (409)') {
46        toast({
47          title: 'Brukeren eksistererer allerede',
48          description:
49            'Hver bruker må ha en unik epost-adresse og et unikt navn.',
50          status: 'error',
51          duration: 3000,
52          isClosable: true,
53        });
54      } else {
55        toast({
56          title: 'Noe gikk galt',
57          description: `${error.message}`,
58          status: 'error',
59          duration: 3000,
60          isClosable: true,
61        });
62      }
63      setResponse(null);
64      setShowLoadingButton(false);
65    }

Hvis vi starter på toppen: Først henter vi brukerens access token fra Auth0 med getAccessTokenSilently(opts). opts har vi satt lenger opp i koden:

1const opts = {
2    audience: 'https://useradmin.gartnerihagen-askim.no',
3    scope: 'create:users read:roles create:role_members',
4  };

Scope er hvilke tillatelser brukeren må ha, og her trenger vi altså tillatelser til å opprette en bruker og oppdatere rollene til brukeren. Se også Auth0s dokumentasjon på hvordan du kaller et beskyttet API med et access token her.

Vi har nå mottatt et access token som vi kan sende med i autorisasjons-headeren når vi kaller create-user-API-et vårt. I tillegg sender vi med formData — altså infoen om brukeren vi vil opprette — i body:

1const api = await fetch(`/api/admin-users/create-user`, {
2    method: 'POST',
3    headers: {
4      'Content-Type': 'application/json',
5      Authorization: `Bearer ${accessToken}`,
6    },
7
8    body: JSON.stringify(formData),
9});

Serverless-funksjonen for å opprette brukere

Vår serverless-funksjon create-user vil nå motta en POST-request der access-tokenet ligger i autorisasjons-headeren og info om brukeren som skal opprettes ("payload") ligger i body.

Før vi kaller Auth0 sitt Management API for å opprette brukeren, er det lurt å sjekke litt først. Jeg bruker biblioteket @serverless-jwt/jwt-verifier for å lese access-tokenet som klienten nettopp sendte, og så bruker jeg jwt.verifyAccessToken for å sjekke at det er et gyldig token. Jeg henter også ut alle tillatelsene (scopes) fra access-tokenet, og sjekker at brukeren faktisk har de tillatelsene vedkommende trenger for å opprette en bruker. Her sjekker jeg kun for scopet create:users, siden jeg tenker at hvis brukeren har denne tillatelsen så er det ikke nødvendig å sjekke om vedkommende har tillatelse til også å lese roller eller tilegne brukeren roller. Det ligger i kortene.

Her er første del av serverless-funksjonen vår:

1// src/api/admin-users/create-user.ts
2
3import { GatsbyFunctionRequest, GatsbyFunctionResponse } from 'gatsby';
4const ManagementClient = require('auth0').ManagementClient;
5const {
6  JwtVerifier,
7  JwtVerifierError,
8  getTokenFromHeader,
9} = require('@serverless-jwt/jwt-verifier');
10
11const ALLOWED_ROLES = ['user', 'admin', 'editor'];
12
13const jwt = new JwtVerifier({
14  issuer: `https://${process.env.GATSBY_AUTH0_DOMAIN}/`,
15  audience: `https://${process.env.AUTH0_USERADMIN_AUDIENCE}`,
16});
17
18export default async function handler(
19  req: GatsbyFunctionRequest,
20  res: GatsbyFunctionResponse
21) {
22  let claims, permissions;
23  const token = getTokenFromHeader(req.headers.authorization);
24  const userRoles = req.body.roles;
25
26  if (req.method !== `POST`) {
27    return res.status(405).json({
28      error: 'method not allowed',
29      error_description: 'You should do a POST request to access this',
30    });
31  }
32
33  userRoles.forEach((role) => {
34    if (!ALLOWED_ROLES.includes(role)) {
35      return res.status(403).json({
36        error: 'invalid user role',
37        error_description: 'Serveren mottok en ugyldig brukerrolle',
38      });
39    }
40  });
41
42  // Verify access token
43  try {
44    claims = await jwt.verifyAccessToken(token);
45    permissions = claims.permissions || [];
46  } catch (err) {
47    if (err instanceof JwtVerifierError) {
48      return res.status(403).json({
49        error: `Something went wrong. ${err.code}`,
50        error_description: `${err.message}`,
51      });
52    }
53  }
54
55  // check if user should have access at all
56  if (!claims || !claims.scope) {
57    return res.status(403).json({
58      error: 'access denied',
59      error_description: 'You do not have access to this',
60    });
61  }
62
63  // Check the permissions
64  if (!permissions.includes('create:users')) {
65    return res.status(403).json({
66      error: 'no create access',
67      status_code: res.statusCode,
68      error_description:
69        'Du må ha admin-tilgang for å opprette brukere. Ta kontakt med styret.',
70      body: {
71        data: [],
72      },
73    });
74  }
75
76// ...create-user.ts fortsetter

Hvis vi har kommet så langt, er det ikke noe i veien for at vi ikke skal få lov til å opprette den nye brukeren. Vi oppretter en ny Auth0 ManagementClient:

1const auth0 = new ManagementClient({
2  domain: `${process.env.GATSBY_AUTH0_DOMAIN}`,
3  clientId: `${process.env.AUTH0_BACKEND_CLIENT_ID}`,
4  clientSecret: `${process.env.AUTH0_BACKEND_CLIENT_SECRET}`,
5  scope: 'create:users read:roles create:role_members',
6});

Så lager vi en konstant userData som inneholder et objekt med data om brukeren som vi henter fra req.body. Det som ligger under connection nedenfor er navnet på databasen hos Auth0 som brukes til å lagre alle brukerne.

1const userData = {
2  connection: 'Username-Password-Authentication',
3  email: req.body.email,
4  name: req.body.name,
5  password: req.body.password,
6  verify_email: false,
7  email_verified: false,
8};

Vi kan nå opprette brukeren med createUser-metoden fra Auth0 Management API-SDK-et (alt dette putter vi i en try/catch-blokk så vi får feilhåndtering):

1const newUser = await auth0.createUser(userData);

Med mindre noe gikk galt, er brukeren nå opprettet hos Auth0 (og ligger i Username-Password-Authentication-databasen på Auth0s servere). Men vi er ikke helt i mål ennå, vi må gi den nye brukeren de rollene vi huket av i skjemaet på klienten. Da trenger vi ytterligere et par metoder fra Auth0s Management-API: getRoles for å hente alle rollene som er definert hos Auth0, og assignRolesToUser for å tilegne roller:

1const allRoles = await auth0.getRoles();
2let rolesToAdd = [];
3allRoles.forEach((role) => {
4  if (userRoles.includes(role.name)) {
5    rolesToAdd.push(role.id);
6  }
7});
8await auth0.assignRolestoUser(
9  {
10    id: newUser.user_id,
11  },
12  {
13    roles: rolesToAdd,
14  }
15);

Først henter vi alle roller med getRoles og lagrer dette i konstanten allRoles. Deretter lager vi et nytt, tomt array, rolesToAdd, som skal inneholde alle rollene vi til slutt skal legge til for brukeren. Vi bruker så forEach for å gå over alle rollene som finnes hos Auth0, og så sjekke om rollen også finnes i userRoles (som vi i starten av koden hentet fra req.body.roles). Hvis så er tilfelle, legger vi til denne rollen i rolesToAdd-arrayet. Legg merke til at vi må bruke ID-en og ikke navnet til rollen, ettersom metoden assignRolesToUser krever dette.

Når rolesToAdd-arrayet er fylt opp med alle rolle-ID-ene brukeren skal ha, kaller vi assignRolesToUser med ID-en til den nye brukeren (som vi fikk da vi kalte createUser) og arrayet med alle rollene som skal legges til.

Gikk alt som det skulle, returnerer vi den nye brukeren og rollene tilbake til klienten — som en bekreftelse på at brukeren er opprettet:

1res.status(200).json({
2    body: {
3      status_code: 200,
4      status_description: 'Ny bruker er opprettet',
5      user: { ...newUser, roles: userRoles },
6    },
7  });

Hele kildekoden til create-user finner du på min Github her.

Bekreftelse på at ny bruker er opprettet

Når klienten (dvs. <CreateUserPage>-komponenten) mottar responsen fra API-et, sjekker jeg først at vi mottok HTTP-statuskode 200, som indikerer at alt er OK. Dette gjør jeg inni en try/catch-blokk, slik at jeg kan bruke throw new Error() og håndtere feilen (jeg bruker Toast-komponenten til Chakra UI for å vise en fornuftig feilmelding).

Hvis alt gikk som det skulle, mottar jeg data om den nye brukeren fra API-et, og jeg bruker useState-hooken i React til å lagre data om brukeren i variabelen response slik: setResponse(data?.body?.user)

Til slutt bruker jeg en modal-komponent i Chakra UI for å vise en bekreftelse på at brukeren er opprettet, med bruker-informasjonen vi nettopp fikk fra API-et:

Skjermbilde: Ny bruker er opprettet.

Oppdatering av brukere

Oppdatering av brukere er ikke så veldig forskjellig. Når vi trykker på knappen "Endre bruker" på en av brukerne i brukeradmin-dashbordet navigerer vi til ruten /user-admin/update-user med Gatsbys navigate-funksjon og sender med data om den aktuelle brukeren som propertyen state. Fra komponenten userAdminPage.tsx:

1onClick={() =>
2    navigate('/user-admin/update-user', {
3      state: userToShow,
4    })
5}

Hvis brukeren er autentisert, lastes komponenten UpdateUserPage via Reach Router som er innebygget i Gatsby. Vi får tilgang til brukerdataene via props.location.state slik: const userToModify = props?.location?.state.

Deretter bruker jeg useState-hooken i React til å sette opp en variabel med et objekt som skal inneholde nåværende state for brukeren jeg skal oppdatere:

1const [userDataForm, setUserDataForm] = useState({
2    created_at: '',
3    last_login: '',
4    email: '',
5    name: '',
6    picture: '',
7    roles: [],
8    user_id: '',
9  });

Første gang komponenten rendres, setter jeg userDataForm til å være lik brukerdataene vi nettopp fikk via props.location.state, slik:

1useEffect(() => {
2  setUserDataForm({
3    ...userToModify,
4    roles: [...userToModify.roles],
5  });
6}, []);

Jeg skal ikke gå i detalj på alt jeg gjør på klienten for å oppdatere brukeren, det handler stort sett bare om å forhåndsutfylle et skjema med info om brukeren vi skal redigere, og så ved endringer på noen av feltene i skjemaet bruke setUserDataForm til å oppdatere state (omtrent som vi gjorde i skjemaet for oppretting av ny bruker). Dette er skjermbildet man får opp når man trykker Endre bruker:

Skjermbilde: oppdater bruker.

Hele kildekoden til komponenten UpdateUserPage finner du på min Github.

Kalle serverless-funksjonen for å oppdatere brukeren

Jeg lagde en handleSubmit-funksjon som kalles når vi trykker "Oppdater"-knappen i skjemaet. handleSubmit henter først access-tokenet til den innloggede brukeren, og jeg spesifiserer at brukeren trenger tillatelsene update:users, read:roles og create:role_members. Så gjør vi en PATCH-request til vår serverless Gatsby Function update-user, og sender access-tokenet i autentiserings-headeren og de oppdaterte dataene om brukeren (userDataForm) som body:

1const opts = {
2  audience: 'https://useradmin.gartnerihagen-askim.no',
3  scope: 'update:users read:roles create:role_members',
4};
5
6try {
7      const accessToken = await getAccessTokenSilently(opts);
8      const api = await fetch(`/api/admin-users/update-user`, {
9        method: 'PATCH',
10        headers: {
11          'Content-Type': 'application/json',
12          Authorization: `Bearer ${accessToken}`,
13        },
14
15        body: JSON.stringify(userDataForm),
16      });
17
18// ...resten av koden

Serverless-funksjonen for å oppdatere brukeren

I Gatsby-funksjonen update-user gjør vi mye av det samme som vi gjorde da vi skulle opprette en bruker. Vi verifiserer access-tokenet og sjekker at klienten som kaller API-et har de nødvendige tillatelsene. Så bruker vi Auth0s Management API-SDK til å opprette en ny ManagementClient som vi kaller auth0, og deretter auth0.updateUser() for å oppdatere brukeren. updateUser() krever at du sender inn ID-en til brukeren du vil oppdatere som parameter, sammen med de oppdaterte brukerdataene:

1const updatedUser = await auth0.updateUser(
2  { id: req.body.user_id },
3  userData
4);

I tillegg må vi legge til og/eller fjerne roller fra brukeren. Jeg lager her et tomt array jeg kaller rolesToRemove og et annet jeg kaller rolesToAdd. Så går jeg over alle roller som er definert hos Auth0, og ser om rollene eksisterer eller ikke, og bruker henholdsvis auth0.assignRolesToUser og auth0.removeRolesFromUser for å legge til eller fjerne roller. Til slutt returnerer API-et vårt info om den oppdaterte brukeren og hvilke roller som er fjernet eller lagt til. Hvis noe går galt (for eksempel hvis Auth0s Management API klager på noe), fanges dette opp av catch-blokken — som returnerer feilinformasjon til klienten. Isåfall bruker jeg Toast-komponenten til Chakra UI for å gi en forhåpentligvis meningsfull feilmelding til brukeren.

Her er resten av backend-koden som oppdaterer brukeren og rollene til brukeren:

1// src/api/admin-users/update-user.ts
2
3  const auth0 = new ManagementClient({
4    domain: `${process.env.GATSBY_AUTH0_DOMAIN}`,
5    clientId: `${process.env.AUTH0_BACKEND_CLIENT_ID}`,
6    clientSecret: `${process.env.AUTH0_BACKEND_CLIENT_SECRET}`,
7    scope: 'update:users read:roles create:role_members',
8  });
9
10  const userData = {
11    connection: 'Username-Password-Authentication',
12    email: req.body.email,
13    name: req.body.name,
14  };
15
16  try {
17    const updatedUser = await auth0.updateUser(
18      { id: req.body.user_id },
19      userData
20    );
21    const allRoles = await auth0.getRoles();
22
23    let rolesToRemove = [];
24    allRoles.forEach((role) => {
25      if (!userRoles.includes(role.name)) {
26        rolesToRemove.push(role.id);
27      }
28    });
29
30    let rolesToAdd = [];
31    allRoles.forEach((role) => {
32      if (userRoles.includes(role.name)) {
33        rolesToAdd.push(role.id);
34      }
35    });
36
37    if (rolesToAdd.length > 0) {
38      await auth0.assignRolestoUser(
39        {
40          id: req.body.user_id,
41        },
42        {
43          roles: rolesToAdd,
44        }
45      );
46    }
47
48    if (rolesToRemove.length > 0) {
49      await auth0.removeRolesFromUser(
50        {
51          id: req.body.user_id,
52        },
53        {
54          roles: rolesToRemove,
55        }
56      );
57    }
58
59    res.status(200).json({
60      body: {
61        status_code: 200,
62        status_description: 'Bruker er oppdatert',
63        user: updatedUser,
64        roles_removed: rolesToRemove,
65        roles_added: rolesToAdd,
66      },
67    });
68  } catch (error) {
69    res.status(error.statusCode).json({
70      error: error.name,
71      status_code: error.statusCode || 500,
72      error_description: error.message,
73    });
74  }

Hele greia finner du på Github her.

Slette brukere

Sletting av brukere er ikke så ulikt. Hvis noen trykker Slett bruker-knappen, lagrer jeg bruker-ID-en og navnet til brukeren som skal slettes i userToDelete og viser en advarsel (her bruker jeg AlertDialog-komponenten til Chakra UI).

Skjermbilde: Dialogbokser for å slette bruker

Slette brukere

Bekrefter du at du er sikker på at brukeren skal slettes, kaller jeg funksjonen handleDeleteUser som igjen henter den innloggede brukerens access-token og ber om tillatelsen delete:users. Så gjøres en DELETE-request til vår serverless-funksjon delete-user, med access-tokenet i autorisasjons-headeren og userToDelete i body:
1const handleDeleteUser = async () => {
2    const opts = {
3      audience: 'https://useradmin.gartnerihagen-askim.no',
4      scope: 'delete:users',
5    };
6
7    try {
8      if (!userToDelete.id.includes('auth0')) {
9        throw new Error('User ID is not valid');
10      }
11
12      const accessToken = await getAccessTokenSilently(opts);
13      const api = await fetch(`/api/admin-users/delete-user`, {
14        method: 'DELETE',
15        headers: {
16          'Content-Type': 'application/json',
17          Authorization: `Bearer ${accessToken}`,
18        },
19
20        body: JSON.stringify({ idToDelete: userToDelete.id }),
21      });
22
23// ... resten av koden

Serverless-funksjonen (API-et vårt) for å slette brukere sjekker så at access-tokenet er gyldig, at brukeren har rettigheter til å slette brukere, samt at bruker-ID-en som er sendt med i req.body.idToDelete er gyldig. Er den det, er det å slette en bruker i Auth0 så enkelt som å opprette en ny Auth0 ManagementClient som vi kaller auth0, og så kalle auth0.deleteUser(). Slik:

1// src/api/admin-users/delete-user.ts
2
3.
4.
5.
6
7const auth0 = new ManagementClient({
8    domain: `${process.env.GATSBY_AUTH0_DOMAIN}`,
9    clientId: `${process.env.AUTH0_BACKEND_CLIENT_ID}`,
10    clientSecret: `${process.env.AUTH0_BACKEND_CLIENT_SECRET}`,
11    scope: 'delete:users',
12  });
13
14  try {
15    const idToDelete = req.body.idToDelete;
16
17    if (!idToDelete || !idToDelete.includes('auth0')) {
18      const error = {
19        name: 'bad user id',
20        statusCode: 400,
21        message: 'Manglende bruker-id eller feil format',
22      };
23      throw error;
24    }
25
26    await auth0.deleteUser({ id: idToDelete });
27
28    res.status(200).json({
29      body: {
30        status_code: 200,
31        status_description: 'Bruker er slettet',
32      },
33    });
34  } catch (error) {
35    res.status(error.statusCode || 500).json({
36      error: error.name,
37      status_code: error.statusCode || 500,
38      error_description: error.message,
39    });
40  }

Hvis alt går som det skal, returneres statuskode 200. Vi sjekker dette på klienten (i userAdminPage.tsx) og bruker Chakra UIs Toast-funksjon for å varsle om at brukeren er slettet.

Hva nå?

Puh! Jeg er i mål og har nå en nettside med brukeradministrasjon!

Jeg har ikke gått i detalj på alle kriker og kroker av hvordan denne løsningen er bygget opp, men oppfordrer i stedet til å ta en kikk på kildekoden som ligger på min Github.

Da jeg startet jobben med å lage nye nettsider til sameiet, tenkte jeg at dette skulle være ganske fort gjort — men er det én ting jeg har lært av prosjektet, så er det at en slik nettside aldri blir ferdig. Det er alltid ett eller annet som kan forbedres.

Jeg kommer nok til å bruke litt tid innimellom til å refaktorere og forbedre koden for å gjøre ting litt ryddigere, og så er planen også å lage en serverless-funksjon som automatisk varsler registrerte brukere når nytt innhold publiseres. Her kommer jeg til sette opp en webhook hos Contentful som kaller en Gatsby Function som bruker Sendgrids node-løsning til å sende en epost med lenke til det nye innholdet.

Har du tilbakemeldinger på noe av det jeg har gjort, eller forslag til ting som kan forbedres, bruk gjerne kommentarfeltet under artiklene mine på lekanger.no — eller send meg en epost på kurt@lekanger.no.

Du finner all kildekoden til hele løsningen på https://github.com/klekanger/gartnerihagen

Denne YouTube-videoen viser hvordan brukergrensesnittet og nettsidene ser ut live: https://youtu.be/XzkTRw5D5mg

Publisert: 05. september 2021 (oppdatert: 15. juli 2022)
#gatsby#javascript#utvikling#react