diff --git a/backend/package-lock.json b/backend/package-lock.json index 57eeaf0..f650467 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -21,6 +21,7 @@ "express-session": "^1.17.3", "jsonwebtoken": "^9.0.2", "jwt-simple": "^0.5.6", + "mongoose-slug-generator": "^1.0.4", "passport": "^0.6.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", @@ -1687,6 +1688,11 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3051,6 +3057,19 @@ "url": "https://opencollective.com/mongoose" } }, + "node_modules/mongoose-slug-generator": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mongoose-slug-generator/-/mongoose-slug-generator-1.0.4.tgz", + "integrity": "sha512-YTluRZROhrHgcncstJTJsxT4K6xR2xIGtF9a0uf4Z1mNFADBVoyanezdKgjQBxN2zfB8PH6wpU8mh4AFxL4+4w==", + "dependencies": { + "async": "^1.5.0", + "shortid": "^2.2.4", + "speakingurl": "^7.0.0" + }, + "engines": { + "node": ">= 0.12.7" + } + }, "node_modules/mongoose/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3105,6 +3124,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/nanoid": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", + "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -3610,6 +3634,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/shortid": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", + "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dependencies": { + "nanoid": "^2.1.0" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -3682,6 +3715,14 @@ "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", "dev": true }, + "node_modules/speakingurl": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-7.0.0.tgz", + "integrity": "sha512-b3w3MPlZxf/s3YhzyD12e9yw3bdl/23gNgxq2fUeKTJ1jt0ZTTTddZTl5SaQv4GwCNJ56DM3kOeRKeO9xab7wQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index d7fd073..47606b9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,6 +24,7 @@ "express-session": "^1.17.3", "jsonwebtoken": "^9.0.2", "jwt-simple": "^0.5.6", + "mongoose-slug-generator": "^1.0.4", "passport": "^0.6.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", diff --git a/backend/src/controller/linkController.ts b/backend/src/controller/linkController.ts new file mode 100644 index 0000000..f0a1b51 --- /dev/null +++ b/backend/src/controller/linkController.ts @@ -0,0 +1,59 @@ +import { Request, Response } from "express"; +import Link from "../models/Link"; + +export default { + // GET-Route zum Abrufen aller Links + getAllLinks: async (req: Request, res: Response) => { + try { + const links = await Link.find(); + console.log(links); + res.json(links); + } catch (error) { + res.status(500).json({ message: error.message }); + } + }, + + // POST-Route zum Erstellen eines neuen Links + createLink: async (req: Request, res: Response) => { + try { + const link = new Link(req.body); + const newLink = await link.save(); + console.log(newLink); + res.status(201).json(newLink); + } catch (error) { + res.status(400).json({ message: error.message }); + } + }, + + // DELETE-Route zum Löschen eines Links + deleteLink: async (req: Request, res: Response) => { + console.log(req.params); + try { + const { id } = req.params; + const link = await Link.findByIdAndDelete(id); + + if (!link) { + return res.status(404).json({ message: "Link not found" }); + } + res.status(200).json({ message: "Link deleted succesfully" }); + } catch (error) { + res.status(500).json({ message: error.message }); + } + }, + + // PUT-Route zum Aktualisieren eines Links + updateLink: async (req: Request, res: Response) => { + console.log(req.body); + try { + const { id } = req.params; + const link = await Link.findByIdAndUpdate(id, req.body, { new: true }); + if (!link) { + return res.status(404).json({ message: "Link not found" }); + } + console.log("Updated link data:", link); + res.status(200).json({ message: "Link updated succesfully", link: link }); + } catch (error) { + res.status(400).json({ message: error.message }); + } + }, +}; diff --git a/backend/src/index.ts b/backend/src/index.ts index 137248d..425506f 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,4 +1,4 @@ -import express, { Request, Response, NextFunction } from "express"; +import express, { Request, Response } from "express"; import bodyParser from "body-parser"; import cors from "cors"; import connectToDB from "./db"; @@ -6,9 +6,12 @@ import { PORT, PASSPORT_SECRET } from "./config"; import passport from "passport"; import { Strategy as LocalStrategy } from "passport-local"; import { UserModel } from "./models/Users"; -import accountController from "./controller/accountController"; import initializePassport from "./middleware/auth"; import session from "express-session"; +import userRoutes from "./routes/userRoutes"; +import statusRoutes from "./routes/statusRoutes"; +import linkRoutes from "./routes/linkRoutes"; + const app = express(); app.use(express.json()); @@ -40,29 +43,9 @@ passport.use(new LocalStrategy(UserModel.authenticate())); passport.serializeUser(UserModel.serializeUser()); passport.deserializeUser(UserModel.deserializeUser()); -app.get("/", (req, res) => { - res.send("Introduction JWT Auth"); -}); - -app.post("/login", passport.authenticate("local"), accountController.login); -app.post("/register", accountController.register); -app.get( - "/status", - passport.authenticate("jwt", { session: false }), - accountController.getStatus -); -app.post("/logout", function (req, res, next) { - console.log(req.session); - - req.session.destroy(function (err) { - if (err) { - return next(err); - } - res.json({ - message: "User successfully logout!", - }); - }); -}); +app.use("/", userRoutes); +app.use("/", statusRoutes); +app.use("/", linkRoutes); //Express-Server app.listen(PORT, () => { diff --git a/backend/src/models/Link.ts b/backend/src/models/Link.ts new file mode 100644 index 0000000..1401943 --- /dev/null +++ b/backend/src/models/Link.ts @@ -0,0 +1,22 @@ +import mongoose, { Document, Schema } from "mongoose"; +export interface Link extends Document { + url: string; + path: string; + created: Date; + modified: Date; +} + +const linkSchema: Schema = new Schema( + { + url: { type: String, required: true }, + path: { type: String, required: true, unique: true }, + //slug: { type: String, slug: "path", unique: true }, + }, + { + timestamps: { createdAt: "created", updatedAt: "modified" }, + } +); + +const Link = mongoose.model("Link", linkSchema); + +export default Link; diff --git a/backend/src/routes/linkRoutes.ts b/backend/src/routes/linkRoutes.ts new file mode 100644 index 0000000..b2e2656 --- /dev/null +++ b/backend/src/routes/linkRoutes.ts @@ -0,0 +1,48 @@ +import express from "express"; +import passport from "passport"; +import linkController from "../controller/linkController"; + +const router = express.Router(); + +// GET-Route zum Abrufen aller Links +router.get( + "/link", + passport.authenticate("jwt", { session: false }), + linkController.getAllLinks +); + +// POST-Route zum Erstellen eines neuen Links +router.post( + "/link", + passport.authenticate("jwt", { session: false }), + linkController.createLink +); + +// DELETE-Route zum Löschen eines Links +router.delete( + "/link/:id", + passport.authenticate("jwt", { session: false }), + linkController.deleteLink +); + +// PUT-Route zum Aktualisieren eines Links +router.put( + "/link/:id", + passport.authenticate("jwt", { session: false }), + linkController.updateLink +); + +// Test Routen +router.post("/test", (req, res) => { + res.json({ message: "Test route works!" }); +}); +router.get("/test", (req, res) => { + res.json({ message: "Test route works!" }); +}); +router.put("/test", (req, res) => { + res.json({ message: "Test route works!" }); +}); +router.delete("/test", (req, res) => { + res.json({ message: "Test route works!" }); +}); +export default router; diff --git a/backend/src/routes/statusRoutes.ts b/backend/src/routes/statusRoutes.ts new file mode 100644 index 0000000..a6256ae --- /dev/null +++ b/backend/src/routes/statusRoutes.ts @@ -0,0 +1,12 @@ +import express from "express"; +import passport from "passport"; +import accountController from "../controller/accountController"; + +const router = express.Router(); +router.get( + "/status", + passport.authenticate("jwt", { session: false }), + accountController.getStatus +); + +export default router; diff --git a/backend/src/routes/userRoutes.ts b/backend/src/routes/userRoutes.ts new file mode 100644 index 0000000..82d608f --- /dev/null +++ b/backend/src/routes/userRoutes.ts @@ -0,0 +1,22 @@ +import express from "express"; +import passport from "passport"; +import accountController from "../controller/accountController"; + +const router = express.Router(); + +router.post("/login", passport.authenticate("local"), accountController.login); + +router.post("/register", accountController.register); + +router.post("/logout", function (req, res, next) { + req.session.destroy(function (err) { + if (err) { + return next(err); + } + res.json({ + message: "User successfully logout!", + }); + }); +}); + +export default router; diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js new file mode 100644 index 0000000..51d85ee --- /dev/null +++ b/public/mockServiceWorker.js @@ -0,0 +1,303 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker (1.3.2). + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70' +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: INTEGRITY_CHECKSUM, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: true, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + const accept = request.headers.get('accept') || '' + + // Bypass server-sent events. + if (accept.includes('text/event-stream')) { + return + } + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = Math.random().toString(16).slice(2) + + event.respondWith( + handleRequest(event, requestId).catch((error) => { + if (error.name === 'NetworkError') { + console.warn( + '[MSW] Successfully emulated a network error for the "%s %s" request.', + request.method, + request.url, + ) + return + } + + // At this point, any exception indicates an issue with the original request/response. + console.error( + `\ +[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, + request.method, + request.url, + `${error.name}: ${error.message}`, + ) + }), + ) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const clonedResponse = response.clone() + sendToClient(client, { + type: 'RESPONSE', + payload: { + requestId, + type: clonedResponse.type, + ok: clonedResponse.ok, + status: clonedResponse.status, + statusText: clonedResponse.statusText, + body: + clonedResponse.body === null ? null : await clonedResponse.text(), + headers: Object.fromEntries(clonedResponse.headers.entries()), + redirected: clonedResponse.redirected, + }, + }) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + const clonedRequest = request.clone() + + function passthrough() { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const headers = Object.fromEntries(clonedRequest.headers.entries()) + + // Remove MSW-specific request headers so the bypassed requests + // comply with the server's CORS preflight check. + // Operate with the headers as an object because request "Headers" + // are immutable. + delete headers['x-msw-bypass'] + + return fetch(clonedRequest, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Bypass requests with the explicit bypass header. + // Such requests can be issued by "ctx.fetch()". + if (request.headers.get('x-msw-bypass') === 'true') { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const clientMessage = await sendToClient(client, { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + mode: request.mode, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.text(), + bodyUsed: request.bodyUsed, + keepalive: request.keepalive, + }, + }) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'MOCK_NOT_FOUND': { + return passthrough() + } + + case 'NETWORK_ERROR': { + const { name, message } = clientMessage.data + const networkError = new Error(message) + networkError.name = name + + // Rejecting a "respondWith" promise emulates a network error. + throw networkError + } + } + + return passthrough() +} + +function sendToClient(client, message) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [channel.port2]) + }) +} + +function sleep(timeMs) { + return new Promise((resolve) => { + setTimeout(resolve, timeMs) + }) +} + +async function respondWithMock(response) { + await sleep(response.delay) + return new Response(response.body, response) +}