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 (
+
+ )
+}
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 (
+
+ )
+ }
+}
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
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 (
+
+ )
+}
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
+
+ {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;
}