diff --git a/docs/backend-guide.md b/docs/backend-guide.md new file mode 100644 index 0000000..a3aedf1 --- /dev/null +++ b/docs/backend-guide.md @@ -0,0 +1,273 @@ +# Slik kobler du deg til backend + + +Backend er en GraphQL-server (Node, Express 5, Apollo Server 5) som snakker med PostgreSQL via Prisma. + +| | Adresse | +|---|---| +| Lokalt | `http://localhost:3001/graphql` | +| VM (via Apache) | `http://it2810-06.idi.ntnu.no/project2/graphql` | + +Spør meg (Matilde) før du endrer noe i `server/prisma/schema.prisma` eller i `.graphql`-filene. Alle bygger mot dem. + +--- + +## 1. Kom i gang lokalt (gjøres én gang) + +Du trenger Node og PostgreSQL på egen maskin. På Mac: `brew install postgresql@16 && brew services start postgresql@16`. + +```bash +git checkout main && git pull +npm install # i prosjektroten + +createdb oppskrifter_dev # egen lokal database + +cd server +cp .env.example .env # lag din egen .env (committes ALDRI) +``` + +Rediger `server/.env`. **Ikke bruk anførselstegn** rundt verdiene: + +``` +DATABASE_URL=postgresql://DITT_BRUKERNAVN@localhost:5432/oppskrifter_dev +PORT=3001 +``` + +På Mac er `DITT_BRUKERNAVN` normalt macOS-brukernavnet ditt (se `whoami`), og da trengs ikke passord. Test gjerne URL-en direkte: `psql "postgresql://DITT_BRUKERNAVN@localhost:5432/oppskrifter_dev"`. + +```bash +npx prisma migrate dev # lager tabellene +npx prisma db seed # legger inn testdata +npm run dev # starter serveren +``` + +Åpne `http://localhost:3001/graphql`. Du skal få opp Apollo Sandbox, og der kan du teste spørringer uten frontend: + +```graphql +query { + recipes { + id + title + tags { name } + } +} +``` + +Kjør `npx prisma studio` for å se og redigere innholdet i databasen i nettleseren. + +### Hver gang du har hentet ny kode (`git pull`) + +```bash +npm install +cd server && npx prisma migrate dev # hvis noen har endret databaseskjemaet +``` + +--- + +## 2. Hva backend kan nå + +Hva den faktisk gjør står i `server/src/schema/typeDefs/recipe.graphql`. Les den hvis noe ikke stemmer med denne guiden. + +**Queries** +- `recipes(search, category, page, pageSize)` gir oppskrifter, nyeste først, kun `PUBLISHED` +- `recipe(id)` gir én oppskrift med ingredienser, steg og tags + +**Mutations** +- `createRecipe(input)` lagrer en oppskrift med ingredienser, steg og tags + +> Planlagt endring: `recipes` skal returnere `{ items, totalCount }` slik at paginering kan fungere. Skal fikse og si ifra før jeg gjør dette. + +Eksempel i Sandbox: + +```graphql +mutation { + createRecipe(input: { + title: "Test" + description: "Test" + prepTimeMinutes: 10 + servings: 2 + category: "middag" + ingredients: [{ name: "Pasta", amount: 200, unit: "g", order: 1 }] + steps: [{ order: 1, instruction: "Kok pasta" }] + tagNames: ["vegetar"] + }) { id title } +} +``` + +Foreløpig settes forfatteren til en fast testbruker (`dev-user`). Dette byttes til innlogget bruker når auth er på plass (se del 5). + +--- + +## 3. Bruke backend fra frontend (Apollo Client) + +Apollo Client er installert i `client/`. Sjekk versjonen med `npm list @apollo/client`: importstiene for hooks har endret seg mellom versjoner. Med nyeste versjon importeres hooks fra `@apollo/client/react`, mens `gql` kommer fra `@apollo/client`. + +```tsx +import { gql } from '@apollo/client'; +import { useQuery, useMutation } from '@apollo/client/react'; + +const RECIPES = gql` + query Recipes($search: String) { + recipes(search: $search) { id title prepTimeMinutes } + } +`; + +function RecipeList() { + const { data, loading, error } = useQuery(RECIPES, { variables: { search: '' } }); + if (loading) return

Laster…

; + if (error) return

Noe gikk galt: {error.message}

; + return ; +} +``` + +Tall må sendes som tall (`Number(...)`), og `order` på ingredienser og steg settes fra posisjonen i listen (1, 2, 3 …). + +Adressen settes i `client/src/lib/apolloClient.ts` fra miljøvariabelen `VITE_GRAPHQL_URL`, med `http://localhost:3001/graphql` som standard. På VM-en brukes `/project2/graphql` (satt i `client/.env.production`), slik at kall går gjennom Apache og slipper CORS-problemer. + +--- + +## 4. Endre databasen eller skjemaet + +1. Endre `server/prisma/schema.prisma`. +2. `npx prisma migrate dev --name kort_beskrivelse` lager en migrasjon i `server/prisma/migrations/`. +3. **Commit migrasjonsmappen**. Uten den får ikke de andre (eller VM-en) endringen. +4. Oppdater `.graphql`-filene og resolverne hvis API-et skal endres. +5. Si fra i gruppa. Alle må kjøre `npx prisma migrate dev` etter neste pull. + + +--- + +## 5. Google-innlogging (shoutout Mari) + +### Sjekk dette først (spike, gjør det før alt annet) + +Google godtar vanligvis bare `https://` som tillatt opprinnelse (*Authorized JavaScript origins*), med unntak av `localhost`. VM-en kjører på `http://it2810-06.idi.ntnu.no`, så innlogging kan fungere lokalt, men bli avvist på VM-en. Opprett OAuth-klient i Google Cloud Console og forsøk å legge inn VM-adressen tidlig. vi burde nok snakke med LA om VM-en kan få HTTPS. + +**Reserveplan hvis det ikke går:** en enkel innlogging uten Google (f.eks. bare brukernavn eller e-post) som lager samme `User`-rad og samme sesjon. Resten av løsningen blir lik, så skriv auth-koden slik at selve identitetsbekreftelsen er ett utbyttbart steg. + +### Tenkt flyt, maybe vurder selv + +1. Frontend viser Google-knappen og får en **ID-token** (`credential`) fra Google. +2. Frontend sender tokenet til backend i en mutation, `loginWithGoogle(idToken)`. +3. Backend **verifiserer tokenet** hos Google, finner eller oppretter `User`, og setter en **httpOnly-cookie** med sesjonen. +4. Alle senere kall sender cookien automatisk. Backend leser den og legger brukeren i GraphQL-`context`. +5. `me`-query gir innlogget bruker (eller `null`), og frontend bruker den til å vise innloggingsstatus. + +### Backend (`server/src/auth/`) + +Pakkene `google-auth-library` og `cookie-parser` er allerede installert. For sesjonstoken trengs f.eks. `jsonwebtoken` (`npm install jsonwebtoken && npm install -D @types/jsonwebtoken`). + +Verifisering av Google-tokenet: + +```ts +import { OAuth2Client } from 'google-auth-library'; + +const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID); + +export async function verifyGoogleToken(idToken: string) { + const ticket = await client.verifyIdToken({ + idToken, + audience: process.env.GOOGLE_CLIENT_ID, + }); + const p = ticket.getPayload(); + if (!p?.sub || !p.email) throw new Error('Ugyldig Google-token'); + return { googleId: p.sub, email: p.email, name: p.name ?? p.email }; +} +``` + +`User`-modellen har allerede `googleId` (unik, valgfri), `email` og `name`. Finn eller opprett brukeren: + +```ts +const user = await prisma.user.upsert({ + where: { googleId: info.googleId }, + update: {}, + create: info, +}); +``` + +**Viktige sikkerhetsregler:** +- Verifiser alltid tokenet på backend. Stol aldri på navn eller e-post sendt fra frontend. +- Cookien skal være `httpOnly` og `sameSite: 'lax'`. Sett `secure: true` bare når siden kjører på HTTPS. +- `GOOGLE_CLIENT_ID` og `SESSION_SECRET` legges i `server/.env` og navnene i `.env.example`. **Aldri verdiene i git.** Client ID er ikke hemmelig, men en eventuell *client secret* og `SESSION_SECRET` er det. + +### Få auth inn i GraphQL + +Dagens `index.ts` leser bare `recipe.graphql` og bruker bare `recipeResolvers`. Du må utvide den slik at flere filer kan slås sammen: + +- Lag `typeDefs/user.graphql`. Siden `Query` og `Mutation` allerede er definert i `recipe.graphql`, må du bruke **`extend type`** i den nye filen: + + ```graphql + type User { id: ID! name: String! email: String! } + + extend type Query { me: User } + extend type Mutation { loginWithGoogle(idToken: String!): User! logout: Boolean! } + ``` + +- Send inn begge filene som en liste: `new ApolloServer({ typeDefs: [recipeTypeDefs, userTypeDefs], resolvers: [recipeResolvers, userResolvers] })`. +- Gi resolverne tilgang til request og response gjennom `context`: + + ```ts + app.use( + '/graphql', + cors({ origin: 'http://localhost:5173', credentials: true }), + express.json(), + cookieParser(), + expressMiddleware(server, { + context: async ({ req, res }) => ({ req, res, userId: readSession(req) }), + }), + ); + ``` + + `origin` må være en konkret adresse, ikke `*`, når `credentials: true` brukes. Dagens `cors()` uten innstillinger fungerer ikke med cookies. + +Når `userId` finnes i context, bytter Matilde `createRecipe` fra `dev-user` til innlogget bruker. Avtal rekkefølgen dere i mellom. + +### Frontend + +Enklest med `@react-oauth/google`: + +```tsx +import { GoogleOAuthProvider, GoogleLogin } from '@react-oauth/google'; + + + login({ variables: { idToken: res.credential } })} + onError={() => console.error('Innlogging feilet')} + /> + +``` + +For at cookien skal følge med i kall til backend lokalt (frontend på port 5173, backend på 3001) må Apollo Client sende den med: + +```ts +new ApolloClient({ + link: new HttpLink({ uri: GRAPHQL_URL, credentials: 'include' }), + cache: new InMemoryCache(), +}); +``` + +(Sjekk HttpLink-oppsettet mot Apollo Client-versjonen som er installert.) På VM-en går frontend og backend gjennom samme adresse via Apache, og da er dette uproblematisk. + +Etter innlogging og utlogging kan du kalle `client.resetStore()` slik at bufrede data fra forrige bruker ikke blir stående. + +### Foreslått rekkefølge + +1. Opprett Google-klient og test om VM-adressen aksepteres. +2. `verifyGoogleToken` + `loginWithGoogle` + `me` i backend, testet i Sandbox. +3. Sesjonscookie og `context`. +4. Knapp og innloggingsstatus i frontend. +5. Beskytte oppskriftsopprettelse (avtales med Matilde). + +--- + +## 6. Feilsøking + +| Symptom | Sannsynlig årsak | +|---|---| +| `curl: Failed to connect` til port 3001 | Serveren kjører ikke. Start med `npm run dev` og les feilmeldingen. | +| `P1000` / `P1010` ved migrate eller seed | Feil bruker, passord eller databasenavn i `DATABASE_URL`, eller databasen finnes ikke (`psql -l`). | +| `Cannot query field "x" on type "y"` | Skjemaet har endret seg. Gjør `git pull` og restart serveren. | +| CORS-feil i nettleseren | Frontend-adressen er ikke tillatt i `cors({ origin })`. Husk også `credentials`. | +| Cookie settes ikke / `me` er alltid `null` | Mangler `credentials: 'include'` i klienten, eller `credentials: true` i CORS. | +| `Argument ... is missing` / `Foreign key` ved `createRecipe` | Forfatteren (`dev-user`) finnes ikke. Kjør `npx prisma db seed`. | +| Alt fungerer lokalt, men ikke på VM | Husk `./deploy/update.sh`, og at `.env` på VM-en er satt riktig. |