diff --git a/package-lock.json b/package-lock.json index 8edb64d..275bbd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,9 @@ "bcrypt": "^6.0.0", "dotenv": "^16.5.0", "formdata-node": "^6.0.3", + "html2pdf.js": "^0.10.3", "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", - "html2pdf.js": "^0.10.3", "mongoose": "^8.15.0", "next": "15.3.1", "qrcode.react": "^4.2.0", @@ -1558,8 +1558,6 @@ }, "node_modules/@types/raf": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", - "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", "license": "MIT", "optional": true }, @@ -1570,8 +1568,6 @@ }, "node_modules/@types/trusted-types": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", "optional": true }, @@ -2096,8 +2092,6 @@ }, "node_modules/atob": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "license": "(MIT OR Apache-2.0)", "bin": { "atob": "bin/atob.js" @@ -2253,6 +2247,13 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", @@ -2274,13 +2275,6 @@ "license": "MIT", "engines": { "node": "^18 || ^20 || >= 21" - "node_modules/base64-arraybuffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" } }, "node_modules/brace-expansion": { @@ -2349,15 +2343,8 @@ "node": ">=16.20.1" } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" "node_modules/btoa": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", - "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", "license": "(MIT OR Apache-2.0)", "bin": { "btoa": "bin/btoa.js" @@ -2366,6 +2353,12 @@ "node": ">= 0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "dev": true, @@ -2460,8 +2453,6 @@ }, "node_modules/canvg": { "version": "3.0.11", - "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", - "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", "license": "MIT", "optional": true, "dependencies": { @@ -2709,8 +2700,6 @@ }, "node_modules/core-js": { "version": "3.42.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz", - "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==", "hasInstallScript": true, "license": "MIT", "optional": true, @@ -2771,8 +2760,6 @@ }, "node_modules/css-line-break": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", - "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", "license": "MIT", "dependencies": { "utrie": "^1.0.2" @@ -2968,8 +2955,6 @@ }, "node_modules/dompurify": { "version": "3.2.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", - "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optional": true, "optionalDependencies": { @@ -3208,8 +3193,6 @@ }, "node_modules/es6-promise": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", "license": "MIT" }, "node_modules/escalade": { @@ -3741,8 +3724,6 @@ }, "node_modules/fflate": { "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", "license": "MIT" }, "node_modules/file-entry-cache": { @@ -4152,8 +4133,6 @@ }, "node_modules/html2canvas": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", - "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", "license": "MIT", "dependencies": { "css-line-break": "^2.1.0", @@ -4165,8 +4144,6 @@ }, "node_modules/html2pdf.js": { "version": "0.10.3", - "resolved": "https://registry.npmjs.org/html2pdf.js/-/html2pdf.js-0.10.3.tgz", - "integrity": "sha512-RcB1sh8rs5NT3jgbN5zvvTmkmZrsUrxpZ/RI8TMbvuReNZAdJZG5TMfA2TBP6ZXxpXlWf9NB/ciLXVb6W2LbRQ==", "license": "MIT", "dependencies": { "es6-promise": "^4.2.5", @@ -4184,8 +4161,6 @@ }, "node_modules/husky": { "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, "license": "MIT", "bin": { @@ -5605,10 +5580,10 @@ "engines": { "node": ">=12", "npm": ">=6" + } + }, "node_modules/jspdf": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz", - "integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.7", @@ -6310,8 +6285,6 @@ }, "node_modules/mongoose": { "version": "8.15.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.15.0.tgz", - "integrity": "sha512-WFKsY1q12ScGabnZWUB9c/QzZmz/ESorrV27OembB7Gz6rrh9m3GA4Srsv1uvW1s9AHO5DeZ6DdUTyF9zyNERQ==", "license": "MIT", "dependencies": { "bson": "^6.10.3", @@ -6788,8 +6761,6 @@ }, "node_modules/performance-now": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "license": "MIT", "optional": true }, @@ -7019,8 +6990,6 @@ }, "node_modules/qrcode.react": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", - "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", "license": "ISC", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -7047,8 +7016,6 @@ }, "node_modules/raf": { "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", "license": "MIT", "optional": true, "dependencies": { @@ -7121,8 +7088,6 @@ }, "node_modules/regenerator-runtime": { "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "license": "MIT", "optional": true }, @@ -7271,8 +7236,6 @@ }, "node_modules/rgbcolor": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", - "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", "optional": true, "engines": { @@ -7702,8 +7665,6 @@ }, "node_modules/stackblur-canvas": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", - "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", "license": "MIT", "optional": true, "engines": { @@ -7966,8 +7927,6 @@ }, "node_modules/svg-pathdata": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", - "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", "license": "MIT", "optional": true, "engines": { @@ -7989,8 +7948,6 @@ }, "node_modules/text-segmentation": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", - "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", "license": "MIT", "dependencies": { "utrie": "^1.0.2" @@ -8323,8 +8280,6 @@ }, "node_modules/utrie": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", - "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", "license": "MIT", "dependencies": { "base64-arraybuffer": "^1.0.2" diff --git a/src/app/api/bookings/[id]/route.js b/src/app/api/bookings/[id]/route.js index bee6c68..dc8cb4c 100644 --- a/src/app/api/bookings/[id]/route.js +++ b/src/app/api/bookings/[id]/route.js @@ -12,7 +12,9 @@ export async function GET(req, { params }) { } export async function PATCH(req, context) { - if (requireAdminAccess()) { + const isAdmin = await requireAdminAccess() + + if (!isAdmin) { return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 }) } try { @@ -40,7 +42,9 @@ export async function PATCH(req, context) { } export async function DELETE(req, context) { - if (requireAdminAccess()) { + const isAdmin = await requireAdminAccess() + + if (!isAdmin) { return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 }) } diff --git a/src/app/api/bookings/route.js b/src/app/api/bookings/route.js index 150a0aa..6159db0 100644 --- a/src/app/api/bookings/route.js +++ b/src/app/api/bookings/route.js @@ -18,7 +18,9 @@ export async function POST(request) { } export async function GET() { - if (requireAdminAccess()) { + const isAdmin = await requireAdminAccess() + + if (!isAdmin) { return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 }) } diff --git a/src/app/api/deleteuser/route.js b/src/app/api/deleteuser/route.js index 9f93492..5531b22 100644 --- a/src/app/api/deleteuser/route.js +++ b/src/app/api/deleteuser/route.js @@ -1,6 +1,12 @@ import { deleteUserByUsername } from '@/lib/db/userDbService' +import { requireAdminAccess } from '@/lib/auth/requireAdminAccess' export async function POST(request) { + const isAdmin = await requireAdminAccess() + + if (!isAdmin) { + return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 }) + } const { username } = await request.json() if (!username) { diff --git a/src/app/api/login/route.js b/src/app/api/login/route.js index a1f8fd2..002f56f 100644 --- a/src/app/api/login/route.js +++ b/src/app/api/login/route.js @@ -2,36 +2,45 @@ import connectDB from '@/lib/db/connectDB' import { findUserByUsername } from '@/lib/db/userDbService' import { NextResponse } from 'next/server' import jsonwebtoken from 'jsonwebtoken' -import hashPassword from '@/lib/utils/hashPassword' import bcrypt from 'bcrypt' export async function POST(request) { const payload = await request.json() - - // Log the payload for debugging - console.log(`${payload.Username} and ${payload.Password} received in the login API`) - console.log(`Login attempt with username: ${payload.Username} and password: ${payload.Password}`) + const { Username, Password } = payload await connectDB() - let login = await findUserByUsername(payload.Username) - - const secretpassword = await hashPassword(payload.Password) - - if (login.Username == payload.Username) { - console.log(`User ${payload.Username} found in the database`) - const isMatch = await bcrypt.compare(payload.Password, login.Password) - console.log(`Password match for user ${payload.Username}: ${isMatch}`) - if (isMatch) { - const response = NextResponse.json({ message: 'Login successful' }, { status: 200 }) - const jwtToken = jsonwebtoken.sign({ username: payload.Username, admin: login.Admin }, process.env.JWT_SECRET, { - expiresIn: '1h', - }) - response.cookies.set('JWT', jwtToken, { httpOnly: false }) - return response - } else login.Password !== secretpassword - { - console.log(`Password incorrect for user: ${payload.Username}`) - return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 }) - } + const user = await findUserByUsername(Username) + + if (!user || user.Username !== Username) { + return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 }) } + + const isMatch = await bcrypt.compare(Password, user.Password) + if (!isMatch) { + return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 }) + } + + const jwtToken = jsonwebtoken.sign({ username: user.Username, admin: user.Admin }, process.env.JWT_SECRET, { + expiresIn: '1h', + }) + + const response = NextResponse.json({ message: 'Login successful' }, { status: 200 }) + + response.cookies.set('token', jwtToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + maxAge: 60 * 60, + }) + + response.cookies.set('JWT', jwtToken, { + httpOnly: false, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + maxAge: 60 * 60, + }) + + return response } diff --git a/src/app/api/logout/route.js b/src/app/api/logout/route.js index c28acb1..1361d91 100644 --- a/src/app/api/logout/route.js +++ b/src/app/api/logout/route.js @@ -1,10 +1,21 @@ -import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' export async function POST() { - const storeCookie = await cookies() - storeCookie.delete('JWT', { httpOnly: false }) - return new Response(JSON.stringify({ message: 'Logged out' }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }) + try { + const response = NextResponse.json({ message: 'Logged out' }, { status: 200 }) + + response.cookies.set('JWT', '', { + path: '/', + expires: new Date(0), + }) + + response.cookies.set('token', '', { + path: '/', + expires: new Date(0), + }) + + return response + } catch (error) { + return NextResponse.json({ error: 'Logout failed' }, { status: 500 }) + } } diff --git a/src/app/api/movies/[id]/route.js b/src/app/api/movies/[id]/route.js index 9c288d9..657dd31 100644 --- a/src/app/api/movies/[id]/route.js +++ b/src/app/api/movies/[id]/route.js @@ -6,7 +6,9 @@ import { requireAdminAccess } from '@/lib/auth/requireAdminAccess' export async function DELETE(req, context) { const { id } = await context.params - if (requireAdminAccess()) { + const isAdmin = await requireAdminAccess() + + if (!isAdmin) { return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 }) } diff --git a/src/app/api/movies/route.js b/src/app/api/movies/route.js index e96f428..b57f55a 100644 --- a/src/app/api/movies/route.js +++ b/src/app/api/movies/route.js @@ -3,7 +3,6 @@ import connectDB from '@/lib/db/connectDB' import { getAllMovies, createMovieFromOmdbTitle, findMovieByTitle, findMoviesByTitle } from '@/lib/db/movieDbService' import { requireAdminAccess } from '@/lib/auth/requireAdminAccess' - export async function GET(req) { await connectDB() @@ -24,7 +23,9 @@ export async function GET(req) { } } export async function POST(req) { - if (requireAdminAccess()) { + const isAdmin = await requireAdminAccess() + + if (!isAdmin) { return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 }) } diff --git a/src/app/api/rooms/[id]/route.js b/src/app/api/rooms/[id]/route.js index 3bb5551..c121aa3 100644 --- a/src/app/api/rooms/[id]/route.js +++ b/src/app/api/rooms/[id]/route.js @@ -6,7 +6,9 @@ import { requireAdminAccess } from '@/lib/auth/requireAdminAccess' export async function DELETE(req, context) { const { id } = await context.params - if (requireAdminAccess()) { + const isAdmin = await requireAdminAccess() + + if (!isAdmin) { return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 }) } diff --git a/src/app/api/rooms/route.js b/src/app/api/rooms/route.js index 3b3cf22..36e57b7 100644 --- a/src/app/api/rooms/route.js +++ b/src/app/api/rooms/route.js @@ -12,7 +12,9 @@ export async function GET() { } export async function POST(req) { - if (requireAdminAccess()) { + const isAdmin = await requireAdminAccess() + + if (!isAdmin) { return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 }) } try { diff --git a/src/app/api/screenings/[id]/route.js b/src/app/api/screenings/[id]/route.js index a4aafa3..889ec1f 100644 --- a/src/app/api/screenings/[id]/route.js +++ b/src/app/api/screenings/[id]/route.js @@ -6,7 +6,9 @@ import { requireAdminAccess } from '@/lib/auth/requireAdminAccess' export async function DELETE(req, context) { const { id } = await context.params - if (requireAdminAccess()) { + const isAdmin = await requireAdminAccess() + + if (!isAdmin) { return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 }) } diff --git a/src/app/api/screenings/route.js b/src/app/api/screenings/route.js index f1be9ba..bd81981 100644 --- a/src/app/api/screenings/route.js +++ b/src/app/api/screenings/route.js @@ -10,7 +10,9 @@ export async function GET() { } export async function POST(req) { - if (requireAdminAccess()) { + const isAdmin = await requireAdminAccess() + + if (!isAdmin) { return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 }) } diff --git a/src/app/api/toggleadmin/route.js b/src/app/api/toggleadmin/route.js index 2182c8e..8ed57ab 100644 --- a/src/app/api/toggleadmin/route.js +++ b/src/app/api/toggleadmin/route.js @@ -1,7 +1,13 @@ import { NextResponse } from 'next/server' import { toggleAdmin } from '@/lib/db/userDbService' +import { requireAdminAccess } from '@/lib/auth/requireAdminAccess' export async function POST(request) { + const isAdmin = await requireAdminAccess() + + if (!isAdmin) { + return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 }) + } const { username } = await request.json() try { const user = await toggleAdmin(username) diff --git a/src/app/login/page.jsx b/src/app/login/page.jsx index 7957c12..3e5304d 100644 --- a/src/app/login/page.jsx +++ b/src/app/login/page.jsx @@ -19,6 +19,7 @@ export default function Login() { ev.preventDefault() const response = await fetch('/api/login', { method: 'POST', + credentials: 'include', headers: { 'Content-Type': 'application/json', }, diff --git a/src/components/LogoutButton.jsx b/src/components/LogoutButton.jsx index 78f2520..32b38a5 100644 --- a/src/components/LogoutButton.jsx +++ b/src/components/LogoutButton.jsx @@ -5,7 +5,7 @@ export default function LogoutButton() { const router = useRouter() async function handleLogout() { - await fetch('/api/logout', { method: 'POST' }) + await fetch('/api/logout', { method: 'POST', credentials: 'include' }) window.dispatchEvent(new Event('loginStatusChanged')) router.push('/login') } diff --git a/src/hooks/useAdminBookings.js b/src/hooks/useAdminBookings.js index d9d7553..0d06afa 100644 --- a/src/hooks/useAdminBookings.js +++ b/src/hooks/useAdminBookings.js @@ -75,7 +75,9 @@ export function useAdminBookings() { message: `Vill du ta bort bokningen för ${booking.name}?`, onConfirm: async () => { await deleteBookingById(booking._id) - setBookings((prev) => prev.filter((booking) => booking._id !== booking._id)) + + setBookings((prev) => prev.filter((b) => b._id !== booking._id)) + setFiltered((prev) => prev.filter((b) => b._id !== booking._id)) setSuccessMessage('Bokningen har tagits bort!') setModal(null) }, diff --git a/src/lib/auth/requireAdminAccess.js b/src/lib/auth/requireAdminAccess.js index d4f3138..b12d9f1 100644 --- a/src/lib/auth/requireAdminAccess.js +++ b/src/lib/auth/requireAdminAccess.js @@ -1,4 +1,16 @@ -// Will be updated to check if user is admin (need login/sign in for this) -export function requireAdminAccess() { - return process.env.NODE_ENV === 'production' +import { cookies } from 'next/headers' +import jwt from 'jsonwebtoken' + +export async function requireAdminAccess() { + const JWT_SECRET = process.env.JWT_SECRET + const token = cookies().get('token')?.value + + if (!token || !JWT_SECRET) return false + + try { + const decoded = jwt.verify(token, JWT_SECRET) + return decoded.admin === true + } catch { + return false + } } diff --git a/src/lib/services/bookingApiService.js b/src/lib/services/bookingApiService.js index dd68c90..72c4bca 100644 --- a/src/lib/services/bookingApiService.js +++ b/src/lib/services/bookingApiService.js @@ -1,17 +1,18 @@ export async function fetchBookings() { - const res = await fetch('/api/bookings') + const res = await fetch('/api/bookings', { method: 'GET', credentials: 'include' }) if (!res.ok) throw new Error('Kunde inte hämta bokningar.') return await res.json() } export async function deleteBookingById(id) { - const res = await fetch(`/api/bookings/${id}`, { method: 'DELETE' }) + const res = await fetch(`/api/bookings/${id}`, { method: 'DELETE', credentials: 'include' }) return { success: res.ok } } export async function updateBookingSeats(id, seats) { const res = await fetch(`/api/bookings/${id}`, { method: 'PATCH', + credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ seats }), }) diff --git a/src/lib/services/movieApiService.js b/src/lib/services/movieApiService.js index 601165b..fd47b4a 100644 --- a/src/lib/services/movieApiService.js +++ b/src/lib/services/movieApiService.js @@ -7,6 +7,7 @@ export async function fetchMovies() { export async function addMovie(formData) { const res = await fetch('/api/movies', { method: 'POST', + credentials: 'include', body: formData, }) @@ -25,6 +26,6 @@ export async function addMovie(formData) { } export async function deleteMovie(id) { - const res = await fetch(`/api/movies/${id}`, { method: 'DELETE' }) + const res = await fetch(`/api/movies/${id}`, { method: 'DELETE', credentials: 'include' }) if (!res.ok) throw new Error('Kunde inte ta bort filmen.') } diff --git a/src/lib/services/roomApiService.js b/src/lib/services/roomApiService.js index ae3491a..d30bb94 100644 --- a/src/lib/services/roomApiService.js +++ b/src/lib/services/roomApiService.js @@ -1,6 +1,7 @@ export async function addRoom(roomData) { const res = await fetch('/api/rooms', { method: 'POST', + credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(roomData), }) @@ -22,6 +23,7 @@ export async function fetchRooms() { export async function deleteRoom(id) { const res = await fetch(`/api/rooms/${id}`, { method: 'DELETE', + credentials: 'include', }) if (!res.ok) { diff --git a/src/lib/services/screeningApiService.js b/src/lib/services/screeningApiService.js index 30f3a1d..97b7d3b 100644 --- a/src/lib/services/screeningApiService.js +++ b/src/lib/services/screeningApiService.js @@ -7,6 +7,7 @@ export async function fetchScreenings() { export async function addScreening(formData) { const res = await fetch('/api/screenings', { method: 'POST', + credentials: 'include', body: formData, }) @@ -25,7 +26,7 @@ export async function addScreening(formData) { } export async function deleteScreening(id) { - const res = await fetch(`/api/screenings/${id}`, { method: 'DELETE' }) + const res = await fetch(`/api/screenings/${id}`, { method: 'DELETE', credentials: 'include' }) if (!res.ok) throw new Error('Kunde inte ta bort visningen.') } diff --git a/src/tests/api/bookings/bookings.test.js b/src/tests/api/bookings/bookings.test.js index e915281..cdbe419 100644 --- a/src/tests/api/bookings/bookings.test.js +++ b/src/tests/api/bookings/bookings.test.js @@ -1,4 +1,10 @@ import { expect, jest, test, describe, beforeAll, beforeEach } from '@jest/globals' +import jwt from 'jsonwebtoken' + +jest.unstable_mockModule('next/headers', () => ({ + __esModule: true, + cookies: jest.fn(), +})) jest.unstable_mockModule('@/lib/db/connectDB', () => ({ __esModule: true, @@ -16,13 +22,17 @@ jest.unstable_mockModule('@/lib/db/bookingDbService', () => ({ jest.unstable_mockModule('@/lib/auth/requireAdminAccess', () => ({ __esModule: true, - requireAdminAccess: jest.fn(() => false), + requireAdminAccess: jest.fn(() => true), })) let GET, PATCH, DELETE let bookingService +let cookies beforeAll(async () => { + const headers = await import('next/headers') + cookies = headers.cookies + bookingService = await import('@/lib/db/bookingDbService') const routeModule = await import('@/app/api/bookings/route') const detailModule = await import('@/app/api/bookings/[id]/route') @@ -33,7 +43,8 @@ beforeAll(async () => { beforeEach(() => { jest.clearAllMocks() - process.env.NODE_ENV = 'development' + const token = jwt.sign({ username: 'admin', admin: true }, process.env.JWT_SECRET || 'test') + cookies.mockReturnValue({ get: () => ({ value: token }) }) }) describe('GET /api/bookings (mocked)', () => { diff --git a/src/tests/api/movies/movies.test.js b/src/tests/api/movies/movies.test.js index e306b56..e7b6693 100644 --- a/src/tests/api/movies/movies.test.js +++ b/src/tests/api/movies/movies.test.js @@ -1,7 +1,12 @@ import { FormData } from 'formdata-node' import { expect, jest, test, describe, beforeEach, beforeAll } from '@jest/globals' +import jwt from 'jsonwebtoken' + +jest.unstable_mockModule('next/headers', () => ({ + __esModule: true, + cookies: jest.fn(), +})) -// Mock database and services jest.unstable_mockModule('@/lib/db/connectDB', () => ({ __esModule: true, default: jest.fn(), @@ -17,10 +22,19 @@ jest.unstable_mockModule('@/lib/db/movieDbService', () => ({ findMoviesByTitle: jest.fn(), })) +jest.unstable_mockModule('@/lib/auth/requireAdminAccess', () => ({ + __esModule: true, + requireAdminAccess: jest.fn(() => true), +})) + let POST, GET, DELETE let movieService +let cookies beforeAll(async () => { + const headers = await import('next/headers') + cookies = headers.cookies + movieService = await import('@/lib/db/movieDbService') const routeModule = await import('@/app/api/movies/route') const deleteModule = await import('@/app/api/movies/[id]/route') @@ -31,7 +45,8 @@ beforeAll(async () => { beforeEach(() => { jest.clearAllMocks() - process.env.NODE_ENV = 'development' + const token = jwt.sign({ username: 'admin', admin: true }, process.env.JWT_SECRET || 'test') + cookies.mockReturnValue({ get: () => ({ value: token }) }) }) describe('POST /api/movies (mocked)', () => { @@ -75,11 +90,18 @@ describe('POST /api/movies (mocked)', () => { expect(data.error).toMatch(/titel behövs/i) }) - test('blocks in production mode', async () => { - process.env.NODE_ENV = 'production' + test('blocks non-admin users', async () => { + jest.resetModules() + + jest.unstable_mockModule('@/lib/auth/requireAdminAccess', () => ({ + __esModule: true, + requireAdminAccess: jest.fn(() => false), + })) + + const { POST: MockedPOST } = await import('@/app/api/movies/route') const req = { formData: async () => new FormData() } - const res = await POST(req) + const res = await MockedPOST(req) const data = await res.json() expect(res.status).toBe(403) @@ -115,10 +137,17 @@ describe('DELETE /api/movies/[id] (mocked)', () => { expect(data.deletedMovie).toEqual(deletedMovie) }) - test('blocks deletion in production mode', async () => { - process.env.NODE_ENV = 'production' + test('blocks non-admin users', async () => { + jest.resetModules() - const res = await DELETE({}, { params: { id: '1' } }) + jest.unstable_mockModule('@/lib/auth/requireAdminAccess', () => ({ + __esModule: true, + requireAdminAccess: jest.fn(() => false), + })) + + const { DELETE: MockedDELETE } = await import('@/app/api/movies/[id]/route') + + const res = await MockedDELETE({}, { params: { id: '1' } }) const data = await res.json() expect(res.status).toBe(403) diff --git a/src/tests/api/rooms/rooms.test.js b/src/tests/api/rooms/rooms.test.js index 84451d7..a0bdabb 100644 --- a/src/tests/api/rooms/rooms.test.js +++ b/src/tests/api/rooms/rooms.test.js @@ -1,4 +1,10 @@ import { expect, jest, test, describe, beforeAll, beforeEach } from '@jest/globals' +import jwt from 'jsonwebtoken' + +jest.unstable_mockModule('next/headers', () => ({ + __esModule: true, + cookies: jest.fn(), +})) jest.unstable_mockModule('@/lib/db/connectDB', () => ({ __esModule: true, @@ -13,10 +19,19 @@ jest.unstable_mockModule('@/lib/db/roomDbService', () => ({ getRoomById: jest.fn(), })) +jest.unstable_mockModule('@/lib/auth/requireAdminAccess', () => ({ + __esModule: true, + requireAdminAccess: jest.fn(() => true), +})) + let GET, POST, DELETE let roomService +let cookies beforeAll(async () => { + const headers = await import('next/headers') + cookies = headers.cookies + roomService = await import('@/lib/db/roomDbService') const routeModule = await import('@/app/api/rooms/route') const deleteModule = await import('@/app/api/rooms/[id]/route') @@ -27,7 +42,8 @@ beforeAll(async () => { beforeEach(() => { jest.clearAllMocks() - process.env.NODE_ENV = 'development' + const token = jwt.sign({ username: 'admin', admin: true }, process.env.JWT_SECRET || 'test') + cookies.mockReturnValue({ get: () => ({ value: token }) }) }) describe('GET /api/rooms', () => { @@ -60,10 +76,7 @@ describe('POST /api/rooms', () => { const mockRoom = { ...input, _id: '1' } roomService.createRoom.mockResolvedValue(mockRoom) - const req = { - json: async () => input, - } - + const req = { json: async () => input } const res = await POST(req) const data = await res.json() @@ -80,10 +93,7 @@ describe('POST /api/rooms', () => { roomService.createRoom.mockRejectedValue(new Error('En salong med detta namn finns redan, testa ett nytt namn!')) - const req = { - json: async () => input, - } - + const req = { json: async () => input } const res = await POST(req) const data = await res.json() @@ -92,9 +102,7 @@ describe('POST /api/rooms', () => { }) test('returns 400 if input invalid', async () => { - const req = { - json: async () => ({ name: 'Missing rows' }), - } + const req = { json: async () => ({ name: 'Missing rows' }) } const res = await POST(req) const data = await res.json() @@ -103,8 +111,15 @@ describe('POST /api/rooms', () => { expect(data.error).toMatch(/invalid/i) }) - test('blocks in production mode', async () => { - process.env.NODE_ENV = 'production' + test('blocks non-admin users', async () => { + jest.resetModules() + + jest.unstable_mockModule('@/lib/auth/requireAdminAccess', () => ({ + __esModule: true, + requireAdminAccess: jest.fn(() => false), + })) + + const { POST: MockedPOST } = await import('@/app/api/rooms/route') const req = { json: async () => ({ @@ -114,7 +129,7 @@ describe('POST /api/rooms', () => { }), } - const res = await POST(req) + const res = await MockedPOST(req) const data = await res.json() expect(res.status).toBe(403) @@ -143,10 +158,17 @@ describe('DELETE /api/rooms/[id]', () => { expect(data.error).toMatch(/kunde inte hittas/i) }) - test('blocks in production mode', async () => { - process.env.NODE_ENV = 'production' + test('blocks non-admin users', async () => { + jest.resetModules() - const res = await DELETE({}, { params: { id: '1' } }) + jest.unstable_mockModule('@/lib/auth/requireAdminAccess', () => ({ + __esModule: true, + requireAdminAccess: jest.fn(() => false), + })) + + const { DELETE: MockedDELETE } = await import('@/app/api/rooms/[id]/route') + + const res = await MockedDELETE({}, { params: { id: '1' } }) const data = await res.json() expect(res.status).toBe(403) diff --git a/src/tests/api/screenings/screenings.test.js b/src/tests/api/screenings/screenings.test.js index e453d5c..99b0763 100644 --- a/src/tests/api/screenings/screenings.test.js +++ b/src/tests/api/screenings/screenings.test.js @@ -1,7 +1,12 @@ import { FormData } from 'formdata-node' import { expect, jest, test, describe, beforeEach, beforeAll } from '@jest/globals' +import jwt from 'jsonwebtoken' + +jest.unstable_mockModule('next/headers', () => ({ + __esModule: true, + cookies: jest.fn(), +})) -// Mock database and services jest.unstable_mockModule('@/lib/db/connectDB', () => ({ __esModule: true, default: jest.fn(), @@ -16,12 +21,20 @@ jest.unstable_mockModule('@/lib/db/screeningDbService', () => ({ updateBookedSeats: jest.fn(), })) +jest.unstable_mockModule('@/lib/auth/requireAdminAccess', () => ({ + __esModule: true, + requireAdminAccess: jest.fn(() => true), +})) + let POST, GET, DELETE let screeningService +let cookies beforeAll(async () => { - screeningService = await import('@/lib/db/screeningDbService') + const headers = await import('next/headers') + cookies = headers.cookies + screeningService = await import('@/lib/db/screeningDbService') const routeModule = await import('@/app/api/screenings/route') const deleteModule = await import('@/app/api/screenings/[id]/route') POST = routeModule.POST @@ -31,7 +44,8 @@ beforeAll(async () => { beforeEach(() => { jest.clearAllMocks() - process.env.NODE_ENV = 'development' + const token = jwt.sign({ username: 'admin', admin: true }, process.env.JWT_SECRET || 'test') + cookies.mockReturnValue({ get: () => ({ value: token }) }) }) describe('POST /api/screenings (mocked)', () => { @@ -52,10 +66,18 @@ describe('POST /api/screenings (mocked)', () => { expect(data).toEqual(mockScreening) }) - test('blocks screening creation in production', async () => { - process.env.NODE_ENV = 'production' + test('blocks non-admin user', async () => { + jest.resetModules() + + jest.unstable_mockModule('@/lib/auth/requireAdminAccess', () => ({ + __esModule: true, + requireAdminAccess: jest.fn(() => false), + })) + + const { POST: MockedPOST } = await import('@/app/api/screenings/route') + const req = { formData: async () => new FormData() } - const res = await POST(req) + const res = await MockedPOST(req) const data = await res.json() expect(res.status).toBe(403) @@ -103,9 +125,17 @@ describe('DELETE /api/screenings/[id] (mocked)', () => { expect(data.deletedScreening).toEqual(mockDeleted) }) - test('blocks deletion in production mode', async () => { - process.env.NODE_ENV = 'production' - const res = await DELETE({}, { params: { id: '1' } }) + test('blocks non-admin user', async () => { + jest.resetModules() + + jest.unstable_mockModule('@/lib/auth/requireAdminAccess', () => ({ + __esModule: true, + requireAdminAccess: jest.fn(() => false), + })) + + const { DELETE: MockedDELETE } = await import('@/app/api/screenings/[id]/route') + + const res = await MockedDELETE({}, { params: { id: '1' } }) const data = await res.json() expect(res.status).toBe(403)