Made a bunch of changes / fixed eevrything

This commit is contained in:
Donavon McDowell 2024-12-17 16:45:27 -07:00
parent 2776f339c6
commit 8af208a8f5
22 changed files with 167 additions and 351 deletions

View File

@ -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}!`)
});

View File

@ -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}`));

View File

@ -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

View File

@ -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
'<span style="background: yellow;">$1</span>'
);
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;

View File

@ -13,7 +13,7 @@ const DocumentSchema = new mongoose.Schema(
visibility: {
type: String,
required: true,
default: "Public",
default: "public",
},
tags: {
type: [String], //Array of strings

View File

@ -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: [
{

View File

@ -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

View File

@ -1 +1 @@
BACKEND_URI='http://localhost:3000'
PUBLIC_BASE_URL='http://localhost:3000'

View File

@ -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)
}

View File

@ -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 };
export async function load({ locals }) {
return { user: locals.user }
}
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 };
};

View File

@ -1,32 +1,17 @@
<script lang="ts">
import { onMount } from 'svelte';
import { PUBLIC_BASE_URL } from '$env/static/public';
import '../app.postcss';
import { page } from "$app/stores";
import { isAuthenticated } from '$lib/stores/auth';
let dropDown = false; // For the dropdown menu (login menu)
export let data: {
isAuthenticated: boolean;
errorMessage: string;
function getInitials(name) {
return name
.split(" ") // Split the name into words
.slice(0, 2) // Take the first two words
.map(word => word.charAt(0).toUpperCase()) // Get the first letter of each word and capitalize
.join(""); // Join them into a string
}
// Update the store values when this layout is loaded
$: isAuthenticated.set(data.isAuthenticated); //This is a store (MAGIC)
const handleSignOut = async () => {
const response = await fetch('/signout', {
method: 'POST',
});
if (response.ok) {
data.isAuthenticated = false;
} else {
console.error('Failed to sign out.');
}
};
</script>
<header class="bg-blue-700 flex flex-row p-4 place-content-between items-center font-roboto shadow-xl">
@ -35,17 +20,17 @@
</a>
<div class="relative dropdown-container">
<button class="rounded-3xl bg-blue-500 p-2 flex flex-row justify-center items-center p-1 hover:bg-blue-400"
<button class="rounded-3xl bg-blue-500 flex flex-row justify-center items-center p-1 hover:bg-blue-400"
on:click={() => dropDown = !dropDown}>
{#if data.isAuthenticated}
<img
src="https://www.citypng.com/public/uploads/preview/hd-mcdonalds-red-round-circular-circle-logo-icon-png-image-7017516947898359qtpcakiqi.png"
class="avatar rounded-full size-8"
alt="User Avatar"
/>
{#if $page.data.user.name}
<div class="rounded-full text-white font-bold bg-blue-400 p-2">
{getInitials($page.data.user.name)}
</div>
<i class="fa-solid fa-chevron-down ml-2 mr-2" style="color: #FFFFFF;"></i>
{:else}
<div class="rounded-full text-white font-bold bg-blue-400 p-2 px-3">
<i class="fa-solid fa-right-to-bracket" style="color: #FFFFFF;"></i>
</div>
<i class="fa-solid fa-chevron-down ml-2 mr-2" style="color: #FFFFFF;"></i>
{/if}
</button>
@ -53,17 +38,16 @@
{#if dropDown}
<!-- Dropdown menu -->
<div class="absolute right-0 mt-2 bg-white shadow-lg rounded-md p-4 z-50 text-nowrap">
{#if data.isAuthenticated}
{#if $page.data.user.name}
<span class="signedInText block mb-2 text-sm">
<small>Signed in</small><br/>
<small>Signed in as</small><br/>
<b>{$page.data.user.name}</b>
</span>
<button
class="bg-blue-500 text-white px-4 py-2 rounded-3xl hover:bg-blue-400 text-nowrap w-full"
on:click={() => {dropDown = false; handleSignOut();}}>
Sign Out
</button>
<form action="/logout" method="POST">
<button class="bg-blue-500 text-white px-4 py-2 rounded-3xl hover:bg-blue-400 text-nowrap w-full">Sign Out</button>
</form>
{:else}
<span class="notSignedInText block mb-2">You are not signed in</span>
<a href="/login" on:click={() => {dropDown = false;}}>
<button
class="bg-blue-500 text-white px-4 py-2 rounded-3xl hover:bg-blue-400 text-nowrap w-full"

View File

@ -1,59 +0,0 @@
<script lang="ts">
import '../app.postcss';
import { signIn, signOut } from "@auth/sveltekit/client"
import { page } from "$app/stores"
let dropDown = false;
</script>
<header class="bg-blue-700 flex flex-row p-4 place-content-between items-center font-roboto shadow-xl">
<a href="/home">
<img class="max-w-36 mr-5 ml-4 aspect-auto" src="/branding/mpe_logo.png" alt="MPE Logo">
</a>
<div class="relative dropdown-container">
<button class="rounded-3xl bg-blue-500 p-2 flex flex-row justify-center items-center p-1 hover:bg-blue-400"
on:click={() => dropDown = !dropDown}>
{#if $page.data.session}
{#if $page.data.session.user?.image}
<img
src={$page.data.session.user.image}
class="avatar rounded-full size-8"
alt="User Avatar"
/>
<i class="fa-solid fa-chevron-down ml-2 mr-2" style="color: #FFFFFF;"></i>
{/if}
{:else}
<i class="fa-solid fa-right-to-bracket" style="color: #FFFFFF;"></i>
<i class="fa-solid fa-chevron-down ml-2 mr-2" style="color: #FFFFFF;"></i>
{/if}
</button>
{#if dropDown}
<!-- Dropdown menu -->
<div class="absolute right-0 mt-2 bg-white shadow-lg rounded-md p-4 z-50 text-nowrap">
{#if $page.data.session}
<span class="signedInText block mb-2 text-sm">
<small>Signed in as</small><br/>
<strong>{$page.data.session.user?.name ?? "User"}</strong>
</span>
<button
class="bg-blue-500 text-white px-4 py-2 rounded-3xl hover:bg-blue-400 text-nowrap w-full"
on:click={() => { signOut(); dropDown = false;}}>
Sign Out
</button>
{:else}
<span class="notSignedInText block mb-2">You are not signed in</span>
<button
class="bg-blue-500 text-white px-4 py-2 rounded-3xl hover:bg-blue-400 text-nowrap w-full"
on:click={() => { signIn('microsoft-entra-id'); dropDown = false;}}>
Sign In
</button>
{/if}
</div>
{/if}
</div>
</header>
<slot />

View File

@ -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)
});

View File

@ -1,7 +1,7 @@
<script lang="js">
import { onMount } from 'svelte';
import { page } from "$app/stores"
import { PUBLIC_BASE_URL } from '$env/static/public';
import { isAuthenticated } from '$lib/stores/auth';
@ -21,7 +21,7 @@
try {
// Make the GET request to your API
const response = await fetch(PUBLIC_BASE_URL+`/document/${id}`);
const response = await fetch(`${PUBLIC_BASE_URL}/api/document/${id}`);
if (response.ok) {
document = await response.json(); // Store document data
} else {
@ -36,7 +36,8 @@
let agreeToDelete = false;
// Function to delete a document by ID
async function deleteDocument(id) {
const url = PUBLIC_BASE_URL+`/documents/${id}`; // Update with your API base URL if different
const url = `${PUBLIC_BASE_URL}/api/document/${id}`; // Update with your API base URL if different
const token = $page.data.user?.token;
// Show the confirmation popup
alertPopup = true;
@ -60,6 +61,7 @@ async function deleteDocument(id) {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
"Authorization": `Bearer ${token}`,
},
});
@ -125,7 +127,7 @@ async function deleteDocument(id) {
<div class="flex flex-col justify-between">
<div class="w-full flex flex-row justify-between">
<h1 class="text-2xl">{document.title}</h1>
{#if $isAuthenticated}
{#if $page.data.user.name}
<div>
<a href="/edit?id={id}"><button class="bg-yellow-500 hover:bg-blue-400 border-double border-blue-100 border-2 p-3 rounded-full px-4 text-white font-roboto"><i class="fa-solid fa-pencil fa-lg"></i></button></a>
<button class="bg-red-500 hover:bg-blue-400 border-double border-blue-100 border-2 p-3 rounded-full px-4 text-white font-roboto" on:click={() => { deleteDocument(id)}}><i class="fa-solid fa-trash fa-lg"></i></button>
@ -150,10 +152,10 @@ async function deleteDocument(id) {
{/if}
</div>
<div class="">
{#if document.created_at}
<p class="font-semibold">Created: {new Date(document.created_at).toLocaleString()}</p>
{#if document.updated_at}
<p class="font-semibold">Updated: {new Date(document.updated_at).toLocaleString()}</p>
{#if document.createdAt}
<p class="font-semibold">Created: {new Date(document.createdAt).toLocaleString()}</p>
{#if document.edited_by}
<p class="font-semibold">Updated: {new Date(document.updatedAt).toLocaleString()}</p>
{/if}
{/if}
</div>

View File

@ -14,14 +14,12 @@
let title = "";
let document = null; // To store fetched document data
let error = null; // To store any error that occurs during fetch
let documentId;
let tags = [];
// Function to fetch the document by ID
async function fetchDocumentById(id) {
try {
const response = await fetch(PUBLIC_BASE_URL+`/document/${id}`);
const response = await fetch(`${PUBLIC_BASE_URL}/api/document/${id}`);
if (response.ok) {
const fetchedDocument = await response.json();
document = fetchedDocument; // Update document data
@ -41,7 +39,6 @@
onMount(() => {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id'); // Get the `id` from the query string
documentId = id;
if (!id) {
error = 'ID is required';
@ -57,11 +54,11 @@
async function save() {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id'); // Get the `id` from the query string
const url = PUBLIC_BASE_URL+"/document/"+id;
const url = `${PUBLIC_BASE_URL}/api/document/`+id;
console.log("Title value:", title);
let edited_by = $page.data.session.user?.name ?? "User";
const edited_by = $page.data.user?.name;
const token = $page.data.user?.token;
// Get the HTML from the tiptap editor
if (editorRef) {
@ -86,7 +83,8 @@
const response = await fetch(url, {
method: "PUT",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
body: JSON.stringify(payload)
});

View File

@ -2,7 +2,6 @@
import { onMount } from 'svelte';
import { page } from "$app/stores"
import { PUBLIC_BASE_URL } from '$env/static/public';
import { isAuthenticated } from '$lib/stores/auth';
let query = false;
let searchQuery = "";
@ -25,7 +24,7 @@
// Function to simulate search API call
async function searchDocuments(query) {
try {
const res = await fetch(PUBLIC_BASE_URL+`/search?query=${query}`);
const res = await fetch(`${PUBLIC_BASE_URL}/api/document/search?query=${query}`);
const data = await res.json();
if (data.length > 0) {
@ -42,7 +41,7 @@
// Function to fetch all documents from the API
async function getAllDocuments() {
try {
const res = await fetch(PUBLIC_BASE_URL+`/document`);
const res = await fetch(`${PUBLIC_BASE_URL}/api/document`);
if (!res.ok) {
throw new Error(`Failed to fetch documents: ${res.statusText}`);
@ -81,7 +80,7 @@
</svelte:head>
<div class="flex items-center justify-center h-full flex-col">
{#if $isAuthenticated}
{#if $page.data.user.role == "admin"}
<div class="flex flex-row justify-end w-full">
<a href="/add" class="bg-green-500 hover:bg-blue-400 border-double border-blue-100 border-2 p-3 rounded-full px-4 mr-4 mt-3 text-white font-roboto">
<i class="fa-solid fa-folder-plus fa-lg"></i>

View File

@ -1,6 +1,5 @@
<script>
import { PUBLIC_BASE_URL } from '$env/static/public';
async function handleSignIn(event) {
event.preventDefault(); // Prevent the default form submission
@ -11,7 +10,7 @@
const data = Object.fromEntries(formData.entries());
try {
const response = await fetch(`${PUBLIC_BASE_URL}/auth/login`, {
const response = await fetch(`${PUBLIC_BASE_URL}/api/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@ -0,0 +1,27 @@
import { redirect } from '@sveltejs/kit'
export const load = async () => {
// we only use this endpoint for the api
// and don't need to see the page
redirect(302, '/')
}
export const actions = {
default({ cookies }) {
cookies.set('token', '', {
path: '/',
expires: new Date(0),
})
cookies.set('name', '', {
path: '/',
expires: new Date(0),
})
cookies.set('email', '', {
path: '/',
expires: new Date(0),
})
// redirect the user
redirect(302, '/login')
},
}

View File

@ -1,3 +0,0 @@
import { signIn } from "../../auth"
import type { Actions } from "./$types"
export const actions: Actions = { default: signIn }

View File

@ -1,12 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
export const POST: RequestHandler = async ({ cookies }) => {
try {
// Clear the authToken cookie
cookies.delete('authToken', { path: '/' });
return new Response(JSON.stringify({ success: true }), { status: 200 });
} catch (error) {
console.error('Error during sign out:', error);
return new Response(JSON.stringify({ success: false, message: 'Failed to sign out.' }), { status: 500 });
}
};

View File

@ -1,6 +1,5 @@
<script>
import { PUBLIC_BASE_URL } from '$env/static/public';
async function handleSignup(event) {
event.preventDefault(); // Prevent the default form submission
@ -11,7 +10,7 @@
const data = Object.fromEntries(formData.entries());
try {
const response = await fetch(`${PUBLIC_BASE_URL}/user`, {
const response = await fetch(`${PUBLIC_BASE_URL}/api/user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',