diff --git a/backend/src/index.ts b/backend/src/index.ts index 6aac9a7..b6e34ec 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -4,11 +4,12 @@ import dotenv from 'dotenv' import init from './init' import Errors, { authorize } from './functions' import fs from 'fs' +import crypto from 'crypto' dotenv.config() const prisma = new PrismaClient() -const app = express() +const app = express() fs.mkdirSync("/var/lib/rheinefuerrheine/artikel", { recursive: true }) fs.mkdirSync("/var/lib/rheinefuerrheine/sponsoren", { recursive: true }) @@ -67,13 +68,15 @@ app.get('/api/auth/verify', async (req, res) => { app.post("/api/article/create", async (req, res) => { const token = req.headers.authorization?.split(' ')[1] - if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) const user = await authorize(token) - if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) - const { title, content, image } = req.body - if(!title || !content || !image) return res.status(400).send(Errors.MISSING_ITEMS) + const { title, content, image, sponsors } = req.body + if (!title || !content || !image) return res.status(400).send(Errors.MISSING_ITEMS) + + // sponsors is an array of sponsor IDs const article = await prisma.articles.create({ data: { @@ -83,6 +86,11 @@ app.post("/api/article/create", async (req, res) => { ID: user.ID } }, + sponsors: { + connect: sponsors.map((sponsor: string) => ({ + ID: sponsor + })) + } } }) @@ -95,13 +103,15 @@ app.post("/api/article/create", async (req, res) => { app.post("/api/article/edit/:id", async (req, res) => { const token = req.headers.authorization?.split(' ')[1] - if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) const user = await authorize(token) - if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) - const { title, content, image } = req.body - if(!title || !content || !image || !req.params.id) return res.status(400).send(Errors.MISSING_ITEMS) + const { title, content, image, sponsors } = req.body + if (!title || !content || !image || !req.params.id) return res.status(400).send(Errors.MISSING_ITEMS) + + // sponsors is an array of sponsor IDs const article = await prisma.articles.update({ where: { @@ -109,11 +119,17 @@ app.post("/api/article/edit/:id", async (req, res) => { }, data: { title, + sponsors: { + set: sponsors.map((sponsor: string) => ({ + ID: sponsor + })) + } } }) + fs.writeFileSync(`/var/lib/rheinefuerrheine/artikel/${article.ID}.html`, content) - fs.writeFileSync(`/var/lib/rheinefuerrheine/artikel/${article.ID}.banner`, image); + if (image !== "stale") fs.writeFileSync(`/var/lib/rheinefuerrheine/artikel/${article.ID}.banner`, image); res.send(article) }) @@ -142,7 +158,9 @@ app.get('/api/article/view/:id', async (req, res) => { createdAt: true, updatedAt: true, } - }) + }).catch(() => null) + + if (!article) return res.status(404).send(Errors.NOT_FOUND) res.send(article) }) @@ -155,10 +173,10 @@ app.get("/api/article/content/:id", async (req, res) => { app.get("/api/allArticles", async (req, res) => { const token = req.headers.authorization?.split(' ')[1] - if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) const user = await authorize(token) - if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) const articles = await prisma.articles.findMany({ select: { @@ -218,20 +236,20 @@ app.get('/api/articles/public', async (req, res) => { app.delete('/api/article/:article', async (req, res) => { const token = req.headers.authorization?.split(' ')[1] - if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) const user = await authorize(token) - if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) const article = await prisma.articles.findUnique({ where: { ID: req.params.article - }, + }, select: { ID: true, title: true, views: true, - + author: { select: { ID: true, @@ -244,9 +262,9 @@ app.delete('/api/article/:article', async (req, res) => { } }) - if(!article) return res.status(404).send(Errors.NOT_FOUND) + if (!article) return res.status(404).send(Errors.NOT_FOUND) - if((!user.admin && !user.article_manage) && article?.author.ID !== user.ID) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if ((!user.admin && !user.article_manage) && article?.author.ID !== user.ID) return res.status(401).send(Errors.INVALID_CREDENTIALS) await prisma.articles.delete({ where: { @@ -255,33 +273,41 @@ app.delete('/api/article/:article', async (req, res) => { }) fs.rmSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.html`) - if(fs.existsSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`)) fs.rmSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`) + if (fs.existsSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`)) fs.rmSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`) res.send('OK') }) -app.post('/api/article/banner/:article', async (req, res) => { - const token = req.headers.authorization?.split(' ')[1] +// app.post('/api/article/banner/:article', async (req, res) => { +// const token = req.headers.authorization?.split(' ')[1] - if(!token || !req.body) return res.status(401).send(Errors.INVALID_CREDENTIALS) +// if(!token || !req.body) return res.status(401).send(Errors.INVALID_CREDENTIALS) - const user = await authorize(token) - if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) +// const user = await authorize(token) +// if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) - fs.writeFileSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`, req.body.data); +// fs.writeFileSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`, req.body.data); - res.send('OK') -}) +// res.send('OK') +// }) app.get('/api/article/banner/:article', async (req, res) => { - if(!fs.existsSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`)) return res.status(404).send(Errors.NOT_FOUND) + if (!fs.existsSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`)) return res.status(404).send(Errors.NOT_FOUND) - res.sendFile(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`) + const content = fs.readFileSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`) + + // its a base64 encoded image. The type is appended at the beginning of the string (e.g. data:image/png;base64,....) Extract the type and send it as content type header and the rest as the body + const type = content.toString().split(';')[0].split(':')[1] + res.setHeader('Content-Type', type) + res.send(Buffer.from(content.toString().split(';base64,')[1], 'base64')) }) -app.get('/api/sponsors/getAll', async (req, res) => { +app.get('/api/sponsors/', async (req, res) => { const sponsors = await prisma.sponsors.findMany({ + orderBy: { + addedAt: 'desc' + } }) res.send(sponsors) @@ -290,19 +316,20 @@ app.get('/api/sponsors/getAll', async (req, res) => { app.post('/api/sponsors/create', async (req, res) => { const token = req.headers.authorization?.split(' ')[1] - if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) const user = await authorize(token) - if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) const { name, description, url, logo, banner } = req.body - if(!name || !description || !url || !logo || !banner) return res.status(400).send(Errors.MISSING_ITEMS) + if (!name || !description || !url || !logo || !banner) return res.status(400).send(Errors.MISSING_ITEMS) + const sponsor = await prisma.sponsors.create({ data: { name, description, - url, + url } }) @@ -315,13 +342,13 @@ app.post('/api/sponsors/create', async (req, res) => { app.patch('/api/sponsors/edit/:id', async (req, res) => { const token = req.headers.authorization?.split(' ')[1] - if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) const user = await authorize(token) - if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) const { name, description, url, logo, banner } = req.body - if(!name || !description || !url || !logo || !banner) return res.status(400).send(Errors.MISSING_ITEMS) + if (!name || !description || !url) return res.status(400).send(Errors.MISSING_ITEMS) const sponsor = await prisma.sponsors.update({ where: { @@ -334,8 +361,8 @@ app.patch('/api/sponsors/edit/:id', async (req, res) => { } }) - fs.writeFileSync(`/var/lib/rheinefuerrheine/sponsoren/${sponsor.ID}.logo`, logo) - fs.writeFileSync(`/var/lib/rheinefuerrheine/sponsoren/${sponsor.ID}.banner`, banner) + if (logo) fs.writeFileSync(`/var/lib/rheinefuerrheine/sponsoren/${sponsor.ID}.logo`, logo) + if (banner) fs.writeFileSync(`/var/lib/rheinefuerrheine/sponsoren/${sponsor.ID}.banner`, banner) res.send(sponsor) }) @@ -343,10 +370,10 @@ app.patch('/api/sponsors/edit/:id', async (req, res) => { app.delete('/api/sponsors/delete/:id', async (req, res) => { const token = req.headers.authorization?.split(' ')[1] - if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) const user = await authorize(token) - if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) await prisma.sponsors.delete({ where: { @@ -360,15 +387,49 @@ app.delete('/api/sponsors/delete/:id', async (req, res) => { res.send('OK') }) -app.get('/api/users/all', async (req, res) => { +app.get('/api/sponsors/:id', async (req, res) => { + const sponsor = await prisma.sponsors.findUnique({ + where: { + ID: req.params.id + } + }) + + if (!sponsor) return res.status(404).send(Errors.NOT_FOUND) + + res.send(sponsor) +}) + +app.get('/api/sponsors/logo/:id', async (req, res) => { + if (!fs.existsSync(`/var/lib/rheinefuerrheine/sponsoren/${req.params.id}.logo`)) return res.status(404).send(Errors.NOT_FOUND) + + const content = fs.readFileSync(`/var/lib/rheinefuerrheine/sponsoren/${req.params.id}.logo`) + + // its a base64 encoded image. The type is appended at the beginning of the string (e.g. data:image/png;base64,....) Extract the type and send it as content type header and the rest as the body + const type = content.toString().split(';')[0].split(':')[1] + res.setHeader('Content-Type', type) + res.send(Buffer.from(content.toString().split(';base64,')[1], 'base64')) +}) + +app.get('/api/sponsors/banner/:id', async (req, res) => { + if (!fs.existsSync(`/var/lib/rheinefuerrheine/sponsoren/${req.params.id}.banner`)) return res.status(404).send(Errors.NOT_FOUND) + + const content = fs.readFileSync(`/var/lib/rheinefuerrheine/sponsoren/${req.params.id}.banner`) + + // its a base64 encoded image. The type is appended at the beginning of the string (e.g. data:image/png;base64,....) Extract the type and send it as content type header and the rest as the body + const type = content.toString().split(';')[0].split(':')[1] + res.setHeader('Content-Type', type) + res.send(Buffer.from(content.toString().split(';base64,')[1], 'base64')) +}) + +app.get('/api/users', async (req, res) => { const token = req.headers.authorization?.split(' ')[1] - if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) const user = await authorize(token) - if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) - if(!user.admin && !user.user_manage) return res.status(401).send(Errors.INVALID_CREDENTIALS) + if (!user.admin && !user.user_manage) return res.status(401).send(Errors.INVALID_CREDENTIALS) const users = await prisma.users.findMany({ select: { @@ -385,7 +446,140 @@ app.get('/api/users/all', async (req, res) => { res.send(users) }) -app.listen(process.env.PORT, () => { +app.get('/api/user/:id', async (req, res) => { + const token = req.headers.authorization?.split(' ')[1] + + if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) + + const user = await authorize(token) + if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) + + if (!user.admin && !user.user_manage) return res.status(401).send(Errors.INVALID_CREDENTIALS) + + const foundUser = await prisma.users.findUnique({ + where: { + ID: req.params.id + }, + select: { + ID: true, + username: true, + admin: true, + article_create: true, + article_manage: true, + sponsor_manage: true, + user_manage: true, + } + }) + + if (!foundUser) return res.status(404).send(Errors.NOT_FOUND) + + res.send(foundUser) +}) + +app.post('/api/users/create', async (req, res) => { + const token = req.headers.authorization?.split(' ')[1] + + if (!token || !req.body) return res.status(401).send(Errors.INVALID_CREDENTIALS) + + const user = await authorize(token) + if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) + + if (!user.admin && !user.user_manage) return res.status(401).send(Errors.INVALID_CREDENTIALS) + + const { username, password, admin, article_create, article_manage, sponsor_manage, user_manage } = req.body + if (!username || !password || admin === undefined || article_create === undefined || article_manage === undefined || sponsor_manage === undefined || user_manage === undefined) return res.status(400).send(Errors.MISSING_ITEMS) + + const newUser = await prisma.users.create({ + data: { + username, + password, + token: crypto.randomBytes(64).toString('hex'), + admin, + article_create, + article_manage, + sponsor_manage, + user_manage, + } + }) + + res.send(newUser) +}) + +app.patch('/api/users/edit/:id', async (req, res) => { + const token = req.headers.authorization?.split(' ')[1] + + if (!token || !req.body) return res.status(401).send(Errors.INVALID_CREDENTIALS) + + const user = await authorize(token) + if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) + + if (!user.admin && !user.user_manage) return res.status(401).send(Errors.INVALID_CREDENTIALS) + + const { username, password, admin, article_create, article_manage, sponsor_manage, user_manage } = req.body + if (!username || admin === undefined || article_create === undefined || article_manage === undefined || sponsor_manage === undefined || user_manage === undefined) return res.status(400).send(Errors.MISSING_ITEMS) + + const newUser = await prisma.users.update({ + where: { + ID: req.params.id + }, + data: { + username, + admin, + article_create, + article_manage, + sponsor_manage, + user_manage, + ...(password && { password }) + } + }) + + res.send(newUser) +}) + +app.delete('/api/users/delete/:id', async (req, res) => { + const token = req.headers.authorization?.split(' ')[1] + + if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) + + const user = await authorize(token) + if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) + + if (!user.admin && !user.user_manage) return res.status(401).send(Errors.INVALID_CREDENTIALS) + + await prisma.users.delete({ + where: { + ID: req.params.id + } + }) + + res.send('OK') +}) + +app.get('/api/stats', async (req, res) => { + const token = req.headers.authorization?.split(' ')[1] + if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS) + + const user = await authorize(token) + if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS) + + const articles = await prisma.articles.count() + const users = await prisma.users.count() + const sponsors = await prisma.sponsors.count() + const views = await prisma.articles.aggregate({ + _sum: { + views: true + } + }) + + res.send({ + articles, + users, + sponsors, + views: views._sum.views + }) +}) + +app.listen(process.env.PORT, () => { console.log(`Server is running on port ${process.env.PORT}`) }) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 293b6e4..913fbc1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "@fontsource-variable/lexend": "^5.0.12", "@fontsource-variable/overpass": "^5.0.9", "@mui/icons-material": "^5.14.9", + "@mui/lab": "^5.0.0-alpha.154", "@mui/material": "^5.14.10", "@types/node": "^16.18.52", "@types/react": "^18.2.22", @@ -1948,9 +1949,9 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz", - "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", + "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2542,9 +2543,9 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.2.tgz", - "integrity": "sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz", + "integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==", "dependencies": { "@floating-ui/dom": "^1.5.1" }, @@ -3383,6 +3384,77 @@ } } }, + "node_modules/@mui/lab": { + "version": "5.0.0-alpha.154", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.154.tgz", + "integrity": "sha512-Rrhu8eUknjV6hhPMqq52e/p4/c6rvnu/k0AhysuljsHDZcHThYEZNe1mHFLveQ1RIje2VnJSsgmcNfcZKeOOAg==", + "dependencies": { + "@babel/runtime": "^7.23.4", + "@mui/base": "5.0.0-beta.25", + "@mui/system": "^5.14.19", + "@mui/types": "^7.2.10", + "@mui/utils": "^5.14.19", + "clsx": "^2.0.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": ">=5.10.11", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/lab/node_modules/@mui/base": { + "version": "5.0.0-beta.25", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.25.tgz", + "integrity": "sha512-Iiv+IcappRRv6IBlknIVmLkXxfp51NEX1+l9f+dIbBuPU4PaRULegr1lCeHKsC45KU5ruxM5xMg4R/de03aJQg==", + "dependencies": { + "@babel/runtime": "^7.23.4", + "@floating-ui/react-dom": "^2.0.4", + "@mui/types": "^7.2.10", + "@mui/utils": "^5.14.19", + "@popperjs/core": "^2.11.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "5.14.10", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.10.tgz", @@ -3433,12 +3505,12 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/@mui/private-theming": { - "version": "5.14.10", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.10.tgz", - "integrity": "sha512-f67xOj3H06wWDT9xBg7hVL/HSKNF+HG1Kx0Pm23skkbEqD2Ef2Lif64e5nPdmWVv+7cISCYtSuE2aeuzrZe78w==", + "version": "5.14.19", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.19.tgz", + "integrity": "sha512-U9w39VpXLGVM8wZlUU/47YGTsBSk60ZQRRxQZtdqPfN1N7OVllQeN4cEKZKR8PjqqR3aYRcSciQ4dc6CttRoXQ==", "dependencies": { - "@babel/runtime": "^7.22.15", - "@mui/utils": "^5.14.10", + "@babel/runtime": "^7.23.4", + "@mui/utils": "^5.14.19", "prop-types": "^15.8.1" }, "engines": { @@ -3446,7 +3518,7 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0", @@ -3459,11 +3531,11 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.14.10", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.10.tgz", - "integrity": "sha512-EJckxmQHrsBvDbFu1trJkvjNw/1R7jfNarnqPSnL+jEQawCkQIqVELWLrlOa611TFtxSJGkdUfCFXeJC203HVg==", + "version": "5.14.19", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.19.tgz", + "integrity": "sha512-jtj/Pyn/bS8PM7NXdFNTHWZfE3p+vItO4/HoQbUeAv3u+cnWXcTBGHHY/xdIn446lYGFDczTh1YyX8G4Ts0Rtg==", "dependencies": { - "@babel/runtime": "^7.22.15", + "@babel/runtime": "^7.23.4", "@emotion/cache": "^11.11.0", "csstype": "^3.1.2", "prop-types": "^15.8.1" @@ -3473,7 +3545,7 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@emotion/react": "^11.4.1", @@ -3490,15 +3562,15 @@ } }, "node_modules/@mui/system": { - "version": "5.14.10", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.10.tgz", - "integrity": "sha512-QQmtTG/R4gjmLiL5ECQ7kRxLKDm8aKKD7seGZfbINtRVJDyFhKChA1a+K2bfqIAaBo1EMDv+6FWNT1Q5cRKjFA==", + "version": "5.14.19", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.19.tgz", + "integrity": "sha512-4e3Q+2nx+vgEsd0h5ftxlZGB7XtkkPos/zWqCqnxUs1l/T70s0lF2YNrWHHdSQ7LgtBu0eQ0qweZG2pR7KwkAw==", "dependencies": { - "@babel/runtime": "^7.22.15", - "@mui/private-theming": "^5.14.10", - "@mui/styled-engine": "^5.14.10", - "@mui/types": "^7.2.4", - "@mui/utils": "^5.14.10", + "@babel/runtime": "^7.23.4", + "@mui/private-theming": "^5.14.19", + "@mui/styled-engine": "^5.14.19", + "@mui/types": "^7.2.10", + "@mui/utils": "^5.14.19", "clsx": "^2.0.0", "csstype": "^3.1.2", "prop-types": "^15.8.1" @@ -3508,7 +3580,7 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@emotion/react": "^11.5.0", @@ -3529,11 +3601,11 @@ } }, "node_modules/@mui/types": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz", - "integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==", + "version": "7.2.10", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.10.tgz", + "integrity": "sha512-wX1vbDC+lzF7FlhT6A3ffRZgEoKWPF8VqRoTu4lZwouFX2t90KyCMsgepMw5DxLak1BSp/KP86CmtZttikb/gQ==", "peerDependencies": { - "@types/react": "*" + "@types/react": "^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -3542,12 +3614,12 @@ } }, "node_modules/@mui/utils": { - "version": "5.14.10", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.10.tgz", - "integrity": "sha512-Rn+vYQX7FxkcW0riDX/clNUwKuOJFH45HiULxwmpgnzQoQr3A0lb+QYwaZ+FAkZrR7qLoHKmLQlcItu6LT0y/Q==", + "version": "5.14.19", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.19.tgz", + "integrity": "sha512-qAHvTXzk7basbyqPvhgWqN6JbmI2wLB/mf97GkSlz5c76MiKYV6Ffjvw9BjKZQ1YRb8rDX9kgdjRezOcoB91oQ==", "dependencies": { - "@babel/runtime": "^7.22.15", - "@types/prop-types": "^15.7.5", + "@babel/runtime": "^7.23.4", + "@types/prop-types": "^15.7.11", "prop-types": "^15.8.1", "react-is": "^18.2.0" }, @@ -3556,7 +3628,7 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0", @@ -4227,9 +4299,9 @@ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==" }, "node_modules/@types/prop-types": { - "version": "15.7.6", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.6.tgz", - "integrity": "sha512-RK/kBbYOQQHLYj9Z95eh7S6t7gq4Ojt/NT8HTk8bWVhA5DaF+5SMnxHKkP4gPNN3wAZkKP+VjAf0ebtYzf+fxg==" + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/q": { "version": "1.5.6", diff --git a/frontend/package.json b/frontend/package.json index 53fa68c..8ef5beb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,7 @@ "@fontsource-variable/lexend": "^5.0.12", "@fontsource-variable/overpass": "^5.0.9", "@mui/icons-material": "^5.14.9", + "@mui/lab": "^5.0.0-alpha.154", "@mui/material": "^5.14.10", "@types/node": "^16.18.52", "@types/react": "^18.2.22", diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 2a12462..ac5bc21 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,6 +1,6 @@ import { Box, Divider } from "@mui/material" import SidebarElement from "./SidebarElement" -import { Home, Newspaper, Person, Savings } from "@mui/icons-material" +import { ArrowBack, Home, Logout, Newspaper, Person, Savings } from "@mui/icons-material" function Sidebar() { @@ -38,6 +38,7 @@ function Sidebar() { + + + ) diff --git a/frontend/src/components/SidebarElement.tsx b/frontend/src/components/SidebarElement.tsx index e81715e..72d4115 100644 --- a/frontend/src/components/SidebarElement.tsx +++ b/frontend/src/components/SidebarElement.tsx @@ -1,4 +1,4 @@ -import { Box, SvgIconTypeMap, Typography } from "@mui/material"; +import { Box, SvgIconTypeMap, SxProps, Theme, Typography } from "@mui/material"; import { OverridableComponent } from "@mui/material/OverridableComponent"; import { useLocation, useNavigate } from "react-router-dom"; @@ -6,12 +6,14 @@ function SidebarElement({ Title, Icon, Path, + sx }: { Title: string; Icon: OverridableComponent> & { muiName: string; }; Path: string; + sx?: SxProps; }) { const navigate = useNavigate(); const location = useLocation(); @@ -38,6 +40,7 @@ function SidebarElement({ gap: "25px", background: (theme) => theme.palette.primary.main, }, + ...sx }} onClick={() => navigate(Path)} > diff --git a/frontend/src/components/SponsorCard.tsx b/frontend/src/components/SponsorCard.tsx deleted file mode 100644 index fd22355..0000000 --- a/frontend/src/components/SponsorCard.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { Avatar, Box, Typography } from "@mui/material"; -import { useRef, useState } from "react"; - -function SponsorCard() { - const logoRef = useRef(null); - const bannerRef = useRef(null); - - const [logo, setLogo] = useState(""); - const [banner, setBanner] = useState("https://placehold.co/500x150"); - - return ( - <> - { - const file = e.target.files?.[0]; - if (!file) return; - - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - setLogo(reader.result as string); - }; - }} - onAbort={() => {}} - required - /> - { - const file = e.target.files?.[0]; - if (!file) return; - - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - setBanner(reader.result as string); - }; - }} - onAbort={() => {}} - required - /> - - - banner bannerRef.current?.click()} - /> - - { - logoRef.current?.click(); - }} - /> - - - - Felix Orgel - - - - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam - nonumy eirmod tempor invidunt u - - - - - - ); -} - -export default SponsorCard; diff --git a/frontend/src/components/SponsorCardEditable.tsx b/frontend/src/components/SponsorCardEditable.tsx new file mode 100644 index 0000000..082a105 --- /dev/null +++ b/frontend/src/components/SponsorCardEditable.tsx @@ -0,0 +1,270 @@ +import { Avatar, Box, Button, TextField } from "@mui/material"; +import { useEffect, useRef, useState } from "react"; +import { getBaseURL } from "../functions"; + +function SponsorCardEditable({ + onSave, + edit, +}: { + onSave: (result: { + name: string, + description: string, + logo: string | null, + banner: string | null, + url: string, + }) => void; + edit?: { + id: string, + name: string, + description: string, + url: string, + }; +}) { + const logoRef = useRef(null); + const bannerRef = useRef(null); + + const [logo, setLogo] = useState("https://placehold.co/150x150"); + const [banner, setBanner] = useState("https://placehold.co/500x150"); + + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + + const [url, setUrl] = useState(""); + + useEffect(() => { + if (!edit) return; + + setName(edit.name); + setDescription(edit.description); + setUrl(edit.url); + setLogo(`${getBaseURL()}/api/sponsors/logo/${edit.id}`); + setBanner(`${getBaseURL()}/api/sponsors/banner/${edit.id}`); + }, [edit]); + + const valid = () => { + if (!name) return false; + if (!description) return false; + if (!logo) return false; + if (!banner) return false; + if (!url) return false; + + if (logo.startsWith("https://placehold.co/")) return false; + if (banner.startsWith("https://placehold.co/")) return false; + + return true; + }; + + return ( + <> + { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + setLogo(reader.result as string); + }; + }} + onAbort={() => {}} + required + /> + { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + setBanner(reader.result as string); + }; + }} + onAbort={() => {}} + required + /> + + + banner bannerRef.current?.click()} + /> + + { + logoRef.current?.click(); + }} + /> + + + { + if (e.target.value.length >= 12) return; + if (e.target.value.includes("\n")) return; + setName(e.target.value); + }} + /> + + { + if (e.target.value.length >= 100) return; + if (e.target.value.includes("\n")) return; + setDescription(e.target.value); + }} + /> + + + + + { + if (e.target.value.length >= 128) return; + if (e.target.value.includes("\n")) return; + setUrl(e.target.value); + }} + /> + + + + ); +} + +export default SponsorCardEditable; diff --git a/frontend/src/components/SponsorImageSmall.tsx b/frontend/src/components/SponsorImageSmall.tsx new file mode 100644 index 0000000..7d4f4e2 --- /dev/null +++ b/frontend/src/components/SponsorImageSmall.tsx @@ -0,0 +1,53 @@ +import { Box, Grid, Avatar } from "@mui/material"; + +export function SponsorImageSmall({ + name, + image, + description, + link, + onRemove, +}: { + name: string; + image: string; + description: string; + link: string; + onRemove?: () => void; +}): JSX.Element { + return ( + + + { + window.open(link, "_blank"); + }) + } + /> + + + ); +} diff --git a/frontend/src/components/SponsorModal.tsx b/frontend/src/components/SponsorModal.tsx index 322e542..02c4ce0 100644 --- a/frontend/src/components/SponsorModal.tsx +++ b/frontend/src/components/SponsorModal.tsx @@ -2,12 +2,14 @@ import { Close } from "@mui/icons-material"; import { Backdrop, Box, + CircularProgress, IconButton, - TextField, Typography, } from "@mui/material"; -import { useState } from "react"; -import SponsorCard from "./SponsorCard"; +import { useEffect, useState } from "react"; +import SponsorCardEditable from "./SponsorCardEditable"; +import axios from "axios"; +import { getBaseURL } from "../functions"; function SponsorModal({ id, @@ -16,64 +18,192 @@ function SponsorModal({ id: string | null; onClose?: () => void; }) { + const [saving, setSaving] = useState(false); + const [loading, setLoading] = useState(false); + const [data, setData] = useState(null); + + useEffect(() => { + setData(null); + if(!id) return; + if (id === "create") return; + setLoading(true); + + axios + .get(`${getBaseURL()}/api/sponsors/${id}`) + .then((res) => { + setData(res.data); + setLoading(false); + }) + .catch(() => { + onClose?.(); + }); + }, [id]); + if (!id) return <>; - return ( - - + + + Lade... + + + ); + return ( + <> + {saving && ( + + + + Speichere... + + + )} + - - Sponsor Erstellen - - - - - + + + Sponsor { id === "create" ? "Erstellen" : "Bearbeiten" } + + + + + - - - + { + setSaving(true); + if (id === "create") { + axios + .post( + `${getBaseURL()}/api/sponsors/create`, + { + name: result.name, + description: result.description, + url: result.url, + logo: result.logo, + banner: result.banner, + }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem( + "token" + )}`, + }, + } + ) + .then(() => { + window.location.reload(); + }); + } else { + result.logo = result.logo?.includes("http") + ? null + : result.logo; + result.banner = result.banner?.includes("http") + ? null + : result.banner; + + axios + .patch( + `${getBaseURL()}/api/sponsors/edit/${id}`, + { + name: result.name, + description: result.description, + url: result.url, + logo: result.logo, + banner: result.banner, + }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem( + "token" + )}`, + }, + } + ) + .then(() => { + window.location.reload(); + }); + } + }} + /> + + + ); } diff --git a/frontend/src/components/UserModal.tsx b/frontend/src/components/UserModal.tsx new file mode 100644 index 0000000..e6cacae --- /dev/null +++ b/frontend/src/components/UserModal.tsx @@ -0,0 +1,382 @@ +import { Close } from "@mui/icons-material"; +import { + Backdrop, + Box, + Button, + Checkbox, + CircularProgress, + IconButton, + Stack, + TextField, + Typography, +} from "@mui/material"; +import { useEffect, useState } from "react"; +import axios from "axios"; +import { getBaseURL } from "../functions"; +import { LoadingButton } from "@mui/lab"; +import { sha256 } from "js-sha256"; + +function UserModal({ + id, + onClose, +}: { + id: string | null; + onClose?: () => void; +}) { + const [saving, setSaving] = useState(false); + const [loading, setLoading] = useState(false); + + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + + const [permissions, setPermissions] = useState({ + admin: false, + article_create: false, + article_manage: false, + sponsor_manage: false, + user_manage: false, + }); + + const valid = () => { + if (!username) return false; + if (!password && id === "create") return false; + + return true; + }; + + useEffect(() => { + setUsername(""); + setPassword(""); + setPermissions({ + admin: false, + article_create: false, + article_manage: false, + sponsor_manage: false, + user_manage: false, + }); + + if (!id) return; + if (id === "create") return; + setLoading(true); + + axios + .get(`${getBaseURL()}/api/user/${id}`, { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }) + .then((res) => { + setUsername(res.data.username); + + setPermissions({ + admin: res.data.admin, + article_create: res.data.article_create, + article_manage: res.data.article_manage, + sponsor_manage: res.data.sponsor_manage, + user_manage: res.data.user_manage, + }); + + setLoading(false); + }); + }, [id]); + + if (!id) return <>; + if (loading) + return ( + + + + Lade... + + + ); + return ( + <> + + + + + + Benutzer {id === "create" ? "Erstellen" : "Bearbeiten"} + + + + + + + { + if (e.target.value.length >= 64) return; + if (e.target.value.includes("\n")) return; + setUsername(e.target.value); + }} + sx={{ + width: "80%", + }} + /> + { + if (e.target.value.length >= 64) return; + if (e.target.value.includes("\n")) return; + setPassword(e.target.value); + }} + sx={{ + width: "80%", + }} + /> + + + + { + setPermissions({ + ...permissions, + admin: e.target.checked, + }); + }} + /> + Admin + + + + { + setPermissions({ + ...permissions, + article_create: e.target.checked, + }); + }} + /> + Artikel Erstellen + + + + { + setPermissions({ + ...permissions, + article_manage: e.target.checked, + }); + }} + /> + Artikel Verwalten + + + + { + setPermissions({ + ...permissions, + sponsor_manage: e.target.checked, + }); + }} + /> + Sponsoren Verwalten + + + + { + setPermissions({ + ...permissions, + user_manage: e.target.checked, + }); + }} + /> + Nutzer Verwalten + + + + { + setSaving(true); + + if (id === "create") { + axios + .post( + `${getBaseURL()}/api/users/create`, + { + username, + password: sha256(`rheine ${password.trim()} rheine`), + admin: permissions.admin, + article_create: permissions.admin + ? false + : permissions.article_create, + article_manage: permissions.admin + ? false + : permissions.article_manage, + sponsor_manage: permissions.admin + ? false + : permissions.sponsor_manage, + user_manage: permissions.admin + ? false + : permissions.user_manage, + }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem( + "token" + )}`, + }, + } + ) + .then(() => { + setSaving(false); + onClose?.(); + }); + } else { + axios + .patch( + `${getBaseURL()}/api/users/edit/${id}`, + { + username, + password: password + ? sha256(`rheine ${password.trim()} rheine`) + : undefined, + admin: permissions.admin, + article_create: permissions.admin + ? false + : permissions.article_create, + article_manage: permissions.admin + ? false + : permissions.article_manage, + sponsor_manage: permissions.admin + ? false + : permissions.sponsor_manage, + user_manage: permissions.admin + ? false + : permissions.user_manage, + }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem( + "token" + )}`, + }, + } + ) + .then(() => { + setSaving(false); + onClose?.(); + }); + } + }} + loading={saving} + > + Speichern + + + + + ); +} + +export default UserModal; diff --git a/frontend/src/index.css b/frontend/src/index.css index 0f9443a..f615158 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -125,3 +125,6 @@ a { text-decoration: none; } +input[name="suneditor_image_radio"] { + display: none; +} \ No newline at end of file diff --git a/frontend/src/pages/AdminFrame.tsx b/frontend/src/pages/AdminFrame.tsx index df246a5..a93913b 100644 --- a/frontend/src/pages/AdminFrame.tsx +++ b/frontend/src/pages/AdminFrame.tsx @@ -6,6 +6,7 @@ import Artikel from "./admin/Artikel"; import Sponsoren from "./admin/Sponsoren"; import Benutzer from "./admin/Benutzer"; import ArticleEditor from "./admin/ArticleEditor"; +import Logout from "./admin/Logout"; function AdminFrame() { return ( @@ -34,6 +35,7 @@ function AdminFrame() { } /> } /> } /> + } /> diff --git a/frontend/src/pages/Artikel.tsx b/frontend/src/pages/Artikel.tsx index 461ddc5..1d66257 100644 --- a/frontend/src/pages/Artikel.tsx +++ b/frontend/src/pages/Artikel.tsx @@ -1,12 +1,20 @@ /* eslint-disable jsx-a11y/alt-text */ import { useParams } from "react-router-dom"; import TopBar from "../components/TopBar"; -import { Box, CircularProgress, Grid, Typography } from "@mui/material"; +import { + Box, + CircularProgress, + Grid, + Tooltip, + Typography, +} from "@mui/material"; import { getBaseURL } from "../functions"; import { useEffect, useRef, useState } from "react"; import axios from "axios"; import "./Artikel.css"; +import "suneditor/dist/css/suneditor.min.css"; +import { SponsorImageSmall } from "../components/SponsorImageSmall"; function Artikel() { const ref = useRef(null); @@ -16,25 +24,10 @@ function Artikel() { const [loadingContent, setLoadingContent] = useState(true); const [article, setArticle] = useState(null); - const [banner, setBanner] = useState(""); - - const loadbanner = () => { - axios - .get(`${getBaseURL()}/api/article/banner/${id}`) - .then((response) => { - setBanner(response.data); - }) - .catch((err) => { - console.log(err); - setBanner("https://placehold.co/1920x1080"); - }); - }; useEffect(() => { if (!id) return; - loadbanner(); - axios.get(`${getBaseURL()}/api/article/content/${id}`).then((response) => { if (ref.current) { ref.current.innerHTML = response.data; @@ -56,7 +49,7 @@ function Artikel() { } }, 500); }); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [id]); return ( @@ -64,20 +57,22 @@ function Artikel() { {loadingContent && } {!loadingContent && ( - + - + + {article?.sponsors.map((sponsor) => ( + + + + ))} + (""); + const [sponsorsAvail, setSponsorsAvail] = useState([]); + const [sponsorsSelected, setSponsorsSelected] = useState([]); + + const sponsorsSelectedRef = useRef([]); + + sponsorsSelectedRef.current = sponsorsSelected; + + const [sponsorMenuEl, setSponsorMenuEl] = useState(null); + const loadbanner = () => { - axios - .get(`${getBaseURL()}/api/article/banner/${id}`) - .then((response) => { - setBanner(response.data); - }) - .catch((err) => { - console.log(err); - setBanner("https://placehold.co/1920x1080"); - }); + if (!id) setBanner("https://placehold.co/1920x1080"); + else setBanner(`${getBaseURL()}/api/article/banner/${id}`); }; const save = async (content: string) => { const title = document.getElementById("article-title")?.innerText; - const banner = document.getElementById("articleBanner-content")?.getAttribute("src"); + let banner = document + .getElementById("articleBanner-content") + ?.getAttribute("src"); if (!title) return false; if (!banner) return false; - console.log(content); + if (banner.startsWith("http")) banner = "stale"; + setSaving(true); const res = await axios .post( @@ -71,6 +81,7 @@ function ArticleEditor() { : `${getBaseURL()}/api/article/edit/${id}`, { title: title, + sponsors: sponsorsSelectedRef.current.map((e) => e.ID), content: content, image: banner, }, @@ -105,17 +116,17 @@ function ArticleEditor() { if (!res.data) return; document.getElementById("article-title")!.innerText = res.data.title; + setSponsorsSelected(res.data.sponsors); axios.get(`${getBaseURL()}/api/article/content/${id}`).then((res) => { if (!res.data) return; editor.current?.setContents(res.data); - setTimeout(() => { // get all images with a data-size property const images = document.querySelectorAll("img[data-size]"); console.log(images); - + // loop through all images and set the width style to the data-size property for (let image of [...images]) { (image as HTMLElement).style.width = @@ -125,11 +136,61 @@ function ArticleEditor() { }, 500); }); }); - // eslint-disable-next-line react-hooks/exhaustive-deps + + axios.get(`${getBaseURL()}/api/sponsors`).then((res) => { + if (!res.data) return; + + setSponsorsAvail(res.data); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [id]); return ( <> + { + setSponsorMenuEl(null); + }} + > + !sponsorsSelected.map(e => e.ID).includes(e.ID))} + getOptionLabel={(option) => option.name} + renderOption={(props, option) => ( + img": { mr: 2, flexShrink: 0 } }} + {...props} + onClick={() => { + sponsorsSelected.push(option as Types.Sponsor); + setSponsorsSelected(sponsorsSelected); + setSponsorMenuEl(null); + }} + > + + {option.name} + + )} + renderInput={(params) => ( + + )} + /> + {loading && } {!loading && ( @@ -170,7 +237,7 @@ function ArticleEditor() { accept="image/*" style={{ display: "none" }} onChange={(e) => { - const file = e.target.files?.[0] + const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); @@ -183,10 +250,11 @@ function ArticleEditor() { required /> - - + - Lorem ipsum whatever + Titel - - + {sponsorsSelected.map((sponsor) => ( + { + setSponsorsSelected( + sponsorsSelected.filter((e) => e.ID !== sponsor.ID) + ); + }} + /> + ))} {}} + onClick={(e) => { + setSponsorMenuEl(e.currentTarget as any); + }} /> @@ -354,28 +427,4 @@ function ArticleEditor() { export default ArticleEditor; -function SponsorImageSmall({ - name, - image, - description, - link, -}: { - name: string; - image: string; - description: string; - link: string; -}): JSX.Element { - return ( - - - - - - ); -} + diff --git a/frontend/src/pages/admin/Benutzer.tsx b/frontend/src/pages/admin/Benutzer.tsx index ab6b13e..900bced 100644 --- a/frontend/src/pages/admin/Benutzer.tsx +++ b/frontend/src/pages/admin/Benutzer.tsx @@ -1,8 +1,4 @@ -import { - ManageAccounts, - PersonAdd, - PersonRemove, -} from "@mui/icons-material"; +import { ManageAccounts, PersonAdd, PersonRemove } from "@mui/icons-material"; import { Box, Button, @@ -17,17 +13,17 @@ import { } from "@mui/material"; import axios from "axios"; import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; import { getBaseURL } from "../../functions"; +import UserModal from "../../components/UserModal"; function Benutzer() { - const navigate = useNavigate(); + const [modalID, setModelID] = useState(null); const [users, setUsers] = useState([]); useEffect(() => { axios - .get(`${getBaseURL()}/api/users/all`, { + .get(`${getBaseURL()}/api/users`, { headers: { Authorization: `Bearer ${localStorage.getItem("token")}`, }, @@ -41,140 +37,165 @@ function Benutzer() { }, []); return ( - + <> + { + setModelID(null); + }} + /> - - Benutzer - - - - + + Benutzer + - + + - - - - - Name - - Berechtigungen - Optionen - - - - {users.map((user) => ( + + +
+ - {user.username} - - {[ - { name: "Admin", value: user.admin }, - { name: "Artikel Erstellen", value: user.article_create }, - { name: "Artikel Verwalten", value: user.article_manage }, - { name: "Sponsoren Verwalten", value: user.sponsor_manage }, - { name: "Nutzer Verwalten", value: user.user_manage }, - ] - .filter((e) => e.value) - .map((e) => e.name) - .join(", ")} - - - - - - - - - - - - + + Name + Berechtigungen + - ))} - -
-
+ + + {users.map((user) => ( + + {user.username} + + {[ + { name: "Admin", value: user.admin }, + { name: "Artikel Erstellen", value: user.article_create }, + { name: "Artikel Verwalten", value: user.article_manage }, + { name: "Sponsoren Verwalten", value: user.sponsor_manage }, + { name: "Nutzer Verwalten", value: user.user_manage }, + ] + .filter((e) => e.value) + .map((e) => e.name) + .join(", ")} + + + + { + setModelID(user.ID); + }} + > + + + + { + axios + .delete(`${getBaseURL()}/api/users/delete/${user.ID}`, { + headers: { + Authorization: `Bearer ${localStorage.getItem( + "token" + )}`, + }, + }) + .then(() => { + setUsers( + users.filter((e) => e.ID !== user.ID) + ); + }); + }} + > + + + + + + ))} + + +
+ ); } diff --git a/frontend/src/pages/admin/Dashboard.tsx b/frontend/src/pages/admin/Dashboard.tsx index 3be1194..febb959 100644 --- a/frontend/src/pages/admin/Dashboard.tsx +++ b/frontend/src/pages/admin/Dashboard.tsx @@ -1,8 +1,34 @@ import { Visibility, Newspaper, Person, Savings } from "@mui/icons-material"; import { Box, Grid, SvgIconTypeMap, Typography } from "@mui/material"; import { OverridableComponent } from "@mui/material/OverridableComponent"; +import axios from "axios"; +import { useEffect, useState } from "react"; +import { getBaseURL } from "../../functions"; function Dashboard() { + const [stats, setStats] = useState<{ + users: number; + articles: number; + sponsors: number; + views: number; + }>({ + users: 0, + articles: 0, + sponsors: 0, + views: 0, + }); + + useEffect(() => { + axios.get(`${getBaseURL()}/api/stats`, { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }).then((response) => { + console.log(response.data); + setStats(response.data); + }); + }, []); + return ( - - - - + + + + ); @@ -49,7 +75,8 @@ function StatDisplay({ flexDirection: "column", padding: "10px", - background: (theme) => `linear-gradient(45deg, ${theme.palette.secondary.light}, ${theme.palette.secondary.main})`, + background: (theme) => + `linear-gradient(45deg, ${theme.palette.secondary.light}, ${theme.palette.secondary.main})`, width: "100%", }} @@ -74,14 +101,16 @@ function StatDisplay({ {Title} - + padding: "5px", + backgroundColor: "#fff", + borderRadius: "5px", + }} + > - - - {Value} - + }} + > + + {Value} + diff --git a/frontend/src/pages/admin/Logout.tsx b/frontend/src/pages/admin/Logout.tsx new file mode 100644 index 0000000..a300bb2 --- /dev/null +++ b/frontend/src/pages/admin/Logout.tsx @@ -0,0 +1,31 @@ +import { Backdrop, CircularProgress, Typography } from "@mui/material" +import { useEffect } from "react" + +function Logout() { + useEffect(() => { + localStorage.removeItem("token") + setTimeout(() => { + window.location.href = "/login" + }, 1000) + }, []) + + return ( + + + + Logge aus... + + + ) +} + +export default Logout \ No newline at end of file diff --git a/frontend/src/pages/admin/Sponsoren.tsx b/frontend/src/pages/admin/Sponsoren.tsx index a35c56d..d7480dc 100644 --- a/frontend/src/pages/admin/Sponsoren.tsx +++ b/frontend/src/pages/admin/Sponsoren.tsx @@ -15,21 +15,51 @@ import { TableRow, Typography, Tooltip, + Avatar, + Backdrop, + CircularProgress, } from "@mui/material"; -import { useNavigate } from "react-router-dom"; import SponsorModal from "../../components/SponsorModal"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import axios from "axios"; +import { getBaseURL } from "../../functions"; +import moment from "moment"; function Sponsoren() { - const navigate = useNavigate(); - const [modalID, setModelID] = useState(null); + const [sponsors, setSponsors] = useState([]); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + setLoaded(false); + axios.get(`${getBaseURL()}/api/sponsors/`).then((res) => { + setSponsors(res.data); + setLoaded(true); + }); + }, []); + return ( <> - { - setModelID(null); - }} /> + + + + { + setModelID(null); + }} + /> - + + {" "} + + Name - Ansprechpartner Datum Optionen - - RF Computer GMBH - R. Fink - - 7 Nov. 2023 - - - - - ( + + + + + {sponsor.name} + + {moment(sponsor.addedAt).locale("de").format("ll")} + + + + + - - - + padding: "5px", + backgroundColor: "#fff", + borderRadius: "5px", + }} + onClick={() => { + setModelID(sponsor.ID); + }} + > + + + - - + - - - - - - + padding: "5px", + backgroundColor: "#fff", + borderRadius: "5px", + "&:hover": { + background: "red", + }, + }} + onClick={() => { + axios + .delete( + `${getBaseURL()}/api/sponsors/delete/${ + sponsor.ID + }`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem( + "token" + )}`, + }, + } + ) + .then(() => { + setSponsors( + sponsors.filter((s) => s.ID !== sponsor.ID) + ); + }); + }} + > + + + + + + + ))} diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index 6ab8b21..ff38fb3 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -7,22 +7,28 @@ declare namespace Types { ID: string; username: string; } - sponsors: { - ID: string; - name: string; - url: string; - description: string; - }[]; + sponsors: Sponsor[]; updatedAt: Date; createdAt: Date; } - interface User { + interface User extends UserPermissions { ID: string; username: string; + } + + interface UserPermissions { admin: boolean; article_create: boolean; article_manage: boolean; sponsor_manage: boolean; user_manage: boolean; } + + interface Sponsor { + ID: string; + name: string; + url: string; + description: string; + addedAt: Date; + } } \ No newline at end of file