diff --git a/.env.sample b/.env.sample index 1ad2d9c..78d520c 100644 --- a/.env.sample +++ b/.env.sample @@ -1,2 +1,6 @@ -PORT=5005 -TOKEN_SECRET=1r0Nh4cK \ No newline at end of file +EXTERNAL_URL= +CLIENT_URL= +TOKEN_SECRET= +MONGODB_URI= +PORT= +DATABASE_URL= \ No newline at end of file diff --git a/app.js b/app.js index 5491ab7..0add4e9 100644 --- a/app.js +++ b/app.js @@ -10,6 +10,10 @@ require("./config")(app); // 👇 Start handling routes here + +const dashboardRouter = require("./routes/dashboard.routes") +app.use("/", dashboardRouter) + const allRoutes = require("./routes"); app.use("/api", allRoutes); @@ -19,8 +23,17 @@ app.use("/api", isAuthenticated, projectRouter); const taskRouter = require("./routes/task.routes"); app.use("/api", isAuthenticated, taskRouter); -const authRouter = require("./routes/auth.routes"); -app.use("/auth", authRouter); +const userAuthRouter = require("./routes/userAuth.routes"); +app.use("/auth", userAuthRouter); + +const therapistAuthRouter = require("./routes/therapistAuth.routes"); +app.use("/therapist", therapistAuthRouter); + +const therapistRouter = require("./routes/therapist.routes"); +app.use("/therapist", isAuthenticated, therapistRouter) + +const GPTRouter = require("./routes/gpt.routes"); +app.use("/ai-therapist", GPTRouter); require("./error-handling")(app); diff --git a/config/index.js b/config/index.js index e5bc383..517b76b 100644 --- a/config/index.js +++ b/config/index.js @@ -11,15 +11,20 @@ const cookieParser = require("cookie-parser"); const cors = require("cors"); + +const client = process.env.CLIENT_URL || "http://localhost:5173" + // Middleware configuration module.exports = (app) => { // Because this is a server that will accept requests from outside and it will be hosted ona server with a `proxy`, express needs to know that it should trust that setting. // Services like heroku use something called a proxy and you need to add this to your server app.set("trust proxy", 1); +// UPDATED FOR LIVE DEPLOYMENT + app.use( cors({ - origin: ["http://localhost:3000"], + origin: [client], }) ); diff --git a/db/index.js b/db/index.js index 7dcda6c..884877b 100644 --- a/db/index.js +++ b/db/index.js @@ -5,7 +5,7 @@ const mongoose = require("mongoose"); // â„šī¸ Sets the MongoDB URI for our app to have access to it. // If no env has been set, we dynamically set it to whatever the folder name was upon the creation of the app -const MONGO_URI = process.env.MONGODB_URI || "mongodb://127.0.0.1:27017/project-management-server"; +const MONGO_URI = process.env.DATABASE_URL ||process.env.MONGODB_URI || "mongodb://127.0.0.1:27017/bookworm-local-test-db"; mongoose .connect(MONGO_URI) diff --git a/error-handling/index.js b/error-handling/index.js index 4f58d57..a5c9b93 100644 --- a/error-handling/index.js +++ b/error-handling/index.js @@ -1,7 +1,7 @@ module.exports = (app) => { app.use((req, res, next) => { // this middleware runs whenever requested page is not available - res.status(404).json({ errorMessage: "This route does not exist" }); + res.status(404).json({ errorMessage: "This route does not exist, will never exist, or is yet to be created!" }); }); app.use((err, req, res, next) => { diff --git a/models/Therapist.model.js b/models/Therapist.model.js new file mode 100644 index 0000000..8d19869 --- /dev/null +++ b/models/Therapist.model.js @@ -0,0 +1,17 @@ +const mongoose = require("mongoose"); +const { Schema, model } = mongoose; + +const therapistSchema = new Schema({ + email: { type: String, unique: true, required: true }, + password: { type: String, required: true }, + name: { type: String, required: true }, + location: { type: String, required: true }, + price: { type: Number, required: true }, + languages: [{ type: String, required: true }], + approach: [{ type: String, required: true }], + specialization: [{ type: String, required: true }], + availability: [{ type: String, required: true }], + appointments: [{ type: Schema.Types.ObjectId, ref: "Appointment" }] +}); + +module.exports = model("Therapist", therapistSchema); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5d98045..b859ef4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "cors": "^2.8.5", "dotenv": "^10.0.0", "express": "^4.17.1", - "express-jwt": "^6.0.0", + "express-jwt": "^7.0.0", "jsonwebtoken": "^8.5.1", "mongoose": "^6.1.2", "morgan": "^1.10.0" @@ -22,6 +22,14 @@ "nodemon": "^2.0.12" } }, + "node_modules/@types/jsonwebtoken": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz", + "integrity": "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "18.7.23", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz", @@ -77,11 +85,6 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, - "node_modules/async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -416,23 +419,22 @@ } }, "node_modules/express-jwt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-6.0.0.tgz", - "integrity": "sha512-C26y9myRjx7CyhZ+BAT3p+gQyRCoDZ7qo8plCvLDaRT6je6ALIAQknT6XLVQGFKwIy/Ux7lvM2MNap5dt0T7gA==", + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-7.7.7.tgz", + "integrity": "sha512-7s04HZ7sAahx7lwKU5AInhVon1kWVitRFxd7cGUKQaLDOQJsAQjB/OEBXaN0GS22RBgX75TjOJXGY7myQmVz5A==", "dependencies": { - "async": "^1.5.0", - "express-unless": "^0.3.0", - "jsonwebtoken": "^8.1.0", - "lodash.set": "^4.0.0" + "@types/jsonwebtoken": "^8.5.8", + "express-unless": "^2.1.3", + "jsonwebtoken": "^8.5.1" }, "engines": { "node": ">= 8.0.0" } }, "node_modules/express-unless": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-0.3.1.tgz", - "integrity": "sha1-JVfBRudb65A+LSR/m1ugFFJpbiA=" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", + "integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==" }, "node_modules/fill-range": { "version": "7.0.1", @@ -710,11 +712,6 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, - "node_modules/lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" - }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1360,6 +1357,14 @@ } }, "dependencies": { + "@types/jsonwebtoken": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz", + "integrity": "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==", + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "18.7.23", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz", @@ -1409,11 +1414,6 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1664,20 +1664,19 @@ } }, "express-jwt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-6.0.0.tgz", - "integrity": "sha512-C26y9myRjx7CyhZ+BAT3p+gQyRCoDZ7qo8plCvLDaRT6je6ALIAQknT6XLVQGFKwIy/Ux7lvM2MNap5dt0T7gA==", + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-7.7.7.tgz", + "integrity": "sha512-7s04HZ7sAahx7lwKU5AInhVon1kWVitRFxd7cGUKQaLDOQJsAQjB/OEBXaN0GS22RBgX75TjOJXGY7myQmVz5A==", "requires": { - "async": "^1.5.0", - "express-unless": "^0.3.0", - "jsonwebtoken": "^8.1.0", - "lodash.set": "^4.0.0" + "@types/jsonwebtoken": "^8.5.8", + "express-unless": "^2.1.3", + "jsonwebtoken": "^8.5.1" } }, "express-unless": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-0.3.1.tgz", - "integrity": "sha1-JVfBRudb65A+LSR/m1ugFFJpbiA=" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", + "integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==" }, "fill-range": { "version": "7.0.1", @@ -1893,11 +1892,6 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, - "lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" - }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", diff --git a/routes/dashboard.routes.js b/routes/dashboard.routes.js new file mode 100644 index 0000000..b7cac93 --- /dev/null +++ b/routes/dashboard.routes.js @@ -0,0 +1,7 @@ +const router = require("express").Router(); + +router.get("/", (req, res, next) => { + res.json("Welcome to the Dashboard, all route calls are based off api-address/api/*. In the future we will implement a UI interface for our backend - an admin dashboard. - Stephen, Andy, Devin"); +}); + +module.exports = router; diff --git a/routes/gpt.routes.js b/routes/gpt.routes.js new file mode 100644 index 0000000..d90a226 --- /dev/null +++ b/routes/gpt.routes.js @@ -0,0 +1,41 @@ +const express = require("express"); +const router = express.Router(); +const mongoose = require("mongoose"); +const dotenv = require("dotenv"); +dotenv.config(); + +const chatAPIKey = process.env.OPENAI_API_KEY; + +router.post("/completions", async (req, res, next) => { + const options = { + method: "POST", + headers: { + Authorization: `Bearer ${chatAPIKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: "gpt-3.5-turbo", + messages: [ + { + role: "system", + content: + "You are an empathetic companion on an emotional wellbeing app. You offer mental health tips and advice to users based on their prompts. Do not assume the role of a professional and remind them of that.", + }, + { role: "user", content: req.body.message }], + max_tokens: 500, + }), + }; + try { + const response = await fetch( + "https://api.openai.com/v1/chat/completions", + options + ); + const data = await response.json(); + res.send(data); + } catch (error) { + console.log(error); + } +}); + + +module.exports = router; \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index 9a06811..22e32e2 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,7 +1,7 @@ const router = require("express").Router(); router.get("/", (req, res, next) => { - res.json("All good in here"); + res.json("You are on the API Route"); }); module.exports = router; diff --git a/routes/therapist.routes.js b/routes/therapist.routes.js new file mode 100644 index 0000000..31b3383 --- /dev/null +++ b/routes/therapist.routes.js @@ -0,0 +1,98 @@ +const express = require("express"); +const router = express.Router(); +const mongoose = require("mongoose"); +const jwt = require("jsonwebtoken"); + +const Therapist = require("../models/Therapist.model"); + +const secretKey = process.env.TOKEN_SECRET; + +// GET /api/therapist - Retrieves a therapist +function authenticateToken(req, res, next) { + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; + + if (!token) { + return res.sendStatus(401); + } + + jwt.verify(token, secretKey, (err, user) => { + if (err) { + return res.sendStatus(403); + } + req.user = user; // Store the user information in the request object for further use if needed. + next(); + }); +} + +// Route to get therapist information based on the authenticated request. +router.get("/therapistInfo", authenticateToken, (req, res, next) => { + // Assuming the user information is stored in req.user. + const therapistId = req.user._id; + + Therapist.findById(therapistId) + .then((therapist) => { + if (!therapist) { + return res.status(404).json({ error: "Therapist not found" }); + } + res.json(therapist); + }) + .catch((err) => res.status(500).json({ error: "Server error" })); +}); + + +//PUT update therapist profile details +router.put("/updateProfile", authenticateToken, async (req, res, next) => { + try { + const { email, name, location, price, languages, availability, approach } = req.body; + + const therapistId = req.user._id; + console.log(req.body); + + if (!mongoose.Types.ObjectId.isValid(therapistId)) { + res.status(400).json({ message: "Specified id is not valid" }); + return; + } + + // Find the therapist by ID and update the information. + const updatedTherapist = await Therapist.findByIdAndUpdate( + therapistId, + { + email, + name, + location, + price, + languages, + availability, + approach, + }, + { new: true } // Return the updated document. + ); + + res.json(updatedTherapist); + console.log(updatedTherapist) + } catch (error) { + console.error("Error updating therapist:", error); + res.status(500).json({ error: "Server error" }); + } +}); + +// DELETE /api/projects/:projectId - Deletes a specific project by id +router.delete("/deleteTherapist", authenticateToken, (req, res, next) => { + const therapistId = req.user._id; + + if (!mongoose.Types.ObjectId.isValid(therapistId)) { + res.status(400).json({ message: "Specified id is not valid" }); + return; + } + + Therapist.findByIdAndRemove(therapistId) + .then(() => + res.json({ + message: `Therapist with ${therapistId} is removed successfully.`, + }) + ) + .catch((error) => res.json(error)); +}); + +module.exports = router; diff --git a/routes/therapistAuth.routes.js b/routes/therapistAuth.routes.js new file mode 100644 index 0000000..8cddcd1 --- /dev/null +++ b/routes/therapistAuth.routes.js @@ -0,0 +1,139 @@ +const express = require("express"); +const bcrypt = require('bcryptjs'); +const jwt = require("jsonwebtoken"); +const Therapist = require("../models/Therapist.model"); + +const { isAuthenticated } = require('../middleware/jwt.middleware.js'); + +const router = express.Router(); +const saltRounds = 10; + +// POST /auth/therapist-signup - Creates a new therapist in the database + router.post('/signup', (req, res, next) => { + const { email, password, name, location, price, languages, availability, approach } = req.body; +console.log("hello", req.body); + // Check if email or password or name are provided as empty string + if (email === '' || password === '' || name === '' || location === '' || price === '' || languages === '' || availability === '' || approach === '') { + res.status(400).json({ message: "Please fill out all required fields" }); + return; + } + + // Use regex to validate the email format + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; + if (!emailRegex.test(email)) { + res.status(400).json({ message: 'Provide a valid email address.' }); + return; + } + + // Use regex to validate the password format + const passwordRegex = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,}/; + if (!passwordRegex.test(password)) { + res.status(400).json({ message: 'Password must have at least 6 characters and contain at least one number, one lowercase and one uppercase letter.' }); + return; + } + + // Check the therapist's collection if a therapist with the same email already exists + Therapist.findOne({ email }) + .then((foundTherapist) => { + // If the therapist with the same email already exists, send an error response + if (foundTherapist) { + res.status(400).json({ message: "This account already exists." }); + return; + } + + // If email is unique, proceed to hash the password + const salt = bcrypt.genSaltSync(saltRounds); + const hashedPassword = bcrypt.hashSync(password, salt); + + // Create the new therapist in the database + // We return a pending promise, which allows us to chain another `then` + return Therapist.create({ email, password: hashedPassword, name, location, price, languages, availability, approach }); + }) + .then((createdTherapist) => { + // Deconstruct the newly created therapist object to omit the password + // We should never expose passwords publicly + const { email, name, location, price, languages, availability, approach, _id } = createdTherapist; + + // Create a new object that doesn't expose the password + const payload = { email, name, location, price, languages, availability, approach, _id }; + + // Create and sign the token + const authToken = jwt.sign( + payload, + process.env.TOKEN_SECRET, + { algorithm: 'HS256', expiresIn: "6h" } + ); + + // Send a json response containing the therapist object + res.status(201).json({ authToken: authToken }); + }) + .catch(err => { + console.log(err); + res.status(500).json({ message: "Internal Server Error" }) + }); +}); + + +// POST /auth/login - Verifies email and password and returns a JWT +router.post('/login', (req, res, next) => { + const { email, password } = req.body; + + // Check if email or password are provided as empty string + if (email === '' || password === '') { + res.status(400).json({ message: "Provide email and password." }); + return; + } + + // Check the therapist's collection if a therapist with the same email exists + Therapist.findOne({ email }) + .then((foundTherapist) => { + + if (!foundTherapist) { + // If the therapist is not found, send an error response + res.status(401).json({ message: "Account not found." }) + return; + } + + // Compare the provided password with the one saved in the database + const passwordCorrect = bcrypt.compareSync(password, foundTherapist.password); + + if (passwordCorrect) { + // Deconstruct the therapist object to omit the password + const { email, name, location, price, languages, availability, approach, _id } = foundTherapist; + + // Create an object that will be set as the token payload + const payload = { email, name, location, price, languages, availability, approach, _id }; + + // Create and sign the token + const authToken = jwt.sign( + payload, + process.env.TOKEN_SECRET, + { algorithm: 'HS256', expiresIn: "6h" } + ); + + // Send the token as the response + res.status(200).json({ authToken: authToken }); + } + else { + res.status(401).json({ message: "Unable to authenticate account" }); + } + + }) + .catch(err => res.status(500).json({ message: "Internal Server Error" })); +}); + + +// GET /auth/verify - Used to verify JWT stored on the client +router.get('/verify', isAuthenticated, (req, res, next) => { + + // If JWT token is valid the payload gets decoded by the + // isAuthenticated middleware and made available on `req.payload` + console.log(`req.payload`, req.payload); + + // Send back the object with therapist data + // previously set as the token payload + res.status(200).json(req.payload); +}); + + +module.exports = router; \ No newline at end of file diff --git a/routes/auth.routes.js b/routes/userAuth.routes.js similarity index 90% rename from routes/auth.routes.js rename to routes/userAuth.routes.js index 5aaaa2c..d9ce0f3 100644 --- a/routes/auth.routes.js +++ b/routes/userAuth.routes.js @@ -3,7 +3,7 @@ const bcrypt = require('bcryptjs'); const jwt = require("jsonwebtoken"); const User = require("../models/User.model"); -const { isAuthenticated } = require('./../middleware/jwt.middleware.js'); +const { isAuthenticated } = require('../middleware/jwt.middleware.js'); const router = express.Router(); const saltRounds = 10; @@ -33,13 +33,13 @@ router.post('/signup', (req, res, next) => { return; } - + // Check the users collection if a user with the same email already exists User.findOne({ email }) .then((foundUser) => { // If the user with the same email already exists, send an error response if (foundUser) { - res.status(400).json({ message: "User already exists." }); + res.status(400).json({ message: "This user already has an account." }); return; } @@ -57,10 +57,17 @@ router.post('/signup', (req, res, next) => { const { email, name, _id } = createdUser; // Create a new object that doesn't expose the password - const user = { email, name, _id }; + const payload = { email, name, _id }; + + // Create and sign the token + const authToken = jwt.sign( + payload, + process.env.TOKEN_SECRET, + { algorithm: 'HS256', expiresIn: "6h" } + ); // Send a json response containing the user object - res.status(201).json({ user: user }); + res.status(201).json({ authToken: authToken }); }) .catch(err => { console.log(err);