From 02bef529c1a6b3ff24abec017c25f0e408241471 Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Tue, 13 Oct 2026 22:35:30 +0200 Subject: [PATCH 1/5] innlogging backend --- server/.env.example | 3 +- server/package.json | 1 + server/src/auth/session.ts | 52 +++++++++++++++++ server/src/index.ts | 31 +++++++++- server/src/schema/resolvers/user.ts | 75 +++++++++++++++++++++++++ server/src/schema/typeDefs/user.graphql | 18 ++++++ 6 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 server/src/auth/session.ts create mode 100644 server/src/schema/resolvers/user.ts create mode 100644 server/src/schema/typeDefs/user.graphql diff --git a/server/.env.example b/server/.env.example index 6a0f1a1..b34fa0b 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,2 +1,3 @@ DATABASE_URL= -PORT= \ No newline at end of file +PORT= +JWT_SECRET= \ No newline at end of file diff --git a/server/package.json b/server/package.json index affc4e8..6569543 100644 --- a/server/package.json +++ b/server/package.json @@ -37,6 +37,7 @@ "@as-integrations/express5": "^1.1.2", "@prisma/client": "^6.19.3", "bcryptjs": "^3.0.3", + "cookie-parser": "^1.4.7", "cors": "^2.8.6", "express": "^5.2.1", "graphql": "^16.14.2", diff --git a/server/src/auth/session.ts b/server/src/auth/session.ts new file mode 100644 index 0000000..b88f2b3 --- /dev/null +++ b/server/src/auth/session.ts @@ -0,0 +1,52 @@ +import jwt from 'jsonwebtoken'; +import type { Request, Response } from 'express'; + +// navnet på cookien som holder innloggingen +const COOKIE_NAME = 'token'; +// hvor lenge man er logget inn i millisekunder +const SEVEN_DAYS_MS = 604800000; + +export type Context = { + req: Request; + res: Response; + userId: string | null; +}; + +// henter nøkkelen fra .env og stopper hvis den mangler +function getSecret() { + const secret = process.env.JWT_SECRET; + if (!secret) { + throw new Error('JWT_SECRET mangler i server/.env'); + } + return secret; +} + +// lager en signert billett med bruker id og legger den i en cookie +export function setLoginCookie(res: Response, userId: string) { + const token = jwt.sign({ userId }, getSecret(), { expiresIn: '7d' }); + res.cookie(COOKIE_NAME, token, { + httpOnly: true, + sameSite: 'lax', + maxAge: SEVEN_DAYS_MS, + }); +} + +// fjerner cookien ved utlogging +export function clearLoginCookie(res: Response) { + res.clearCookie(COOKIE_NAME); +} + +// leser cookien og finner bruker id, null hvis ingen eller ugyldig +export function getUserId(req: Request): string | null { + const token = req.cookies?.[COOKIE_NAME]; + if (!token) return null; + + try { + const payload = jwt.verify(token, getSecret()) as { userId: string }; + return payload.userId; + } catch { + // billetten er utløpt eller noen har tullet med den + return null; + } +} + diff --git a/server/src/index.ts b/server/src/index.ts index ad8a4ff..662526e 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,20 +1,45 @@ import 'dotenv/config'; import express from 'express'; import cors from 'cors'; +import cookieParser from 'cookie-parser'; import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@as-integrations/express5'; import { readFileSync } from 'fs'; import { join } from 'path'; import { recipeResolvers } from './schema/resolvers/recipe'; +import { userResolvers } from './schema/resolvers/user'; +import { getUserId, type Context } from './auth/session'; -const typeDefs = readFileSync(join(__dirname, 'schema/typeDefs/recipe.graphql'), 'utf-8'); +const typeDefs = ['recipe.graphql', 'user.graphql'].map((file) => + readFileSync(join(__dirname, 'schema/typeDefs', file), 'utf-8'), +); + +const resolvers = { + Query: { ...recipeResolvers.Query, ...userResolvers.Query }, + Mutation: { ...recipeResolvers.Mutation, ...userResolvers.Mutation }, +}; + +// for å teste med Apollo Sandbox +const corsOptions = { + origin: ['https://sandbox.embed.apollographql.com', 'https://studio.apollographql.com'], + credentials: true, +}; async function start() { const app = express(); - const server = new ApolloServer({ typeDefs, resolvers: recipeResolvers }); + const server = new ApolloServer({ typeDefs, resolvers }); await server.start(); - app.use('/graphql', cors(), express.json(), expressMiddleware(server)); + app.use( + '/graphql', + cors(corsOptions), + cookieParser(), + express.json(), + expressMiddleware(server, { + // finner hvem som er logget inn for hver forespørsel + context: async ({ req, res }) => ({ req, res, userId: getUserId(req) }), + }), + ); app.listen(3001, () => console.log('Server kjører på http://localhost:3001/graphql')); } diff --git a/server/src/schema/resolvers/user.ts b/server/src/schema/resolvers/user.ts new file mode 100644 index 0000000..2e633d1 --- /dev/null +++ b/server/src/schema/resolvers/user.ts @@ -0,0 +1,75 @@ +import bcrypt from 'bcryptjs'; +import { GraphQLError } from 'graphql'; +import { prisma } from '../../db/client'; +import { clearLoginCookie, setLoginCookie, type Context } from '../../auth/session'; + +// samme krav som i frontend +const MIN_PASSWORD_LENGTH = 6; + +type Credentials = { username: string; password: string }; + +export const userResolvers = { + Query: { + // innlogget bruker og null hvis ingen er logget inn + me: async (_: unknown, __: unknown, context: Context) => { + if (!context.userId) return null; + return prisma.user.findUnique({ where: { id: context.userId } }); + }, + }, + + Mutation: { + // lager ny bruker og logger inn + register: async (_: unknown, args: Credentials, context: Context) => { + const username = args.username.trim(); + + // sjekker på nytt her + if (!username || args.password.length < MIN_PASSWORD_LENGTH) { + throw new GraphQLError('Ugyldig brukernavn eller passord.', { + extensions: { code: 'BAD_USER_INPUT' }, + }); + } + + const existing = await prisma.user.findUnique({ where: { username } }); + if (existing) { + throw new GraphQLError('Brukernavnet er allerede tatt.', { + extensions: { code: 'BAD_USER_INPUT' }, + }); + } + + // lagrer aldri passordet bare hashen + const passwordHash = await bcrypt.hash(args.password, 10); + const user = await prisma.user.create({ + data: { username, name: username, passwordHash }, + }); + + setLoginCookie(context.res, user.id); + return user; + }, + + // logger inn hvis brukernavn og passord stemmer + login: async (_: unknown, args: Credentials, context: Context) => { + const user = await prisma.user.findUnique({ + where: { username: args.username.trim() }, + }); + + const passwordIsCorrect = + user?.passwordHash != null && + (await bcrypt.compare(args.password, user.passwordHash)); + + if (!user || !passwordIsCorrect) { + throw new GraphQLError('Feil brukernavn eller passord.', { + extensions: { code: 'UNAUTHENTICATED' }, + }); + } + + setLoginCookie(context.res, user.id); + return user; + }, + + // fjerner cookien + logout: (_: unknown, __: unknown, context: Context) => { + clearLoginCookie(context.res); + return true; + }, + }, +}; \ No newline at end of file diff --git a/server/src/schema/typeDefs/user.graphql b/server/src/schema/typeDefs/user.graphql new file mode 100644 index 0000000..fa76a9f --- /dev/null +++ b/server/src/schema/typeDefs/user.graphql @@ -0,0 +1,18 @@ +type User { + id: ID! + username: String! + name: String! + languagePref: String! + unitSystem: String! +} + +extend type Query { + # innlogget bruker, null hvis ingen er logget inn + me: User +} + +extend type Mutation { + register(username: String!, password: String!): User! + login(username: String!, password: String!): User! + logout: Boolean! +} \ No newline at end of file From d203c89e7c6b3a8a027a939dfee92c0bfbf5cbf5 Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Tue, 13 Oct 2026 23:36:55 +0200 Subject: [PATCH 2/5] kobler auth til backend --- client/src/graphql/auth.ts | 44 +++++++++++++++ client/src/lib/AuthProvider.tsx | 94 +++++++++++++++++++-------------- client/src/lib/authContext.ts | 6 +-- client/src/types/user.ts | 3 ++ 4 files changed, 103 insertions(+), 44 deletions(-) create mode 100644 client/src/graphql/auth.ts diff --git a/client/src/graphql/auth.ts b/client/src/graphql/auth.ts new file mode 100644 index 0000000..caaff47 --- /dev/null +++ b/client/src/graphql/auth.ts @@ -0,0 +1,44 @@ +import { gql } from '@apollo/client'; + +const USER_FIELDS = gql` + fragment UserFields on User { + id + username + name + languagePref + unitSystem + } +`; + +export const ME = gql` + query Me { + me { + ...UserFields + } + } + ${USER_FIELDS} +`; + +export const LOGIN = gql` + mutation Login($username: String!, $password: String!) { + login(username: $username, password: $password) { + ...UserFields + } + } + ${USER_FIELDS} +`; + +export const REGISTER = gql` + mutation Register($username: String!, $password: String!) { + register(username: $username, password: $password) { + ...UserFields + } + } + ${USER_FIELDS} +`; + +export const LOGOUT = gql` + mutation Logout { + logout + } +`; \ No newline at end of file diff --git a/client/src/lib/AuthProvider.tsx b/client/src/lib/AuthProvider.tsx index a9db75f..e544a8a 100644 --- a/client/src/lib/AuthProvider.tsx +++ b/client/src/lib/AuthProvider.tsx @@ -1,6 +1,9 @@ import { useState, type ReactNode } from 'react'; +import { CombinedGraphQLErrors } from '@apollo/client'; +import { useMutation, useQuery } from '@apollo/client/react'; import type { User } from '../types/user'; import { AuthContext } from './authContext'; +import { LOGIN, LOGOUT, ME, REGISTER } from '../graphql/auth'; // midlertidige brukere til jeg får koblet meg til databasen const MOCK_GUEST_USER: User = { @@ -8,69 +11,78 @@ const MOCK_GUEST_USER: User = { name: 'Gjest', authProvider: 'GUEST', }; -const MOCK_TEST_USER: User = { - id: 'test', - name: 'Testbruker', - authProvider: 'TEST', + +// brukeren slik backend sender den +type BackendUser = { + id: string; + username: string; + name: string; + languagePref: string; + unitSystem: string; }; -// en konto er brukernavn, passord og brukeren som logges inn -type Account = { username: string; password: string; user: User }; +type MeData = { me: BackendUser | null }; -// midlertidig liste over kontoer til backend er koblet på -const MOCK_ACCOUNTS: Account[] = [ - { username: 'test', password: 'test123', user: MOCK_TEST_USER }, -]; +// gjør om feil fra serveren til en melding som kan vises +function getErrorMessage(error: unknown) { + if (CombinedGraphQLErrors.is(error)) { + return error.errors[0]?.message ?? 'Noe gikk galt.'; + } + return 'Fikk ikke kontakt med serveren.'; +} type AuthProviderProps = { children: ReactNode }; export function AuthProvider({ children }: AuthProviderProps) { - const [user, setUser] = useState(null); - // kontoene som finnes, nye legges til når noen lager bruker - const [accounts, setAccounts] = useState(MOCK_ACCOUNTS); + const [guest, setGuest] = useState(null); + + // spør serveren hvem som er logget inn + const { data, loading, refetch } = useQuery(ME); + const [loginMutation] = useMutation(LOGIN); + const [registerMutation] = useMutation(REGISTER); + const [logoutMutation] = useMutation(LOGOUT); + + // innlogget bruker fra backend ellers gjesten + const backendUser: User | null = data?.me + ? { ...data.me, authProvider: 'PASSWORD' } + : null; + const user = backendUser ?? guest; const continueAsGuest = () => { - setUser(MOCK_GUEST_USER); + setGuest(MOCK_GUEST_USER); }; // logger inn hvis brukernavn og passord stemmer - const login = (username: string, password: string) => { - const account = accounts.find( - (account) => - account.username === username && account.password === password, - ); - - if (account) { - setUser(account.user); - return true; + const login = async (username: string, password: string) => { + try { + await loginMutation({ variables: { username, password } }); + await refetch(); + return null; + } catch (error) { + return getErrorMessage(error); } - return false; }; - // lager ny bruker og logger inn. false hvis brukernavnet er tatt - const register = (username: string, password: string) => { - if (accounts.some((account) => account.username === username)) { - return false; + // lager ny bruker og logger inn, feilmelding hvis brukernavnet er tatt + const register = async (username: string, password: string) => { + try { + await registerMutation({ variables: { username, password } }); + await refetch(); + return null; + } catch (error) { + return getErrorMessage(error); } - - const newUser: User = { - id: username, - name: username, - authProvider: 'PASSWORD', - }; - - setAccounts([...accounts, { username, password, user: newUser }]); - setUser(newUser); - return true; }; - const logout = () => { - setUser(null); + const logout = async () => { + setGuest(null); + await logoutMutation(); + await refetch(); }; return ( {children} diff --git a/client/src/lib/authContext.ts b/client/src/lib/authContext.ts index 8e11abc..c371533 100644 --- a/client/src/lib/authContext.ts +++ b/client/src/lib/authContext.ts @@ -6,9 +6,9 @@ export type AuthContextValue = { user: User | null; loading: boolean; continueAsGuest: () => void; - login: (username: string, password: string) => boolean; - register: (username: string, password: string) => boolean; - logout: () => void; + login: (username: string, password: string) => Promise; + register: (username: string, password: string) => Promise; + logout: () => Promise; }; // felles boks for innlogget bruker export const AuthContext = createContext(null); diff --git a/client/src/types/user.ts b/client/src/types/user.ts index 1f6cbee..b45fd61 100644 --- a/client/src/types/user.ts +++ b/client/src/types/user.ts @@ -4,4 +4,7 @@ export type User = { id: string; name: string; authProvider: AuthProvider; + username?: string; + languagePref?: string; + unitSystem?: string; }; From fc96a268d623efbb1074e2ea415bf90af7bf41db Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Tue, 13 Oct 2026 23:52:32 +0200 Subject: [PATCH 3/5] =?UTF-8?q?innlogging=20og=20registrering=20bruker=20n?= =?UTF-8?q?=C3=A5=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/routes/ProfilePage.tsx | 6 +++--- client/src/routes/RegisterPage.tsx | 11 ++++++----- client/src/routes/TestUserLoginPage.tsx | 9 +++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/client/src/routes/ProfilePage.tsx b/client/src/routes/ProfilePage.tsx index 611654d..98d8c9a 100644 --- a/client/src/routes/ProfilePage.tsx +++ b/client/src/routes/ProfilePage.tsx @@ -46,9 +46,9 @@ export default function ProfilePage() { const desktopTab: RecipeTab = activeTab === 'preferences' ? 'favorites' : activeTab; - function handleLogout() { - logout(); - navigate('/'); + async function handleLogout() { + await logout(); + navigate('/login'); } return ( diff --git a/client/src/routes/RegisterPage.tsx b/client/src/routes/RegisterPage.tsx index 5e6366f..7d00bd4 100644 --- a/client/src/routes/RegisterPage.tsx +++ b/client/src/routes/RegisterPage.tsx @@ -21,7 +21,7 @@ export default function RegisterPage() { const navigate = useNavigate(); const { register } = useCurrentUser(); - function handleSubmit(event: SubmitEvent) { + async function handleSubmit(event: SubmitEvent) { // hindrer at siden lastes på nytt event.preventDefault(); @@ -43,11 +43,12 @@ export default function RegisterPage() { return; } - // lager brukeren, false betyr at brukernavnet er tatt - if (register(username.trim(), password)) { - navigate('/'); + // lager brukeren og feilmelding hvis noe gikk galt + const errorMessage = await register(username.trim(), password); + if (errorMessage) { + setError(errorMessage); } else { - setError('Brukernavnet er allerede tatt.'); + navigate('/'); } } diff --git a/client/src/routes/TestUserLoginPage.tsx b/client/src/routes/TestUserLoginPage.tsx index aa70d92..8bf2729 100644 --- a/client/src/routes/TestUserLoginPage.tsx +++ b/client/src/routes/TestUserLoginPage.tsx @@ -18,7 +18,7 @@ export default function TestUserLoginPage() { // feilmelding hvis noe er feil const [error, setError] = useState(''); - function handleSubmit(event: SubmitEvent) { + async function handleSubmit(event: SubmitEvent) { // hindrer at siden lastes på nytt event.preventDefault(); @@ -29,10 +29,11 @@ export default function TestUserLoginPage() { } // sjekker brukernavn og passord - if (login(username, password)) { - navigate('/'); + const errorMessage = await login(username, password); + if (errorMessage) { + setError(errorMessage); } else { - setError('Feil brukernavn eller passord.'); + navigate('/'); } } From 0f246dc554171a40be917b1bdfc1f59920bfdc80 Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Tue, 13 Oct 2026 23:59:52 +0200 Subject: [PATCH 4/5] =?UTF-8?q?kj=C3=B8rte=20prettier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/graphql/auth.ts | 2 +- server/src/auth/session.ts | 5 ++--- server/src/index.ts | 11 ++++++++--- server/src/schema/resolvers/user.ts | 8 ++++++-- server/src/schema/typeDefs/user.graphql | 2 +- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/client/src/graphql/auth.ts b/client/src/graphql/auth.ts index caaff47..df91209 100644 --- a/client/src/graphql/auth.ts +++ b/client/src/graphql/auth.ts @@ -41,4 +41,4 @@ export const LOGOUT = gql` mutation Logout { logout } -`; \ No newline at end of file +`; diff --git a/server/src/auth/session.ts b/server/src/auth/session.ts index b88f2b3..cc6c3cd 100644 --- a/server/src/auth/session.ts +++ b/server/src/auth/session.ts @@ -25,7 +25,7 @@ function getSecret() { export function setLoginCookie(res: Response, userId: string) { const token = jwt.sign({ userId }, getSecret(), { expiresIn: '7d' }); res.cookie(COOKIE_NAME, token, { - httpOnly: true, + httpOnly: true, sameSite: 'lax', maxAge: SEVEN_DAYS_MS, }); @@ -48,5 +48,4 @@ export function getUserId(req: Request): string | null { // billetten er utløpt eller noen har tullet med den return null; } -} - +} diff --git a/server/src/index.ts b/server/src/index.ts index 662526e..b0fafe3 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -21,7 +21,10 @@ const resolvers = { // for å teste med Apollo Sandbox const corsOptions = { - origin: ['https://sandbox.embed.apollographql.com', 'https://studio.apollographql.com'], + origin: [ + 'https://sandbox.embed.apollographql.com', + 'https://studio.apollographql.com', + ], credentials: true, }; @@ -41,7 +44,9 @@ async function start() { }), ); - app.listen(3001, () => console.log('Server kjører på http://localhost:3001/graphql')); + app.listen(3001, () => + console.log('Server kjører på http://localhost:3001/graphql'), + ); } -start(); \ No newline at end of file +start(); diff --git a/server/src/schema/resolvers/user.ts b/server/src/schema/resolvers/user.ts index 2e633d1..60656d5 100644 --- a/server/src/schema/resolvers/user.ts +++ b/server/src/schema/resolvers/user.ts @@ -1,7 +1,11 @@ import bcrypt from 'bcryptjs'; import { GraphQLError } from 'graphql'; import { prisma } from '../../db/client'; -import { clearLoginCookie, setLoginCookie, type Context } from '../../auth/session'; +import { + clearLoginCookie, + setLoginCookie, + type Context, +} from '../../auth/session'; // samme krav som i frontend const MIN_PASSWORD_LENGTH = 6; @@ -72,4 +76,4 @@ export const userResolvers = { return true; }, }, -}; \ No newline at end of file +}; diff --git a/server/src/schema/typeDefs/user.graphql b/server/src/schema/typeDefs/user.graphql index fa76a9f..32edda7 100644 --- a/server/src/schema/typeDefs/user.graphql +++ b/server/src/schema/typeDefs/user.graphql @@ -15,4 +15,4 @@ extend type Mutation { register(username: String!, password: String!): User! login(username: String!, password: String!): User! logout: Boolean! -} \ No newline at end of file +} From e0b9d69e43ed1bd3a763d077a0b8818125bb31ff Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Wed, 14 Oct 2026 20:52:08 +0200 Subject: [PATCH 5/5] gjestebruker i backend --- client/src/graphql/auth.ts | 23 ++++++++++- client/src/lib/AuthProvider.tsx | 52 +++++++++++-------------- client/src/lib/authContext.ts | 2 +- client/src/routes/LoginPage.tsx | 12 +++--- server/src/auth/session.ts | 20 +++++++--- server/src/index.ts | 11 +++++- server/src/schema/resolvers/user.ts | 23 +++++++++++ server/src/schema/typeDefs/user.graphql | 4 +- 8 files changed, 100 insertions(+), 47 deletions(-) diff --git a/client/src/graphql/auth.ts b/client/src/graphql/auth.ts index df91209..3f04d88 100644 --- a/client/src/graphql/auth.ts +++ b/client/src/graphql/auth.ts @@ -1,4 +1,13 @@ -import { gql } from '@apollo/client'; +import { gql, type TypedDocumentNode } from '@apollo/client'; + +export type BackendUser = { + id: string; + username: string; + name: string; + languagePref: string; + unitSystem: string; + isGuest: boolean; +}; const USER_FIELDS = gql` fragment UserFields on User { @@ -7,10 +16,11 @@ const USER_FIELDS = gql` name languagePref unitSystem + isGuest } `; -export const ME = gql` +export const ME: TypedDocumentNode<{ me: BackendUser | null }> = gql` query Me { me { ...UserFields @@ -42,3 +52,12 @@ export const LOGOUT = gql` logout } `; + +export const CONTINUE_AS_GUEST = gql` + mutation ContinueAsGuest { + continueAsGuest { + ...UserFields + } + } + ${USER_FIELDS} +`; diff --git a/client/src/lib/AuthProvider.tsx b/client/src/lib/AuthProvider.tsx index e544a8a..bf4d844 100644 --- a/client/src/lib/AuthProvider.tsx +++ b/client/src/lib/AuthProvider.tsx @@ -1,27 +1,15 @@ -import { useState, type ReactNode } from 'react'; +import { type ReactNode } from 'react'; import { CombinedGraphQLErrors } from '@apollo/client'; import { useMutation, useQuery } from '@apollo/client/react'; import type { User } from '../types/user'; import { AuthContext } from './authContext'; -import { LOGIN, LOGOUT, ME, REGISTER } from '../graphql/auth'; - -// midlertidige brukere til jeg får koblet meg til databasen -const MOCK_GUEST_USER: User = { - id: 'guest', - name: 'Gjest', - authProvider: 'GUEST', -}; - -// brukeren slik backend sender den -type BackendUser = { - id: string; - username: string; - name: string; - languagePref: string; - unitSystem: string; -}; - -type MeData = { me: BackendUser | null }; +import { + CONTINUE_AS_GUEST, + LOGIN, + LOGOUT, + ME, + REGISTER, +} from '../graphql/auth'; // gjør om feil fra serveren til en melding som kan vises function getErrorMessage(error: unknown) { @@ -34,22 +22,27 @@ function getErrorMessage(error: unknown) { type AuthProviderProps = { children: ReactNode }; export function AuthProvider({ children }: AuthProviderProps) { - const [guest, setGuest] = useState(null); - // spør serveren hvem som er logget inn - const { data, loading, refetch } = useQuery(ME); + const { data, loading, refetch } = useQuery(ME); const [loginMutation] = useMutation(LOGIN); const [registerMutation] = useMutation(REGISTER); const [logoutMutation] = useMutation(LOGOUT); + const [guestMutation] = useMutation(CONTINUE_AS_GUEST); - // innlogget bruker fra backend ellers gjesten - const backendUser: User | null = data?.me - ? { ...data.me, authProvider: 'PASSWORD' } + // innlogget bruker enten gjest eller vanlig bruker + const user: User | null = data?.me + ? { ...data.me, authProvider: data.me.isGuest ? 'GUEST' : 'PASSWORD' } : null; - const user = backendUser ?? guest; - const continueAsGuest = () => { - setGuest(MOCK_GUEST_USER); + // lager eller gjenbruker en gjestebruker + const continueAsGuest = async () => { + try { + await guestMutation(); + await refetch(); + return null; + } catch (error) { + return getErrorMessage(error); + } }; // logger inn hvis brukernavn og passord stemmer @@ -75,7 +68,6 @@ export function AuthProvider({ children }: AuthProviderProps) { }; const logout = async () => { - setGuest(null); await logoutMutation(); await refetch(); }; diff --git a/client/src/lib/authContext.ts b/client/src/lib/authContext.ts index c371533..016afef 100644 --- a/client/src/lib/authContext.ts +++ b/client/src/lib/authContext.ts @@ -5,7 +5,7 @@ import type { User } from '../types/user'; export type AuthContextValue = { user: User | null; loading: boolean; - continueAsGuest: () => void; + continueAsGuest: () => Promise; login: (username: string, password: string) => Promise; register: (username: string, password: string) => Promise; logout: () => Promise; diff --git a/client/src/routes/LoginPage.tsx b/client/src/routes/LoginPage.tsx index 749f529..ada21ee 100644 --- a/client/src/routes/LoginPage.tsx +++ b/client/src/routes/LoginPage.tsx @@ -18,19 +18,21 @@ export default function LoginPage() { } // logger inn som gjest og går til forside - function handleGuest() { - continueAsGuest(); - navigate('/'); + async function handleGuest() { + const errorMessage = await continueAsGuest(); + if (!errorMessage) { + navigate('/'); + } } - // for å måle bredden rundt google-knappen + // for å måle bredden rundt google knappen const googleBoxRef = useRef(null); const [googleButtonWidth, setGoogleButtonWidth] = useState(400); // måler hver gang den endrer størrelse useEffect(() => { const box = googleBoxRef.current; - // stopper hvis Google-knappen er skjult + // stopper hvis google knappen er skjult if (!box) return; const observer = new ResizeObserver(() => { diff --git a/server/src/auth/session.ts b/server/src/auth/session.ts index cc6c3cd..5fc8925 100644 --- a/server/src/auth/session.ts +++ b/server/src/auth/session.ts @@ -5,6 +5,8 @@ import type { Request, Response } from 'express'; const COOKIE_NAME = 'token'; // hvor lenge man er logget inn i millisekunder const SEVEN_DAYS_MS = 604800000; +// gjester er logget inn så lenge nettleseren tillater +const FOUR_HUNDRED_DAYS_MS = 400 * 24 * 60 * 60 * 1000; export type Context = { req: Request; @@ -12,6 +14,9 @@ export type Context = { userId: string | null; }; +// det som står i billetten +type Session = { userId: string; isGuest: boolean }; + // henter nøkkelen fra .env og stopper hvis den mangler function getSecret() { const secret = process.env.JWT_SECRET; @@ -22,12 +27,15 @@ function getSecret() { } // lager en signert billett med bruker id og legger den i en cookie -export function setLoginCookie(res: Response, userId: string) { - const token = jwt.sign({ userId }, getSecret(), { expiresIn: '7d' }); +export function setLoginCookie(res: Response, userId: string, isGuest = false) { + const maxAge = isGuest ? FOUR_HUNDRED_DAYS_MS : SEVEN_DAYS_MS; + const token = jwt.sign({ userId, isGuest }, getSecret(), { + expiresIn: maxAge / 1000, + }); res.cookie(COOKIE_NAME, token, { httpOnly: true, sameSite: 'lax', - maxAge: SEVEN_DAYS_MS, + maxAge, }); } @@ -37,13 +45,13 @@ export function clearLoginCookie(res: Response) { } // leser cookien og finner bruker id, null hvis ingen eller ugyldig -export function getUserId(req: Request): string | null { +export function getSession(req: Request): Session | null { const token = req.cookies?.[COOKIE_NAME]; if (!token) return null; try { - const payload = jwt.verify(token, getSecret()) as { userId: string }; - return payload.userId; + const payload = jwt.verify(token, getSecret()) as Session; + return { userId: payload.userId, isGuest: payload.isGuest === true }; } catch { // billetten er utløpt eller noen har tullet med den return null; diff --git a/server/src/index.ts b/server/src/index.ts index 0da6e16..180b488 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -8,7 +8,7 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import { recipeResolvers } from './schema/resolvers/recipe'; import { userResolvers } from './schema/resolvers/user'; -import { getUserId, type Context } from './auth/session'; +import { getSession, setLoginCookie, type Context } from './auth/session'; import uploadRouter from './routes/upload'; const typeDefs = ['recipe.graphql', 'user.graphql'].map((file) => @@ -48,7 +48,14 @@ async function start() { express.json(), expressMiddleware(server, { // finner hvem som er logget inn for hver forespørsel - context: async ({ req, res }) => ({ req, res, userId: getUserId(req) }), + context: async ({ req, res }) => { + const session = getSession(req); + // fornyer cookies på gjestebruker hver gang dde besøker nettsiden + if (session?.isGuest) { + setLoginCookie(res, session.userId, true); + } + return { req, res, userId: session?.userId ?? null }; + }, }), ); diff --git a/server/src/schema/resolvers/user.ts b/server/src/schema/resolvers/user.ts index 60656d5..e454e67 100644 --- a/server/src/schema/resolvers/user.ts +++ b/server/src/schema/resolvers/user.ts @@ -6,6 +6,7 @@ import { setLoginCookie, type Context, } from '../../auth/session'; +import { randomUUID } from 'crypto'; // samme krav som i frontend const MIN_PASSWORD_LENGTH = 6; @@ -75,5 +76,27 @@ export const userResolvers = { clearLoginCookie(context.res); return true; }, + + // lager en gjestebruker uten passord og logger inn + continueAsGuest: async (_: unknown, __: unknown, context: Context) => { + // gjenbruker gjesten hvis nettleseren allerede har en + if (context.userId) { + const current = await prisma.user.findUnique({ + where: { id: context.userId }, + }); + if (current?.isGuest) return current; + } + + const guest = await prisma.user.create({ + data: { + username: `gjest-${randomUUID()}`, + name: 'Gjest', + isGuest: true, + }, + }); + + setLoginCookie(context.res, guest.id, true); + return guest; + }, }, }; diff --git a/server/src/schema/typeDefs/user.graphql b/server/src/schema/typeDefs/user.graphql index 32edda7..ebeb36c 100644 --- a/server/src/schema/typeDefs/user.graphql +++ b/server/src/schema/typeDefs/user.graphql @@ -4,10 +4,11 @@ type User { name: String! languagePref: String! unitSystem: String! + isGuest: Boolean! } extend type Query { - # innlogget bruker, null hvis ingen er logget inn + # innlogget bruker og null hvis ingen er logget inn me: User } @@ -15,4 +16,5 @@ extend type Mutation { register(username: String!, password: String!): User! login(username: String!, password: String!): User! logout: Boolean! + continueAsGuest: User! }