diff --git a/backend/routes/msal.js b/backend/routes/msal.js deleted file mode 100644 index cdd6920..0000000 --- a/backend/routes/msal.js +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -const fs = require('fs'); -const crypto = require('crypto'); -const express = require('express'); - -const msal = require('@azure/msal-node'); - -/** - * If you have encrypted your private key with a *pass phrase* as recommended, - * you'll need to decrypt it before passing it to msal-node for initialization. - */ -// Secrets should never be hardcoded. The dotenv npm package can be used to store secrets or certificates -// in a .env file (located in project's root directory) that should be included in .gitignore to prevent -// accidental uploads of the secrets. - -// Certificates can also be read-in from files via NodeJS's fs module. However, they should never be -// stored in the project's directory. Production apps should fetch certificates from -// Azure KeyVault (https://azure.microsoft.com/products/key-vault), or other secure key vaults. - -// Please see "Certificates and Secrets" (https://learn.microsoft.com/azure/active-directory/develop/security-best-practices-for-app-registration#certificates-and-secrets) -// for more information. -const privateKeySource = fs.readFileSync('../certs/example.key'); - -const privateKeyObject = crypto.createPrivateKey({ - key: privateKeySource, - passphrase: "2255", // enter your certificate passphrase here - format: 'pem' -}); - -const privateKey = privateKeyObject.export({ - format: 'pem', - type: 'pkcs8' -}); - -// Before running the sample, you will need to replace the values in the config -const config = { - auth: { - clientId: "3cdfac60-e7fb-4648-89d3-67966c497d35", //Client ID - authority: "https://login.microsoftonline.com/538b9b1c-23fa-4102-b36e-a4d83fc9c4c1", //Tenant ID - clientCertificate: { - thumbprint: 'DD79B973F2D634840948970C712907DF4423C982', // can be obtained when uploading certificate to Azure AD - privateKey: privateKey, - } - }, - system: { - loggerOptions: { - loggerCallback(loglevel, message, containsPii) { - console.log(message); - }, - piiLoggingEnabled: false, - logLevel: msal.LogLevel.Verbose, - } - } -}; - -// Create msal application object -const cca = new msal.ConfidentialClientApplication(config); - -// Create Express app -const app = express(); - -app.use(express.urlencoded({ extended: false })); - -app.get('/', (req, res) => { - const authCodeUrlParameters = { - scopes: ["user.read"], - redirectUri: "http://localhost:3000/redirect", - responseMode: 'form_post', - }; - - // get url to sign user in and consent to scopes needed for application - cca.getAuthCodeUrl(authCodeUrlParameters).then((response) => { - console.log(response); - res.redirect(response); - }).catch((error) => console.log(JSON.stringify(error))); -}); - -app.post('/redirect', (req, res) => { - const tokenRequest = { - code: req.body.code, - scopes: ["user.read"], - redirectUri: "http://localhost:3000/redirect", - }; - - cca.acquireTokenByCode(tokenRequest).then((response) => { - console.log("\nResponse: \n:", response); - res.status(200).send('Congratulations! You have signed in successfully'); - }).catch((error) => { - console.log(error); - res.status(500).send(error); - }); -}); - -const SERVER_PORT = process.env.PORT || 3000; - -app.listen(SERVER_PORT, () => { - console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`) -}); \ No newline at end of file diff --git a/backend/routes/passport.js b/backend/routes/passport.js deleted file mode 100644 index 576647c..0000000 --- a/backend/routes/passport.js +++ /dev/null @@ -1,64 +0,0 @@ -const express = require('express'); -const session = require('express-session'); -const passport = require('passport'); -const { OIDCStrategy } = require('passport-azure-ad'); - -const app = express(); - -// Session setup -app.use( - session({ - secret: 'your-secret', - resave: false, - saveUninitialized: true, - }) -); - -// Azure AD OIDC Strategy -passport.use( - new OIDCStrategy( - { - identityMetadata: `https://login.microsoftonline.com/538b9b1c-23fa-4102-b36e-a4d83fc9c4c1/v2.0/.well-known/openid-configuration`, - clientID: '3cdfac60-e7fb-4648-89d3-67966c497d35', - responseType: 'code', - responseMode: 'query', - redirectUrl: 'http://localhost:3000/auth/callback', - clientSecret: '5Gi8Q~_pmDtvN3.Jwqt85kiI.uiyAAC7Z.4iFayY', - allowHttpForRedirectUrl: true, - }, - (issuer, sub, profile, accessToken, refreshToken, done) => { - // Save the user profile and tokens - return done(null, { profile, accessToken, refreshToken }); - } - ) -); - -// Passport serialization -passport.serializeUser((user, done) => done(null, user)); -passport.deserializeUser((user, done) => done(null, user)); - -// Initialize Passport -app.use(passport.initialize()); -app.use(passport.session()); - -// Authentication routes -app.get('/auth', passport.authenticate('azuread-openidconnect')); - -app.get( - '/auth/callback', - passport.authenticate('azuread-openidconnect', { failureRedirect: '/' }), - (req, res) => { - res.send("Success"); - } -); - -// Logout route -app.get('/logout', (req, res) => { - req.logout(() => { - res.redirect('/'); - }); -}); - -// Start server -const port = process.env.PORT || 3000; -app.listen(port, () => console.log(`Server running on http://localhost:${port}`)); diff --git a/backend/src/controllers/authController.js b/backend/src/controllers/authController.js index 4fc9004..7cbc8ff 100644 --- a/backend/src/controllers/authController.js +++ b/backend/src/controllers/authController.js @@ -35,7 +35,31 @@ exports.login = async (req, res) => { } // Set the token as an HttpOnly cookie - res.cookie('authToken', token, { + res.cookie('token', token, { + httpOnly: true, // Cannot be accessed by JavaScript + secure: process.env.NODE_ENV === 'production', // Use Secure flag only in production (requires HTTPS) + sameSite: 'Strict', // Prevents CSRF attacks + maxAge: 3600000, // 1 hour + }); + + // Set the token as an HttpOnly cookie + res.cookie('name', user.name, { + httpOnly: true, // Cannot be accessed by JavaScript + secure: process.env.NODE_ENV === 'production', // Use Secure flag only in production (requires HTTPS) + sameSite: 'Strict', // Prevents CSRF attacks + maxAge: 3600000, // 1 hour + }); + + // Set the token as an HttpOnly cookie + res.cookie('email', user.email, { + httpOnly: true, // Cannot be accessed by JavaScript + secure: process.env.NODE_ENV === 'production', // Use Secure flag only in production (requires HTTPS) + sameSite: 'Strict', // Prevents CSRF attacks + maxAge: 3600000, // 1 hour + }); + + // Set the token as an HttpOnly cookie + res.cookie('role', user.role, { httpOnly: true, // Cannot be accessed by JavaScript secure: process.env.NODE_ENV === 'production', // Use Secure flag only in production (requires HTTPS) sameSite: 'Strict', // Prevents CSRF attacks diff --git a/backend/src/controllers/documentController.js b/backend/src/controllers/documentController.js index 0e0ef9d..0c99bda 100644 --- a/backend/src/controllers/documentController.js +++ b/backend/src/controllers/documentController.js @@ -30,6 +30,47 @@ exports.getDocumentById = async (req, res) => { } }; +exports.searchDocuments = async (req, res) => { + try { + const { query } = req.query; // Extract the search query from the query string + + if (!query) { + return res.status(400).json({ message: "Search query is required." }); + } + + // Perform the search + const documents = await Document.find({ + $or: [ + { title: { $regex: query, $options: "i" } }, // Case-insensitive match in title + { body: { $regex: query, $options: "i" } }, // Case-insensitive match in body + { tags: { $regex: query, $options: "i" } }, // Case-insensitive match in tags array + ], + }); + + // Highlight the query in the fields + const highlightedDocuments = documents.map((doc) => { + const highlightMatch = (text) => + text.replace( + new RegExp(`(${query})`, "gi"), // Match the query case-insensitively + '$1' + ); + + return { + ...doc._doc, // Convert Mongoose document to plain object + title: highlightMatch(doc.title || ""), + body: highlightMatch(doc.body || ""), + tags: doc.tags.map((tag) => highlightMatch(tag)), // Highlight each tag + }; + }); + + res.json(highlightedDocuments); // Return the matching and highlighted documents + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + + + exports.updateDocumentById = async (req, res) => { try { const documentId = req.params.id; diff --git a/backend/src/models/Document.js b/backend/src/models/Document.js index e58541e..e8ebbca 100644 --- a/backend/src/models/Document.js +++ b/backend/src/models/Document.js @@ -13,7 +13,7 @@ const DocumentSchema = new mongoose.Schema( visibility: { type: String, required: true, - default: "Public", + default: "public", }, tags: { type: [String], //Array of strings diff --git a/backend/src/models/User.js b/backend/src/models/User.js index fd74cb0..c6f69c9 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -21,7 +21,7 @@ const UserSchema = new mongoose.Schema( role: { type: String, required: false, // Optional for OAuth users - default: "User", //User = Read, Write Permissions | Admin = Read, Write, Delete Permissions + default: "user", //User = Read, Write Permissions | Admin = Read, Write, Delete Permissions }, oauthProviders: [ { diff --git a/backend/src/routes/documentRoutes.js b/backend/src/routes/documentRoutes.js index 4609175..a3de99a 100644 --- a/backend/src/routes/documentRoutes.js +++ b/backend/src/routes/documentRoutes.js @@ -4,6 +4,7 @@ const router = express.Router(); const documentController = require('../controllers/documentController'); router.post('/', documentController.createDocument); //Create +router.get('/search', documentController.searchDocuments); //Get a document by id router.get('/', documentController.getDocument); //Get all documents router.get('/:id', documentController.getDocumentById); //Get a document by id router.put('/:id', authenticateToken, documentController.updateDocumentById); //Update a document by id diff --git a/frontend/env.locals b/frontend/env.locals index 3f8e4f4..ea69bd1 100644 --- a/frontend/env.locals +++ b/frontend/env.locals @@ -1 +1 @@ -BACKEND_URI='http://localhost:3000' \ No newline at end of file +PUBLIC_BASE_URL='http://localhost:3000' \ No newline at end of file diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts new file mode 100644 index 0000000..ff6461b --- /dev/null +++ b/frontend/src/hooks.server.ts @@ -0,0 +1,20 @@ +export async function handle({ event, resolve }) { + // this cookie would be set inside a login route + const token = event.cookies.get('token'); + const name = event.cookies.get('name'); + const email = event.cookies.get('email'); + const role = event.cookies.get('role'); + + // you can get the user data from a database + // const user = await getUser(session) + + // this is passed to `event` inside server `load` functions + // and passed to handlers inside `+page.ts` + event.locals.user = { + token: token, + name: name, + email: email, + role: role + } + return resolve(event) +} \ No newline at end of file diff --git a/frontend/src/routes/+layout.server.ts b/frontend/src/routes/+layout.server.ts index 4c85c3f..a1e537d 100644 --- a/frontend/src/routes/+layout.server.ts +++ b/frontend/src/routes/+layout.server.ts @@ -1,42 +1,3 @@ -import type { LayoutServerLoad } from './$types'; -import { PUBLIC_BASE_URL } from '$env/static/public'; - -export const load: LayoutServerLoad = async ({ cookies, fetch }) => { - let isAuthenticated = false; - let user = null; - let errorMessage = ''; - - // Retrieve the auth token from cookies - const token = cookies.get('authToken'); - - if (!token) { - errorMessage = 'No auth token found. Please log in.'; - return { isAuthenticated, user, errorMessage }; - } - - try { - // Make a server-side fetch request to validate the token - const response = await fetch(`${PUBLIC_BASE_URL}/auth/status`, { - method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, - credentials: 'include', - }); - - if (response.ok) { - const data = await response.json(); - isAuthenticated = data.authenticated; - user = data.user; - } else { - errorMessage = 'Authentication failed. Please log in again.'; - } - } catch (error) { - console.error('Error checking authentication:', error); - errorMessage = 'An error occurred while checking authentication.'; - } - - // Return data to the layout - return { isAuthenticated, user, errorMessage }; -}; - +export async function load({ locals }) { + return { user: locals.user } +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index d812d5b..1fb907e 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,32 +1,17 @@
@@ -35,17 +20,17 @@
- - \ No newline at end of file diff --git a/frontend/src/routes/add/+page.svelte b/frontend/src/routes/add/+page.svelte index 4e8720c..f4e04ea 100644 --- a/frontend/src/routes/add/+page.svelte +++ b/frontend/src/routes/add/+page.svelte @@ -13,9 +13,9 @@ let title = ""; async function save() { - const url = PUBLIC_BASE_URL+"/document"; - - let created_by = $page.data.session.user?.name ?? "User"; + const url = `${PUBLIC_BASE_URL}/api/document`; + const token = $page.data.user?.token; + let created_by = $page.data.user?.name ?? "User"; // Get the HTML from the tiptap editor if (editorRef) { @@ -39,7 +39,8 @@ const response = await fetch(url, { method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "Authorization": `Bearer ${token}`, }, body: JSON.stringify(payload) }); diff --git a/frontend/src/routes/document/+page.svelte b/frontend/src/routes/document/+page.svelte index 4cbd915..121ea41 100644 --- a/frontend/src/routes/document/+page.svelte +++ b/frontend/src/routes/document/+page.svelte @@ -1,7 +1,7 @@