diff --git a/.gitignore b/.gitignore index e35094d..232fd4d 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +**/**/.env diff --git a/api/package-lock.json b/api/package-lock.json index 3d1b60b..d1f7bb2 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -13,6 +13,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.0", + "express-oauth2-jwt-bearer": "^1.6.0", "mongodb": "^6.9.0", "mongoose": "^8.4.1", "ts-node-dev": "^2.0.0", @@ -686,6 +687,17 @@ "node": ">= 0.10.0" } }, + "node_modules/express-oauth2-jwt-bearer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/express-oauth2-jwt-bearer/-/express-oauth2-jwt-bearer-1.6.0.tgz", + "integrity": "sha512-HXnez7vocYlOqlfF3ozPcf/WE3zxT7zfUNfeg5FHJnvNwhBYlNXiPOvuCtBalis8xcigvwtInzEKhBuH87+9ug==", + "dependencies": { + "jose": "^4.13.1" + }, + "engines": { + "node": "^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0 || ^20.2.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -993,6 +1005,14 @@ "node": ">=0.12.0" } }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -2422,6 +2442,14 @@ "vary": "~1.1.2" } }, + "express-oauth2-jwt-bearer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/express-oauth2-jwt-bearer/-/express-oauth2-jwt-bearer-1.6.0.tgz", + "integrity": "sha512-HXnez7vocYlOqlfF3ozPcf/WE3zxT7zfUNfeg5FHJnvNwhBYlNXiPOvuCtBalis8xcigvwtInzEKhBuH87+9ug==", + "requires": { + "jose": "^4.13.1" + } + }, "fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2625,6 +2653,11 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, + "jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==" + }, "kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", diff --git a/api/package.json b/api/package.json index 17c2024..c0ce802 100644 --- a/api/package.json +++ b/api/package.json @@ -5,8 +5,8 @@ "main": "src/server.ts", "scripts": { "build": "tsc", - "start": "nodemon ./dist/server.js", - "dev": "nodemon ./src/server.ts", + "start": "node src/index.ts", + "dev": "npx ts-node-dev --respawn --pretty --transpile-only src/server.ts", "test": "echo \"Error: no test specified\" && exit 1", "prettier": "npx prettier --write .", "format": "prettier --check ." @@ -19,6 +19,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.0", + "express-oauth2-jwt-bearer": "^1.6.0", "mongodb": "^6.9.0", "mongoose": "^8.4.1", "ts-node-dev": "^2.0.0", diff --git a/api/src/controllers/auth0-errors.ts b/api/src/controllers/auth0-errors.ts new file mode 100644 index 0000000..23dcee5 --- /dev/null +++ b/api/src/controllers/auth0-errors.ts @@ -0,0 +1,35 @@ +import { Request, Response, NextFunction } from "express"; +import { + InvalidTokenError, + UnauthorizedError, +} from "express-oauth2-jwt-bearer"; + +export const errorHandler = ( + error: any, + request: Request, + response: Response, + next: NextFunction, +) => { + console.log("Auth Error"); + + if (error instanceof InvalidTokenError) { + const message = "Bad credentials"; + + response.status(error.status).json({ message }); + + return; + } + + if (error instanceof UnauthorizedError) { + const message = "Requires authentication"; + + response.status(error.status).json({ message }); + + return; + } + + const status = 500; + const message = "Internal Server Error"; + + response.status(status).json({ message }); +}; diff --git a/api/src/controllers/auth0-middleware.ts b/api/src/controllers/auth0-middleware.ts new file mode 100644 index 0000000..ed88db0 --- /dev/null +++ b/api/src/controllers/auth0-middleware.ts @@ -0,0 +1,21 @@ +import { auth } from "express-oauth2-jwt-bearer"; +import dotenv from "dotenv"; +import path from "path"; + +dotenv.config({ path: path.resolve(__dirname, "../.env") }); + +const auth0Domain = process.env.AUTH0_DOMAIN; +const auth0Audience = process.env.AUTH0_AUDIENCE; + +try { + if (!auth0Domain || !auth0Audience) { + throw new Error("AUTH0_DOMAIN or AUTH0_AUDIENCE is not set"); + } +} catch (error) { + console.error(error); +} + +export const validateAccessToken = auth({ + issuerBaseURL: `https://${auth0Domain}`, + audience: auth0Audience, +}); diff --git a/api/src/controllers/auth0-notFound.ts b/api/src/controllers/auth0-notFound.ts new file mode 100644 index 0000000..752e809 --- /dev/null +++ b/api/src/controllers/auth0-notFound.ts @@ -0,0 +1,11 @@ +import { Request, Response, NextFunction } from "express"; + +export const notFoundHandler = ( + request: Request, + response: Response, + next: NextFunction, +) => { + const message = "Not Found"; + + response.status(404).json({ message }); +}; diff --git a/api/src/routes/user.ts b/api/src/routes/user.ts index ecfa60e..f3a3b1d 100644 --- a/api/src/routes/user.ts +++ b/api/src/routes/user.ts @@ -2,9 +2,13 @@ import express from "express"; import mongoose from "mongoose"; import dbConnect from "../config/db"; import sgMail from "@sendgrid/mail"; +// import { validateAccessToken } from "../controllers/auth0-middleware"; const router = express.Router(); +// TODO: Add auth0 middleware +// router.use(validateAccessToken); + // Call the dbConnect function to connect to MongoDB dbConnect(); @@ -67,9 +71,15 @@ router.post("/create-user", async (req: any, res: any) => { // Test route to check if the API is working router.post("/test", async (req: any, res: any) => { console.log("Received group data:"); - const { name } = req.body; - return res.status(200).json({ name }); + let name; + if (req.body.name === undefined) { + name = "empty"; + } else { + ({ name } = req.body); + } + + return res.status(200).json(`Your name is ${name}`); }); router.post("/send-email", async (req: any, res: any) => { diff --git a/api/src/routes/workshop.ts b/api/src/routes/workshop.ts index 4fa779c..13d47a8 100644 --- a/api/src/routes/workshop.ts +++ b/api/src/routes/workshop.ts @@ -1,11 +1,15 @@ import express from "express"; import mongoose from "mongoose"; import dbConnect from "../config/db"; // Import the dbConnect function +// import { validateAccessToken } from "../controllers/auth0-middleware"; import { createWorkshop, getWorkshop } from "../controllers/workshopController"; const router = express.Router(); +// TODO: Add auth0 middleware +// router.use(validateAccessToken); + // Call the dbConnect function to connect to MongoDB dbConnect(); @@ -51,6 +55,12 @@ router.get( }, ); +router.post("/testId/:id", async (req: any, res: any) => { + res + .status(200) + .json({ message: "Workshop test successful", id: req.params.id }); +}); + // POPULATE VERSION (if details of mentor/mentee objects are needed on the frontend like name or picture) // import express from 'express'; diff --git a/api/src/server.ts b/api/src/server.ts index c3f6ace..2a4ecb9 100644 --- a/api/src/server.ts +++ b/api/src/server.ts @@ -1,11 +1,14 @@ import dotenv from "dotenv"; +import path from "path"; + dotenv.config({ path: path.resolve(__dirname, "../.env") }); import express from "express"; import bodyParser from "body-parser"; import connectDB from "./config/db"; +import { errorHandler } from "./controllers/auth0-errors"; +import { notFoundHandler } from "./controllers/auth0-notFound"; import * as routes from "./routes/index"; -import path from "path"; var cors = require("cors"); @@ -19,6 +22,9 @@ app.use("/workshop", routes.workshop); connectDB(); +app.use(notFoundHandler); +app.use(errorHandler); + app.listen(process.env.PORT || 8000, () => console.log(`Server running on port ${process.env.PORT || 8000}`), ); diff --git a/app/.env b/app/.env deleted file mode 100644 index 61fb955..0000000 --- a/app/.env +++ /dev/null @@ -1 +0,0 @@ -REACT_APP_API_URL = http://127.0.0.1:8000 \ No newline at end of file diff --git a/app/.env.example b/app/.env.example new file mode 100644 index 0000000..c53f8e6 --- /dev/null +++ b/app/.env.example @@ -0,0 +1,6 @@ +REACT_APP_API_URL = http://127.0.0.1:8000 + +# auth +REACT_APP_AUTH0_DOMAIN=##check notion## +REACT_APP_AUTH0_CLIENT_ID=##check notion## +REACT_APP_AUTH0_CALLBACK_URL=http://localhost:3000/callback diff --git a/app/package-lock.json b/app/package-lock.json index 4cb3bea..57f4d2c 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,6 +8,7 @@ "name": "my-app", "version": "0.1.0", "dependencies": { + "@auth0/auth0-react": "^2.2.4", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -27,6 +28,9 @@ "typescript": "^4.9.5", "web-vitals": "^2.1.4", "yup": "^1.4.0" + }, + "devDependencies": { + "prettier": "^3.3.3" } }, "node_modules/@adobe/css-tools": { @@ -57,6 +61,23 @@ "node": ">=6.0.0" } }, + "node_modules/@auth0/auth0-react": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@auth0/auth0-react/-/auth0-react-2.2.4.tgz", + "integrity": "sha512-l29PQC0WdgkCoOc6WeMAY26gsy/yXJICW0jHfj0nz8rZZphYKrLNqTRWFFCMJY+sagza9tSgB1kG/UvQYgGh9A==", + "dependencies": { + "@auth0/auth0-spa-js": "^2.1.3" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17 || ^18", + "react-dom": "^16.11.0 || ^17 || ^18" + } + }, + "node_modules/@auth0/auth0-spa-js": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@auth0/auth0-spa-js/-/auth0-spa-js-2.1.3.tgz", + "integrity": "sha512-NMTBNuuG4g3rame1aCnNS5qFYIzsTUV5qTFPRfTyYFS1feS6jsCBR+eTq9YkxCp1yuoM2UIcjunPaoPl77U9xQ==" + }, "node_modules/@babel/code-frame": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", @@ -14467,6 +14488,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -18406,6 +18442,19 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "@auth0/auth0-react": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@auth0/auth0-react/-/auth0-react-2.2.4.tgz", + "integrity": "sha512-l29PQC0WdgkCoOc6WeMAY26gsy/yXJICW0jHfj0nz8rZZphYKrLNqTRWFFCMJY+sagza9tSgB1kG/UvQYgGh9A==", + "requires": { + "@auth0/auth0-spa-js": "^2.1.3" + } + }, + "@auth0/auth0-spa-js": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@auth0/auth0-spa-js/-/auth0-spa-js-2.1.3.tgz", + "integrity": "sha512-NMTBNuuG4g3rame1aCnNS5qFYIzsTUV5qTFPRfTyYFS1feS6jsCBR+eTq9YkxCp1yuoM2UIcjunPaoPl77U9xQ==" + }, "@babel/code-frame": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", @@ -28412,6 +28461,12 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" }, + "prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true + }, "pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", diff --git a/app/package.json b/app/package.json index 1460a5e..73ea78c 100644 --- a/app/package.json +++ b/app/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@auth0/auth0-react": "^2.2.4", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -27,7 +28,9 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "format": "npx prettier --write \"src/**/*.{js,jsx,ts,tsx}\"", + "format-check": "npx prettier --check \"src/**/*.{js,jsx,ts,tsx}\"" }, "eslintConfig": { "extends": [ @@ -46,5 +49,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "prettier": "^3.3.3" } } diff --git a/app/src/App.tsx b/app/src/App.tsx index 55df1c2..61c5e31 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,25 +1,24 @@ -import React, { type ReactElement } from "react" -import { BrowserRouter as Router, Routes, Route } from "react-router-dom" -import Home from "./pages/Home" -import MentorDashboard from "./pages/MentorDashboard" -import MenteeDashboard from "./pages/MenteeDashboard" -import CreateWorkshop from "./pages/CreateWorkshop" -import CreateMeeting from "./pages/CreateMeeting" +import React, { type ReactElement } from "react"; +import { Routes, Route } from "react-router-dom"; +import Home from "./pages/Home"; +import MentorDashboard from "./pages/MentorDashboard"; +import CreateWorkshop from "./pages/CreateWorkshop"; +import CreateMeeting from "./pages/CreateMeeting"; +import AuthCallback from "./pages/auth-callback"; + function App(): ReactElement { return (
- - - } /> - } /> - } /> - } /> - } /> - } /> - - + + } /> + } /> + } /> + } /> + } /> + } /> +
); } -export default App; +export default App; \ No newline at end of file diff --git a/app/src/components/Navbar.tsx b/app/src/components/Navbar.tsx index d80398b..a39d1fc 100644 --- a/app/src/components/Navbar.tsx +++ b/app/src/components/Navbar.tsx @@ -1,8 +1,14 @@ import react, { type ReactElement } from "react"; import { useNavigate } from "react-router-dom"; +import { LoginButton } from "./mock-login-button"; +import { SignupButton } from "./mock-sign-up"; +import { LogoutButton } from "./mock-logout-button"; +import { useAuth0 } from "@auth0/auth0-react"; const Navbar = (): ReactElement => { const navigate = useNavigate(); + const { isAuthenticated } = useAuth0(); + return ( <>
@@ -54,6 +60,15 @@ const Navbar = (): ReactElement => { Create Meeting
+
+ {!isAuthenticated && ( + <> + + + + )} + {isAuthenticated && } +
diff --git a/app/src/components/mock-login-button.tsx b/app/src/components/mock-login-button.tsx new file mode 100644 index 0000000..9be1df9 --- /dev/null +++ b/app/src/components/mock-login-button.tsx @@ -0,0 +1,34 @@ +import { useAuth0 } from "@auth0/auth0-react"; +import React from "react"; + +export const LoginButton = () => { + const { loginWithRedirect } = useAuth0(); + + const handleLogin = async () => { + await loginWithRedirect({ + appState: { + returnTo: "/home", + }, + }); + }; + + return ( + + ); +}; diff --git a/app/src/components/mock-logout-button.tsx b/app/src/components/mock-logout-button.tsx new file mode 100644 index 0000000..37a5400 --- /dev/null +++ b/app/src/components/mock-logout-button.tsx @@ -0,0 +1,34 @@ +import { useAuth0 } from "@auth0/auth0-react"; +import React from "react"; + +export const LogoutButton = () => { + const { logout } = useAuth0(); + + const handleLogout = () => { + logout({ + logoutParams: { + returnTo: window.location.origin, + }, + }); + }; + + return ( + + ); +}; diff --git a/app/src/components/mock-sign-up.tsx b/app/src/components/mock-sign-up.tsx new file mode 100644 index 0000000..4fdd26e --- /dev/null +++ b/app/src/components/mock-sign-up.tsx @@ -0,0 +1,37 @@ +import { useAuth0 } from "@auth0/auth0-react"; +import React from "react"; + +export const SignupButton = () => { + const { loginWithRedirect } = useAuth0(); + + const handleSignUp = async () => { + await loginWithRedirect({ + appState: { + returnTo: "/home", + }, + authorizationParams: { + screen_hint: "signup", + }, + }); + }; + + return ( + + ); +}; diff --git a/app/src/index.tsx b/app/src/index.tsx index cf4e546..906ae28 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -3,14 +3,21 @@ import ReactDOM from "react-dom/client"; import "./styles/main.scss"; import App from "./App"; import reportWebVitals from "./reportWebVitals"; +import { Auth0ProviderWithNavigate } from "./utils/auth0-provider"; +import { BrowserRouter } from "react-router-dom"; +document.title = "PWW"; const root = ReactDOM.createRoot( - document.getElementById("root") as HTMLElement, + document.getElementById("root") as HTMLElement ); root.render( - - , + + + + + + ); // If you want to start measuring performance in your app, pass a function diff --git a/app/src/pages/auth-callback.tsx b/app/src/pages/auth-callback.tsx new file mode 100644 index 0000000..d3a4b6f --- /dev/null +++ b/app/src/pages/auth-callback.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import Navbar from "../components/Navbar"; + +const AuthCallback = () => { + return ; +}; + +export default AuthCallback; diff --git a/app/src/utils/auth0-provider.tsx b/app/src/utils/auth0-provider.tsx new file mode 100644 index 0000000..717efe7 --- /dev/null +++ b/app/src/utils/auth0-provider.tsx @@ -0,0 +1,36 @@ +import { Auth0Provider } from "@auth0/auth0-react"; +import React from "react"; +import { useNavigate } from "react-router-dom"; + +export const Auth0ProviderWithNavigate = ({ + children, +}: { + children: React.ReactNode; +}) => { + const navigate = useNavigate(); + + const domain = process.env.REACT_APP_AUTH0_DOMAIN; + const clientId = process.env.REACT_APP_AUTH0_CLIENT_ID; + const redirectUri = process.env.REACT_APP_AUTH0_CALLBACK_URL; + + const onRedirectCallback = (appState: any) => { + navigate(appState?.returnTo || window.location.pathname); + }; + + if (!(domain && clientId && redirectUri)) { + return null; + } + + return ( + + {children} + + ); +};