diff --git a/package-lock.json b/package-lock.json index 8ee9388..8edb64d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,11 @@ "name": "kino-project", "version": "0.1.0", "dependencies": { + "bcrypt": "^6.0.0", "dotenv": "^16.5.0", "formdata-node": "^6.0.3", + "jsonwebtoken": "^9.0.2", + "jwt-decode": "^4.0.0", "html2pdf.js": "^0.10.3", "mongoose": "^8.15.0", "next": "15.3.1", @@ -2250,6 +2253,27 @@ "dev": true, "license": "MIT" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/bcrypt/node_modules/node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "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", @@ -2325,6 +2349,11 @@ "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", @@ -2970,6 +2999,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.155", "dev": true, @@ -5547,6 +5585,26 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" "node_modules/jspdf": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz", @@ -5579,6 +5637,36 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/kareem": { "version": "2.6.3", "license": "Apache-2.0", @@ -5845,11 +5933,53 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-update": { "version": "6.1.0", "dev": true, @@ -6322,6 +6452,17 @@ "license": "MIT", "optional": true }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "dev": true, @@ -7180,7 +7321,6 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", - "dev": true, "funding": [ { "type": "github", @@ -7253,7 +7393,6 @@ }, "node_modules/semver": { "version": "7.7.2", - "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" diff --git a/package.json b/package.json index 51f5dd9..141031c 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,11 @@ "watch-css": "sass --watch src/styles/main.scss:public/dist/styles.css" }, "dependencies": { + "bcrypt": "^6.0.0", "dotenv": "^16.5.0", "formdata-node": "^6.0.3", + "jsonwebtoken": "^9.0.2", + "jwt-decode": "^4.0.0", "html2pdf.js": "^0.10.3", "mongoose": "^8.15.0", "next": "15.3.1", diff --git a/src/app/admin/page.jsx b/src/app/admin/page.jsx index 42549f6..c0d7f6f 100644 --- a/src/app/admin/page.jsx +++ b/src/app/admin/page.jsx @@ -1,6 +1,5 @@ 'use client' - -import { useState } from 'react' +import { useState, useEffect } from 'react' import AdminRoomForm from '@/components/admin/AdminRoomForm' import AdminScreeningForm from '@/components/admin/AdminScreeningForm' import AdminMovieForm from '@/components/admin/AdminMovieForm' @@ -12,10 +11,31 @@ import { useAdminData } from '@/hooks/useAdminData' import AdminTabNav from '@/components/admin/AdminTabNav' import AdminRoomList from '@/components/admin/AdminRoomList' import AdminBookingPanel from '@/components/admin/AdminBookingPanel' +import AdminCreateUser from '@/components/admin/AdminCreateUser' +import { jwtDecode } from 'jwt-decode' export default function AdminPanel() { + const [isAdmin, setIsAdmin] = useState(false) + const [checked, setChecked] = useState(false) const [activeTab, setActiveTab] = useState('list') + useEffect(() => { + const cookies = document.cookie.split(';').map((c) => c.trim()) + const jwtCookie = cookies.find((c) => c.startsWith('JWT=')) + if (jwtCookie) { + const token = jwtCookie.split('=')[1] + try { + const decoded = jwtDecode(token) + setIsAdmin(decoded.admin) + } catch (e) { + setIsAdmin(false) + } + } else { + setIsAdmin(false) + } + setChecked(true) + }, []) + const { movies, screenings, @@ -42,6 +62,14 @@ export default function AdminPanel() { loading, } = useAdminData() + if (!checked) { + return
Laddar...
+ } + + if (!isAdmin) { + return

Du är inte admin!

+ } + return (
@@ -128,6 +156,13 @@ export default function AdminPanel() { )} + {activeTab === 'user1' && ( + <> +
+ +
+ + )}
{successMessage && setSuccessMessage(null)} />} diff --git a/src/app/api/changepassword/route.js b/src/app/api/changepassword/route.js new file mode 100644 index 0000000..7d3f96c --- /dev/null +++ b/src/app/api/changepassword/route.js @@ -0,0 +1,18 @@ +import { updateUserPassword } from '@/lib/db/userDbService' +import { NextResponse } from 'next/server' + +export async function POST(request) { + const payload = await request.json() + + console.log(`Received request to update password for user: ${payload.Username}`) + + try { + const updatedUser = await updateUserPassword(payload.Username, payload.Password) + + console.log(`Password updated successfully for user: ${updatedUser.Username}`) + return NextResponse.json({ message: 'Password updated successfully' }, { status: 200 }) + } catch (error) { + console.error(`Error updating password for user ${username}:`, error) + return NextResponse.json({ error: 'Failed to update password' }, { status: 500 }) + } +} diff --git a/src/app/api/deleteuser/route.js b/src/app/api/deleteuser/route.js new file mode 100644 index 0000000..9f93492 --- /dev/null +++ b/src/app/api/deleteuser/route.js @@ -0,0 +1,17 @@ +import { deleteUserByUsername } from '@/lib/db/userDbService' + +export async function POST(request) { + const { username } = await request.json() + + if (!username) { + return new Response('Username is required', { status: 400 }) + } + + try { + await deleteUserByUsername(username) + return new Response(`User ${username} deleted successfully`, { status: 200 }) + } catch (error) { + console.error('Error deleting user:', error) + return new Response('Failed to delete user', { status: 500 }) + } +} diff --git a/src/app/api/getusers/route.js b/src/app/api/getusers/route.js new file mode 100644 index 0000000..5bc925f --- /dev/null +++ b/src/app/api/getusers/route.js @@ -0,0 +1,30 @@ +import { NextResponse } from 'next/server' +import connectDB from '@/lib/db/connectDB' +import { getAllUsers } from '@/lib/db/userDbService' +import { jwtDecode } from 'jwt-decode' +export async function GET(request) { + const cookieHeader = request.headers.get('cookie') || '' + const cookies = cookieHeader.split(';').map((c) => c.trim()) + const jwtCookie = cookies.find((c) => c.startsWith('JWT=')) + if (!jwtCookie) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const token = jwtCookie.split('=')[1] + try { + const decoded = jwtDecode(token) + if (!decoded.admin) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + } catch (error) { + return NextResponse.json({ error: 'Invalid token' }, { status: 401 }) + } + try { + await connectDB() + const users = await getAllUsers() + return NextResponse.json(users, { status: 200 }) + } catch (error) { + console.error('Error fetching users:', error) + return NextResponse.json({ error: 'Failed to fetch users' }, { status: 500 }) + } +} diff --git a/src/app/api/login/route.js b/src/app/api/login/route.js new file mode 100644 index 0000000..a1f8fd2 --- /dev/null +++ b/src/app/api/login/route.js @@ -0,0 +1,37 @@ +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}`) + + 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 }) + } + } +} diff --git a/src/app/api/logout/route.js b/src/app/api/logout/route.js new file mode 100644 index 0000000..c28acb1 --- /dev/null +++ b/src/app/api/logout/route.js @@ -0,0 +1,10 @@ +import { cookies } from 'next/headers' + +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' }, + }) +} diff --git a/src/app/api/register/route.js b/src/app/api/register/route.js new file mode 100644 index 0000000..0b69006 --- /dev/null +++ b/src/app/api/register/route.js @@ -0,0 +1,23 @@ +import { NextResponse } from 'next/server' +import { createUser } from '@/lib/db/userDbService' +import bcrypt from 'bcrypt' + +export async function POST(request) { + const payload = await request.json() + + // Hash the password before storing it + console.log(`Registering user with username: ${payload.Username}`) + const salt = await bcrypt.genSalt(15) + const hash = await bcrypt.hash(payload.Password, salt) + const hashedPassword = hash + + // Create a new user in the database + try { + const newUser = await createUser(payload.Username, hashedPassword) + console.log(`User created successfully: ${newUser.Username}`) + return NextResponse.json({ message: 'User registered successfully' }, { status: 201 }) + } catch (error) { + console.error(`Error creating user: ${error.message}`) + return NextResponse.json({ error: 'Failed to register user' }, { status: 500 }) + } +} diff --git a/src/app/api/toggleadmin/route.js b/src/app/api/toggleadmin/route.js new file mode 100644 index 0000000..2182c8e --- /dev/null +++ b/src/app/api/toggleadmin/route.js @@ -0,0 +1,12 @@ +import { NextResponse } from 'next/server' +import { toggleAdmin } from '@/lib/db/userDbService' + +export async function POST(request) { + const { username } = await request.json() + try { + const user = await toggleAdmin(username) + return NextResponse.json({ success: true, user }) + } catch (err) { + return NextResponse.json({ success: false, error: err.message }, { status: 500 }) + } +} diff --git a/src/app/forgotpassword/page.jsx b/src/app/forgotpassword/page.jsx new file mode 100644 index 0000000..696a7d9 --- /dev/null +++ b/src/app/forgotpassword/page.jsx @@ -0,0 +1,57 @@ +'use client' +import { useState } from 'react' +import { useRouter } from 'next/navigation' + +export default function Register() { + const [Username, setUsername] = useState('') + const [Password, setPassword] = useState('') + const [Error, setError] = useState(false) + const router = useRouter() + + return ( +
{ + ev.preventDefault() + const response = await fetch('/api/changepassword', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + Username, + Password, + }), + }) + if (response.ok) { + router.push('/login') + } else { + setError(true) + } + }} + > +

Glömt lösenord

+

Användarnamn

+ setUsername(ev.target.value)} + /> +

Nytt lösenord

+ setPassword(ev.target.value)} + /> + +
+ ) +} diff --git a/src/app/login/account/page.jsx b/src/app/login/account/page.jsx new file mode 100644 index 0000000..990c2e1 --- /dev/null +++ b/src/app/login/account/page.jsx @@ -0,0 +1,40 @@ +import { cookies } from 'next/headers' +import LogoutButton from '@/components/LogoutButton' +import { jwtDecode } from 'jwt-decode' + +export default async function Secret() { + const allCookies = await cookies() + const jwtCookie = allCookies.get('JWT') + let username = null + if (jwtCookie) { + try { + const decoded = jwtDecode(jwtCookie.value) + username = decoded.username + } catch (e) { + console.error('Invalid JWT token:', e) + } + } + if (username) { + return ( +
+
+
+

Användare {username} har loggat in!

+
+
+

Hej {username}!

+
+ +
+
+ ) + } else { + console.log('User is not logged in') + return ( +
+

Du är inte inloggad!

+ Logga in +
+ ) + } +} diff --git a/src/app/login/page.jsx b/src/app/login/page.jsx index d5c46c8..7957c12 100644 --- a/src/app/login/page.jsx +++ b/src/app/login/page.jsx @@ -1,14 +1,68 @@ +'use client' +import { useState } from 'react' +import { useRouter } from 'next/navigation' + export default function Login() { + const [Username, setUsername] = useState('') + const [Password, setPassword] = useState('') + const [Error, setError] = useState('') + const router = useRouter() + return (
-

Login

+

Logga in

-
-

Username

- -

Password

- + { + ev.preventDefault() + const response = await fetch('/api/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + Username, + Password, + }), + }) + if (response.ok) { + window.dispatchEvent(new Event('loginStatusChanged')) + router.push('/login/account') + } else { + setError(true) + } + }} + > + {Error &&

Fel användarnamn eller lösenord

} +
+

Inget konto?

+ +
+

Användarnamn

+ setUsername(ev.target.value)} + /> +

Lösenord

+ setPassword(ev.target.value)} + /> + Glömt lösenordet?
diff --git a/src/app/register/page.jsx b/src/app/register/page.jsx new file mode 100644 index 0000000..04ba256 --- /dev/null +++ b/src/app/register/page.jsx @@ -0,0 +1,58 @@ +'use client' +import { useState } from 'react' +import { useRouter } from 'next/navigation' + +export default function Register() { + const [Username, setUsername] = useState('') + const [Password, setPassword] = useState('') + const [Error, setError] = useState(false) + const router = useRouter() + + return ( +
{ + ev.preventDefault() + const response = await fetch('/api/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + Username, + Password, + }), + }) + if (response.ok) { + router.push('/login') + } else { + setError(true) + } + }} + > + {Error &&

Användaren finns redan

} +

Registrera konto

+

Användarnamn

+ setUsername(ev.target.value)} + /> +

Lösenord

+ setPassword(ev.target.value)} + /> + +
+ ) +} diff --git a/src/components/LogoutButton.jsx b/src/components/LogoutButton.jsx new file mode 100644 index 0000000..78f2520 --- /dev/null +++ b/src/components/LogoutButton.jsx @@ -0,0 +1,18 @@ +'use client' +import { useRouter } from 'next/navigation' + +export default function LogoutButton() { + const router = useRouter() + + async function handleLogout() { + await fetch('/api/logout', { method: 'POST' }) + window.dispatchEvent(new Event('loginStatusChanged')) + router.push('/login') + } + + return ( + + ) +} diff --git a/src/components/NavMenu.jsx b/src/components/NavMenu.jsx index 01ee7e6..8ec424d 100644 --- a/src/components/NavMenu.jsx +++ b/src/components/NavMenu.jsx @@ -1,6 +1,7 @@ 'use client' -import { useState } from 'react' -import { useRouter } from 'next/navigation' +import { useEffect, useState } from 'react' +import { useRouter, usePathname } from 'next/navigation' +import { jwtDecode } from 'jwt-decode' export default function NavMenu() { const [homeClass, setHomeClass] = useState('header__nav-item') @@ -8,7 +9,35 @@ export default function NavMenu() { const [aboutClass, setAboutClass] = useState('header__nav-item') const [adminClass, setAdminClass] = useState('header__nav-item') const [loginClass, setLoginClass] = useState('header__nav-item') + const [isLoggedIn, setIsLoggedIn] = useState(false) + const [Admin, setAdmin] = useState(false) const router = useRouter() + const pathname = usePathname() + + // Checks if user is logged in by checking cookies + useEffect(() => { + const checkLogin = () => { + const cookies = document.cookie.split(';').map((c) => c.trim()) + const jwtCookie = cookies.find((c) => c.startsWith('JWT=')) + if (jwtCookie) { + const token = jwtCookie.split('=')[1] + try { + const decoded = jwtDecode(token) + setIsLoggedIn(!!decoded.username) + setAdmin(decoded.admin) + } catch (e) { + setIsLoggedIn(false) + setAdmin(false) + } + } else { + setIsLoggedIn(false) + setAdmin(false) + } + } + checkLogin() + window.addEventListener('loginStatusChanged', checkLogin) + return () => window.removeEventListener('loginStatusChanged', checkLogin) + }, []) const handleHomeClick = () => { setHomeClass('header__nav-item menu-active') @@ -78,7 +107,7 @@ export default function NavMenu() { > OM OSS - {process.env.NODE_ENV === 'development' && ( + {(process.env.NODE_ENV === 'development' || Admin === true) && (
  • { @@ -89,14 +118,31 @@ export default function NavMenu() {
  • )}
  • { - handleLoginClick() + if (isLoggedIn) { + setHomeClass('header__nav-item') + setMoviesClass('header__nav-item') + setAboutClass('header__nav-item') + setAdminClass('header__nav-item') + setLoginClass('header__nav-item menu-active') + router.push('/login/account') + } else { + handleLoginClick() + } }} > - LOGIN + {isLoggedIn ? 'KONTO' : 'LOGGA IN'}
  • + {process.env.NODE_ENV === 'development' && + console.log(`isLoggedIn:${isLoggedIn} NODE_ENV:${process.env.NODE_ENV} Admin:${Admin}`)} ) } diff --git a/src/components/admin/AdminCreateUser.jsx b/src/components/admin/AdminCreateUser.jsx new file mode 100644 index 0000000..ab75ed4 --- /dev/null +++ b/src/components/admin/AdminCreateUser.jsx @@ -0,0 +1,128 @@ +'use client' +import { useState } from 'react' + +export default function AdminCreate() { + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + const [success, setSuccess] = useState(null) + const [users, setUsers] = useState([]) // <-- Lägg till denna + const [userError, setUserError] = useState(null) + + async function handleSubmit(e) { + e.preventDefault() + setError(null) + setSuccess(null) + try { + const response = await fetch('/api/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ Username: username, Password: password }), + }) + if (!response.ok) throw new Error('Failed to create user') + setSuccess('Användare skapad!') + setUsername('') + setPassword('') + } catch (err) { + setError('Kunde inte skapa användare') + } + } + + async function fetchUsers() { + setUserError(null) + try { + const response = await fetch('/api/getusers') + if (!response.ok) throw new Error('Kunde inte hämta användare') + const data = await response.json() + setUsers(data) + } catch (err) { + setUserError('Kunde inte hämta användare') + } + } + + async function handleToggleAdmin(username) { + try { + const res = await fetch('/api/toggleadmin', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username }), + }) + if (!res.ok) throw new Error('Kunde inte toggla admin') + // Uppdatera användarlistan efter toggling + fetchUsers() + } catch (err) { + setUserError('Kunde inte toggla admin') + } + } + + async function handleDeleteUser(username) { + try { + const res = await fetch('/api/deleteuser', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username }), + }) + if (!res.ok) throw new Error('Kunde inte ta bort användare') + fetchUsers() + } catch (err) { + setUserError('Kunde inte ta bort användare') + } + } + + return ( +
    +
    +

    Skapa konto

    +
    + setUsername(e.target.value)} + /> + setPassword(e.target.value)} + /> + +
    + {error &&
    {error}
    } + {success &&
    {success}
    } +
    +
    +
    +

    Användare

    +

    Här kan du hantera befintliga användare.

    + +
    + {userError &&
    {userError}
    } +
    + + {Array.isArray(users) && + users.map((user) => ( +

    + {user.Username} {user.Admin === true || user.Admin === 'true' ? '(Admin)' : ''} + + +

    + ))} +
    +
    + ) +} diff --git a/src/components/admin/AdminTabNav.jsx b/src/components/admin/AdminTabNav.jsx index 3650d05..5a2c095 100644 --- a/src/components/admin/AdminTabNav.jsx +++ b/src/components/admin/AdminTabNav.jsx @@ -18,6 +18,9 @@ export default function AdminTabNav({ activeTab, setActiveTab }) { + ) } diff --git a/src/lib/db/models/User.js b/src/lib/db/models/User.js new file mode 100644 index 0000000..78bf249 --- /dev/null +++ b/src/lib/db/models/User.js @@ -0,0 +1,9 @@ +import mongoose from 'mongoose' + +const userSchema = new mongoose.Schema({ + Username: { type: String, required: true }, + Password: { type: String, required: true }, + Admin: { type: Boolean, default: false }, +}) + +export default mongoose.models.User || mongoose.model('User', userSchema) diff --git a/src/lib/db/userDbService.js b/src/lib/db/userDbService.js new file mode 100644 index 0000000..e89c64a --- /dev/null +++ b/src/lib/db/userDbService.js @@ -0,0 +1,72 @@ +import User from './models/User' +import connectDB from './connectDB' +import hashPassword from '@/lib/utils/hashPassword' + +// Work in progress: testing. +export async function getAllUsers() { + await connectDB() + console.log('Fetching all users from the database...') + return await User.find().select('Username Admin') +} + +// Work in progress: testing. +export async function findUserByUsername(username) { + await connectDB() + + const findUser = await User.findOne({ Username: username }).select('Username Password Admin') + console.log(`User found: ${findUser ? findUser.Username : 'No user found'}`) + if (!findUser) { + console.log(`User with username ${username} not found.`) + throw new Error('Användaren kunde inte hittas.') + } + return findUser +} + +// Creates a new user. +// Password is in plain text, work in progress to hash it. +export async function createUser(username, password) { + hashPassword(password) + await connectDB() + + const existingUser = await User.findOne({ Username: username }) + if (username && existingUser) { + console.log(`User with username ${username} already exists.`) + throw new Error('Användarnamnet finns redan, vänligen välj ett annat.') + } + + const newUser = await User.create({ Username: username, Password: password, Admin: false }) + return newUser +} + +// Work in progress: testing. +export async function deleteUserByUsername(username) { + await connectDB() + console.log(`Attempting to delete user with username: ${username}`) + return await User.findOneAndDelete({ Username: username }) +} + +// Work in progress: testing. +export async function updateUserPassword(username, newPassword) { + await connectDB() + newPassword = await hashPassword(newPassword) + return await User.findOneAndUpdate({ Username: username }, { Password: newPassword }, { new: true }) +} + +// Work in progress: testing. +export async function userExists(username) { + await connectDB() + const user = await User.findOne({ Username: username }) + return !!user +} + +export async function toggleAdmin(username) { + await connectDB() + const user = await User.findOne({ Username: username }) + if (!user) { + throw new Error(`User with username ${username} not found.`) + } + console.log(`Toggling admin status for user: ${username}. Current admin status: ${user.Admin}`) + user.Admin = !user.Admin + await user.save() + return user +} diff --git a/src/lib/utils/hashPassword.js b/src/lib/utils/hashPassword.js new file mode 100644 index 0000000..a80c4fa --- /dev/null +++ b/src/lib/utils/hashPassword.js @@ -0,0 +1,7 @@ +import bcrypt from 'bcrypt' + +export default async function hashPassword(password) { + const salt = await bcrypt.genSalt(15) + const hashedPassword = await bcrypt.hash(password, salt) + return hashedPassword +} diff --git a/src/styles/loginPage.scss b/src/styles/loginPage.scss index cdea79d..10ffe3a 100644 --- a/src/styles/loginPage.scss +++ b/src/styles/loginPage.scss @@ -46,6 +46,10 @@ color: #295377; } + &__error_text { + color: red; + } + &__form_password { color: #295377; }