Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 273 additions & 0 deletions docs/backend-guide.md
Original file line number Diff line number Diff line change
@@ -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 <p>Laster…</p>;
if (error) return <p>Noe gikk galt: {error.message}</p>;
return <ul>{data.recipes.map((r) => <li key={r.id}>{r.title}</li>)}</ul>;
}
```

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';

<GoogleOAuthProvider clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>
<GoogleLogin
onSuccess={(res) => login({ variables: { idToken: res.credential } })}
onError={() => console.error('Innlogging feilet')}
/>
</GoogleOAuthProvider>
```

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. |