From efc889274cdd9495cc10a1291758344d7364137e Mon Sep 17 00:00:00 2001 From: Nick Beaird Date: Tue, 1 Sep 2020 00:58:54 -0700 Subject: [PATCH 01/10] Update tests for organization --- backend/{test/unit => models}/answer.test.js | 8 +-- backend/{test/unit => models}/checkin.test.js | 8 +-- backend/{test/unit => models}/event.test.js | 8 +-- backend/{test/unit => models}/project.test.js | 8 +-- .../unit => models}/projectTeamMemer.test.js | 8 +-- .../{test/unit => models}/question.test.js | 8 +-- .../unit => models}/recurringEvent.test.js | 8 +-- backend/{test/unit => models}/user.test.js | 8 +-- .../events.router.test.js} | 13 ++-- backend/setup-test.js | 65 +++++++++++++++++++ .../test/api-integration-test-example.test.js | 51 --------------- backend/test/db-handler.js | 48 -------------- backend/test/jest-test-runner.test.js | 6 -- backend/test/model-unit-test-example.test.js | 46 ------------- 14 files changed, 94 insertions(+), 199 deletions(-) rename backend/{test/unit => models}/answer.test.js (76%) rename backend/{test/unit => models}/checkin.test.js (73%) rename backend/{test/unit => models}/event.test.js (84%) rename backend/{test/unit => models}/project.test.js (81%) rename backend/{test/unit => models}/projectTeamMemer.test.js (84%) rename backend/{test/unit => models}/question.test.js (79%) rename backend/{test/unit => models}/recurringEvent.test.js (84%) rename backend/{test/unit => models}/user.test.js (82%) rename backend/{test/int/api.events.test.js => routers/events.router.test.js} (74%) create mode 100644 backend/setup-test.js delete mode 100644 backend/test/api-integration-test-example.test.js delete mode 100644 backend/test/db-handler.js delete mode 100644 backend/test/jest-test-runner.test.js delete mode 100644 backend/test/model-unit-test-example.test.js diff --git a/backend/test/unit/answer.test.js b/backend/models/answer.test.js similarity index 76% rename from backend/test/unit/answer.test.js rename to backend/models/answer.test.js index 2205e387d..7e30d46ad 100644 --- a/backend/test/unit/answer.test.js +++ b/backend/models/answer.test.js @@ -1,9 +1,7 @@ -const Answer = require("../../models/answer.model"); -const dbHandler = require("../db-handler"); +const Answer = require("./answer.model"); -// Required database setup and teardown -beforeAll(async () => await dbHandler.connect()); -afterAll(async () => await dbHandler.closeDatabase()); +const { setupDB } = require("../setup-test"); +setupDB("answer-model"); describe("Answer Model saves the correct values", () => { const submittedAnswer = { diff --git a/backend/test/unit/checkin.test.js b/backend/models/checkin.test.js similarity index 73% rename from backend/test/unit/checkin.test.js rename to backend/models/checkin.test.js index bf9df0952..2c1537873 100644 --- a/backend/test/unit/checkin.test.js +++ b/backend/models/checkin.test.js @@ -1,9 +1,7 @@ -const Checkin = require("../../models/answer.model"); -const dbHandler = require("../db-handler"); +const Checkin = require("./checkIn.model"); -// Required database setup and teardown -beforeAll(async () => await dbHandler.connect()); -afterAll(async () => await dbHandler.closeDatabase()); +const { setupDB } = require("../setup-test"); +setupDB("checkin-model"); describe("Checkin Model saves the correct values", () => { test("Save a model instance and then read from the db", async (done) => { diff --git a/backend/test/unit/event.test.js b/backend/models/event.test.js similarity index 84% rename from backend/test/unit/event.test.js rename to backend/models/event.test.js index ba5ded1dc..605570a9b 100644 --- a/backend/test/unit/event.test.js +++ b/backend/models/event.test.js @@ -1,9 +1,7 @@ -const Event = require("../../models/event.model"); -const dbHandler = require("../db-handler"); +const Event = require("./event.model"); -// Required database setup and teardown -beforeAll(async () => await dbHandler.connect()); -afterAll(async () => await dbHandler.closeDatabase()); +const { setupDB } = require("../setup-test"); +setupDB("event-model"); describe("Event Model saves the correct values", () => { test("Save a model instance and then read from the db", async (done) => { diff --git a/backend/test/unit/project.test.js b/backend/models/project.test.js similarity index 81% rename from backend/test/unit/project.test.js rename to backend/models/project.test.js index 53bd7f837..d90744d8f 100644 --- a/backend/test/unit/project.test.js +++ b/backend/models/project.test.js @@ -1,9 +1,7 @@ -const Project = require("../../models/project.model"); -const dbHandler = require("../db-handler"); +const Project = require("./project.model"); -// Required database setup and teardown -beforeAll(async () => await dbHandler.connect()); -afterAll(async () => await dbHandler.closeDatabase()); +const { setupDB } = require("../setup-test"); +setupDB("project-model"); describe("Project Model saves the correct values", () => { test("Save a model instance and then read from the db", async (done) => { diff --git a/backend/test/unit/projectTeamMemer.test.js b/backend/models/projectTeamMemer.test.js similarity index 84% rename from backend/test/unit/projectTeamMemer.test.js rename to backend/models/projectTeamMemer.test.js index 722b930c7..f33c5bd05 100644 --- a/backend/test/unit/projectTeamMemer.test.js +++ b/backend/models/projectTeamMemer.test.js @@ -1,9 +1,7 @@ -const ProjectTeamMember = require("../../models/project.model"); -const dbHandler = require("../db-handler"); +const ProjectTeamMember = require("./projectTeamMember.model"); -// Required database setup and teardown -beforeAll(async () => await dbHandler.connect()); -afterAll(async () => await dbHandler.closeDatabase()); +const { setupDB } = require("../setup-test"); +setupDB("projectTeamMember-model"); // Please add and expand on this simple test. describe("ProjectTeamMember Model saves the correct values", () => { diff --git a/backend/test/unit/question.test.js b/backend/models/question.test.js similarity index 79% rename from backend/test/unit/question.test.js rename to backend/models/question.test.js index 4d567327b..5143bd92f 100644 --- a/backend/test/unit/question.test.js +++ b/backend/models/question.test.js @@ -1,9 +1,7 @@ -const Question = require("../../models/question.model"); -const dbHandler = require("../db-handler"); +const Question = require("./question.model"); -// Required database setup and teardown -beforeAll(async () => await dbHandler.connect()); -afterAll(async () => await dbHandler.closeDatabase()); +const { setupDB } = require("../setup-test"); +setupDB("question-model"); // Please add and expand on this simple test. describe("Question Model saves the correct values", () => { diff --git a/backend/test/unit/recurringEvent.test.js b/backend/models/recurringEvent.test.js similarity index 84% rename from backend/test/unit/recurringEvent.test.js rename to backend/models/recurringEvent.test.js index e619d0d5c..1af469471 100644 --- a/backend/test/unit/recurringEvent.test.js +++ b/backend/models/recurringEvent.test.js @@ -1,9 +1,7 @@ -const RecurringEvent = require("../../models/recurringEvent.model"); -const dbHandler = require("../db-handler"); +const RecurringEvent = require("./recurringEvent.model"); -// Required database setup and teardown -beforeAll(async () => await dbHandler.connect()); -afterAll(async () => await dbHandler.closeDatabase()); +const { setupDB } = require("../setup-test"); +setupDB("recurringEvent-model"); // Please add and expand on this simple test. describe("Question Model saves the correct values", () => { diff --git a/backend/test/unit/user.test.js b/backend/models/user.test.js similarity index 82% rename from backend/test/unit/user.test.js rename to backend/models/user.test.js index 20393e1e2..6978e671e 100644 --- a/backend/test/unit/user.test.js +++ b/backend/models/user.test.js @@ -1,9 +1,7 @@ -const User = require("../../models/user.model"); -const dbHandler = require("../db-handler"); +const User = require("./user.model"); -// Required database setup and teardown -beforeAll(async () => await dbHandler.connect()); -afterAll(async () => await dbHandler.closeDatabase()); +const { setupDB } = require("../setup-test"); +setupDB("user-model"); // Please add and expand on this simple test. describe("Question Model saves the correct values", () => { diff --git a/backend/test/int/api.events.test.js b/backend/routers/events.router.test.js similarity index 74% rename from backend/test/int/api.events.test.js rename to backend/routers/events.router.test.js index a2a2069a4..4a07bbfe8 100644 --- a/backend/test/int/api.events.test.js +++ b/backend/routers/events.router.test.js @@ -1,15 +1,12 @@ const supertest = require("supertest"); -const app = require("../../app"); +const app = require("../app"); const request = supertest(app); -const dbHandler = require("../db-handler"); +const { setupDB } = require("../setup-test"); +setupDB("api-events"); -const Event = require("../../models/event.model.js"); -const Project = require("../../models/project.model.js"); - -beforeAll(async () => await dbHandler.connect()); -// afterEach(async () => await Event.remove({})); -afterAll(async () => await dbHandler.closeDatabase()); +const db = require("../models"); +const Event = db.event; // API Tests describe("Test add data with POST and then retrieve the data with GET", () => { diff --git a/backend/setup-test.js b/backend/setup-test.js new file mode 100644 index 000000000..39742132f --- /dev/null +++ b/backend/setup-test.js @@ -0,0 +1,65 @@ +// test-setup.js +const mongoose = require("mongoose"); +mongoose.set("useCreateIndex", true); +mongoose.promise = global.Promise; + +const { MongoMemoryServer } = require("mongodb-memory-server"); + +async function removeAllCollections() { + const collections = Object.keys(mongoose.connection.collections); + for (const collectionName of collections) { + const collection = mongoose.connection.collections[collectionName]; + await collection.deleteMany(); + } +} + +async function dropAllCollections() { + const collections = Object.keys(mongoose.connection.collections); + for (const collectionName of collections) { + const collection = mongoose.connection.collections[collectionName]; + try { + await collection.drop(); + } catch (error) { + // Sometimes this error happens, but you can safely ignore it + if (error.message === "ns not found") return; + // This error occurs when you use it.todo. You can + // safely ignore this error too + if (error.message.includes("a background operation is currently running")) + return; + console.log(error.message); + } + } +} +let mongoServer; +module.exports = { + setupDB(databaseName) { + // Connect to Mongoose + beforeAll(async () => { + mongoServer = new MongoMemoryServer({ + instance: { dbName: databaseName }, + }); + const mongoUri = await mongoServer.getUri(); + const opts = { + useNewUrlParser: true, + useFindAndModify: false, + useCreateIndex: true, + useUnifiedTopology: true, + }; + await mongoose.connect(mongoUri, opts, (err) => { + if (err) console.error(err); + }); + }); + + // Cleans up database between each test + afterEach(async () => { + await removeAllCollections(); + }); + + // Disconnect Mongoose + afterAll(async () => { + await dropAllCollections(); + await mongoose.connection.close(); + await mongoServer.stop(); + }); + }, +}; diff --git a/backend/test/api-integration-test-example.test.js b/backend/test/api-integration-test-example.test.js deleted file mode 100644 index 2e6d4727f..000000000 --- a/backend/test/api-integration-test-example.test.js +++ /dev/null @@ -1,51 +0,0 @@ -// API Test Example file - This is for instructional purposes on writing an API integration test. - -const supertest = require("supertest"); -const app = require("../app"); -const request = supertest(app); -const dbHandler = require("./db-handler"); - -const Event = require("../models/event.model.js"); - -/** - * Connect to a new in-memory database before running any tests. - */ -beforeAll(async () => await dbHandler.connect()); - -// Uncomment out the line below to clear out the DB of Users between tests. -// afterEach(async () => await Event.remove({})); - -/** - * Remove and close the db and server. - */ -afterAll(async () => await dbHandler.closeDatabase()); - -// API Tests -describe("Test GET with no data in database", () => { - test("GET events", async (done) => { - const response = await request.get("/api/events"); - expect(response.statusCode).toBe(200); - done(); - }); -}); - -describe("Test add data with POST and then retrieve the data with GET", () => { - test("POST and then GET", async (done) => { - const res = await request.post("/api/events").send({ - createdDate: "2020-05-20T21:16:44.498Z", - checkinReady: true, - }); - const response = await request.get("/api/events"); - expect(response.statusCode).toBe(200); - const event_json = response.body[0]; - expect(event_json.createdDate === "2020-05-20T21:16:44.498Z"); - - done(); - }); - - test("Data can be verified from a database query", async (done) => { - const databaseEventQuery = await Event.find(); - expect(databaseEventQuery[0].length >= 1); - done(); - }); -}); diff --git a/backend/test/db-handler.js b/backend/test/db-handler.js deleted file mode 100644 index ce73dfab2..000000000 --- a/backend/test/db-handler.js +++ /dev/null @@ -1,48 +0,0 @@ -/* tests/db-handler.js - The setup and teardown methods for the test instance of MongoDB. -This file should be imported and used in all backend unit, integration, and other tests. - -The reason that the backend unit tests are in a single directory is largely to make -development easier to import this file. -*/ - -const mongoose = require("mongoose"); -const { MongoMemoryServer } = require("mongodb-memory-server"); - -mongoServer = new MongoMemoryServer(); - -/** - * Connect to the in-memory database. - */ -module.exports.connect = async () => { - const mongoUri = await mongoServer.getUri(); - const opts = { - useNewUrlParser: true, - useFindAndModify: false, - useCreateIndex: true, - useUnifiedTopology: true, - }; - await mongoose.connect(mongoUri, opts, (err) => { - if (err) console.error(err); - }); -}; - -/** - * Drop database, close the connection and stop mongod. - */ -module.exports.closeDatabase = async () => { - await mongoose.connection.dropDatabase(); - await mongoose.connection.close(); - await mongoServer.stop(); -}; - -/** - * Remove all the data for all db collections. - */ -module.exports.clearDatabase = async () => { - const collections = mongoose.connection.collections; - - for (const key in collections) { - const collection = collections[key]; - await collection.deleteMany(); - } -}; diff --git a/backend/test/jest-test-runner.test.js b/backend/test/jest-test-runner.test.js deleted file mode 100644 index 7f165c889..000000000 --- a/backend/test/jest-test-runner.test.js +++ /dev/null @@ -1,6 +0,0 @@ -// Jest Test Runner Test - This file shows that we can test our test runner. - -test("The testing environment is working", () => { - const test_env_var = process.env.NODE_ENV; - expect(test_env_var).toBe("test"); -}); diff --git a/backend/test/model-unit-test-example.test.js b/backend/test/model-unit-test-example.test.js deleted file mode 100644 index 1f3a3bc61..000000000 --- a/backend/test/model-unit-test-example.test.js +++ /dev/null @@ -1,46 +0,0 @@ -// Unit Test Example - This is for instructional purposes on writing a backend unit test. - -const mongoose = require("mongoose"); -const dbHandler = require("./db-handler"); - -/** - * Connect to a new in-memory database before running any tests. - */ -beforeAll(async () => await dbHandler.connect()); - -/** - * Remove and close the db and server. - */ -afterAll(async () => await dbHandler.closeDatabase()); - -// Test data -const Checkin = require("../models/checkIn.model.js"); -const answerData = { - userId: "1", - eventId: "23", - checkedIn: true, - createdDate: 1594023390039, -}; - -describe("Verify that our unit tests have a working database", () => { - test("Save a model instance and then read from the db", async (done) => { - await Checkin.create(answerData); - const savedCheckinTime = await Checkin.find(); - expect(savedCheckinTime[0].checkedIn === answerData.checkedIn); - done(); - }); - - test("Data added from a previous test persists in the db", async (done) => { - const test = await Checkin.find(); - done(); - }); - - it("Test writers can create a model within the tests", async () => { - const User = mongoose.model("User", new mongoose.Schema({ name: String })); - const count = await User.countDocuments(); - expect(count).toEqual(0); - - const userFromDatabase = await User.find(); - expect(userFromDatabase.length === 1); - }); -}); From a176062e66bcac44b9e19f23bcaeb986c0ffbe44 Mon Sep 17 00:00:00 2001 From: Nick Beaird Date: Wed, 2 Sep 2020 16:25:59 -0700 Subject: [PATCH 02/10] Create a user test --- backend/config/auth.config.js | 4 +++ backend/routers/auth.router.test.js | 46 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 backend/config/auth.config.js create mode 100644 backend/routers/auth.router.test.js diff --git a/backend/config/auth.config.js b/backend/config/auth.config.js new file mode 100644 index 000000000..067043f2b --- /dev/null +++ b/backend/config/auth.config.js @@ -0,0 +1,4 @@ +module.exports = { + secret: "vrms-secret-key", + custom_request_header: process.env.CUSTOM_REQUEST_HEADER, +}; diff --git a/backend/routers/auth.router.test.js b/backend/routers/auth.router.test.js new file mode 100644 index 000000000..0743cb72b --- /dev/null +++ b/backend/routers/auth.router.test.js @@ -0,0 +1,46 @@ +const supertest = require("supertest"); +const app = require("../app"); +const request = supertest(app); + +const { setupDB } = require("../setup-test"); +setupDB("api-auth"); + +// const authRouter = require("./auth.router"); + +const CONFIG = require("../config/auth.config"); + +const db = require("../models"); +const User = db.user; + +// API Tests +describe("Test that we can create a user using Auth", () => { + test("Submit a user and retrieve that user", async () => { + // Test Data + const submittedData = { + name: { firstName: "test_first", lastName: "test_last" }, + email: "test@test.com", + }; + let headers = {}; + headers["x-customrequired-header"] = CONFIG.custom_request_header; + + // Add an event with a project using the API. + const res = await request + .post("/api/users") + .send(submittedData) + .set(headers); + + expect(res.status).toBe(201); + + // Retrieve and compare the the Event values using the DB. + const databaseEventQuery = await User.find(); + const databaseEvent = databaseEventQuery[0]; + expect(databaseEvent.length >= 1); + expect(databaseEvent.name === submittedData.name); + + // Retrieve and compare the the values using the API. + const response = await request.get("/api/users").set(headers); + expect(response.statusCode).toBe(200); + const APIData = response.body[0]; + expect(APIData.name === submittedData.name); + }); +}); From 1036c4596ad794f1910cfe181f56c160dda3d4fc Mon Sep 17 00:00:00 2001 From: Nick Beaird Date: Wed, 2 Sep 2020 16:30:04 -0700 Subject: [PATCH 03/10] Make ecnryption key more secure --- backend/config/auth.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/config/auth.config.js b/backend/config/auth.config.js index 067043f2b..672156f4a 100644 --- a/backend/config/auth.config.js +++ b/backend/config/auth.config.js @@ -1,4 +1,5 @@ module.exports = { - secret: "vrms-secret-key", + secret: + "c0d7d0716e4cecffe9dcc77ff90476d98f5aace08ea40f5516bd982b06401021191f0f24cd6759f7d8ca41b64f68d0b3ad19417453bddfd1dbe8fcb197245079", custom_request_header: process.env.CUSTOM_REQUEST_HEADER, }; From 389b38ee2ac87b79d7dbb6d2129a3e706e5eeee7 Mon Sep 17 00:00:00 2001 From: Nick Beaird Date: Thu, 3 Sep 2020 03:10:08 -0700 Subject: [PATCH 04/10] Get a working authentication verification for user --- backend/app.js | 2 + backend/controllers/user.controller.js | 119 ++++++++++++++++++++++ backend/middleware/index.js | 7 ++ backend/middleware/verifySignup.js | 38 ++++++++ backend/models/index.js | 20 ++++ backend/models/role.model.js | 10 ++ backend/models/user.model.js | 130 +++++++++++++------------ backend/package.json | 5 +- backend/routers/auth.router.js | 27 +++++ backend/routers/auth.router.test.js | 111 ++++++++++++++++++++- backend/server.js | 40 ++++++++ backend/yarn.lock | 13 +++ 12 files changed, 455 insertions(+), 67 deletions(-) create mode 100644 backend/controllers/user.controller.js create mode 100644 backend/middleware/index.js create mode 100644 backend/middleware/verifySignup.js create mode 100644 backend/models/index.js create mode 100644 backend/models/role.model.js create mode 100644 backend/routers/auth.router.js diff --git a/backend/app.js b/backend/app.js index a31b9131e..760b47fdf 100644 --- a/backend/app.js +++ b/backend/app.js @@ -44,6 +44,7 @@ const projectsRouter = require("./routers/projects.router"); const recurringEventsRouter = require("./routers/recurringEvents.router"); const projectTeamMembersRouter = require("./routers/projectTeamMembers.router"); const slackRouter = require("./routers/slack.router"); +const authRouter = require("./routers/auth.router"); app.use("/api/events", eventsRouter); app.use("/api/checkins", checkInsRouter); @@ -56,6 +57,7 @@ app.use("/api/projects", projectsRouter); app.use("/api/recurringevents", recurringEventsRouter); app.use("/api/projectteammembers", projectTeamMembersRouter); app.use("/api/slack", slackRouter); +app.use("/api/auth", authRouter); const CLIENT_BUILD_PATH = path.join(__dirname, "../client/build"); // Serve static files from the React frontend app diff --git a/backend/controllers/user.controller.js b/backend/controllers/user.controller.js new file mode 100644 index 000000000..e5f11fe25 --- /dev/null +++ b/backend/controllers/user.controller.js @@ -0,0 +1,119 @@ +const config = require("../config/auth.config"); +const db = require("../models"); +const User = db.user; +const Role = db.role; + +var jwt = require("jsonwebtoken"); + +const { body, validationResult } = require("express-validator"); + +exports.validate = (method) => { + switch (method) { + case "signupUser": { + return [ + body("name.firstName").not().isEmpty().trim().escape(), + body("name.lastName").not().isEmpty().trim().escape(), + body("email", "Invalid email").exists().isEmail().normalizeEmail(), + ]; + } + } +}; + +exports.createUser = (req, res) => { + // Finds the validation errors in this request and wraps them in an object with handy functions + const errors = validationResult(req); + + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }); + } + const user = new User({ + name: { + firstName: req.body.firstName, + lastName: req.body.lastName, + }, + email: req.body.email, + }); + + user.save((err, user) => { + if (err) { + res.status(500).send({ message: err }); + return; + } + + if (req.body.roles) { + Role.find( + { + name: { $in: req.body.roles }, + }, + (err, roles) => { + if (err) { + res.status(500).send({ message: err }); + return; + } + + user.roles = roles.map((role) => role._id); + user.save((err) => { + if (err) { + res.status(500).send({ message: err }); + return; + } + + res.send({ + message: "User was registered successfully with specified role!", + }); + }); + } + ); + } else { + Role.findOne({ name: "APP_USER" }, (err, role) => { + if (err) { + res.status(500).send({ message: err }); + return; + } + + user.roles = [role._id]; + user.save((err) => { + if (err) { + res.status(500).send({ message: err }); + return; + } + + res.send({ message: "User was registered successfully!" }); + }); + }); + } + }); +}; + +exports.signin = (req, res) => { + User.findOne({ + email: req.body.email, + }) + .populate("roles", "-__v") + .exec((err, user) => { + if (err) { + res.status(500).send({ message: err }); + return; + } + + if (!user) { + return res.status(404).send({ message: "User Not found." }); + } + + var token = jwt.sign({ id: user.id }, config.secret, { + expiresIn: 86400, // 24 hours + }); + + var authorities = []; + + for (let i = 0; i < user.roles.length; i++) { + authorities.push("ROLE_" + user.roles[i].name.toUpperCase()); + } + res.status(200).send({ + id: user._id, + email: user.email, + roles: authorities, + accessToken: token, + }); + }); +}; diff --git a/backend/middleware/index.js b/backend/middleware/index.js new file mode 100644 index 000000000..4faeec45a --- /dev/null +++ b/backend/middleware/index.js @@ -0,0 +1,7 @@ +const authJwt = require("./authJwt"); +const verifySignUp = require("./verifySignUp"); + +module.exports = { + authJwt, + verifySignUp, +}; diff --git a/backend/middleware/verifySignup.js b/backend/middleware/verifySignup.js new file mode 100644 index 000000000..b8b204ed5 --- /dev/null +++ b/backend/middleware/verifySignup.js @@ -0,0 +1,38 @@ +const db = require("../models"); +const ROLES = db.ROLES; +const User = db.user; + +checkDuplicateEmail = function (req, res, next) { + User.findOne({ email: req.body.email }).then((user) => { + if (user) { + res.status(400).send({ + message: "Failed! Email is already in use!", + }); + + return; + } + next(); + }); +}; + +checkRolesExisted = (req, res, next) => { + if (req.body.roles) { + for (let i = 0; i < req.body.roles.length; i++) { + if (!ROLES.includes(req.body.roles[i])) { + res.status(400).send({ + message: `Failed! Role ${req.body.roles[i]} does not exist!`, + }); + return; + } + } + } + + next(); +}; + +const verifySignUp = { + checkDuplicateEmail, + checkRolesExisted, +}; + +module.exports = verifySignUp; diff --git a/backend/models/index.js b/backend/models/index.js new file mode 100644 index 000000000..21ab00e81 --- /dev/null +++ b/backend/models/index.js @@ -0,0 +1,20 @@ +const mongoose = require("mongoose"); +mongoose.Promise = global.Promise; + +const db = {}; + +db.mongoose = mongoose; + +db.answer = require("./answer.model"); +db.checkin = require("./checkIn.model"); +db.event = require("./event.model"); +db.project = require("./project.model"); +db.projectTeamMember = require("./projectTeamMember.model"); +db.question = require("./question.model"); +db.recurringEvent = require("./recurringEvent.model"); +db.role = require("./role.model"); +db.user = require("./user.model"); + +db.ROLES = ["user", "admin", "moderator"]; + +module.exports = db; diff --git a/backend/models/role.model.js b/backend/models/role.model.js new file mode 100644 index 000000000..26ac63ce0 --- /dev/null +++ b/backend/models/role.model.js @@ -0,0 +1,10 @@ +const mongoose = require("mongoose"); + +const Role = mongoose.model( + "Role", + new mongoose.Schema({ + name: { type: String }, + }) +); + +module.exports = Role; diff --git a/backend/models/user.model.js b/backend/models/user.model.js index ce4c7d87b..6161432b5 100644 --- a/backend/models/user.model.js +++ b/backend/models/user.model.js @@ -1,74 +1,78 @@ -const mongoose = require('mongoose'); +const mongoose = require("mongoose"); // const bcrypt = require('bcrypt-nodejs'); mongoose.Promise = global.Promise; const userSchema = mongoose.Schema({ - name: { - firstName: { type: String }, - lastName: { type: String } + name: { + firstName: { type: String }, + lastName: { type: String }, + }, + email: { type: String, unique: true }, + accessLevel: { type: String, default: "user" }, + roles: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: "Role", + }, + ], + createdDate: { type: Date, default: Date.now }, + currentRole: { type: String }, // will remove but need to update check-in form + desiredRole: { type: String }, // will remove but need to update check-in form + newMember: { type: Boolean }, + currentJobTitle: { type: String }, // free-text of their current job title + desiredJobTitle: { type: String }, // free-text of their desired job title + skillsToMatch: [{ type: String }], // skills the user either has or wants to learn - will use to match to projects + firstAttended: { type: String }, + attendanceReason: { type: String }, + githubHandle: { type: String }, + projects: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: "Project", }, - email: { type: String, unique: true }, - accessLevel: { type: String, default: "user" }, - createdDate: { type: Date, default: Date.now }, - currentRole: { type: String }, // will remove but need to update check-in form - desiredRole: { type: String }, // will remove but need to update check-in form - newMember: { type: Boolean }, - currentJobTitle: { type: String }, // free-text of their current job title - desiredJobTitle: { type: String }, // free-text of their desired job title - skillsToMatch: [{ type: String }], // skills the user either has or wants to learn - will use to match to projects - firstAttended: { type: String }, - attendanceReason: { type: String }, - githubHandle: { type: String}, - projects: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: 'Project' - } - ], - githubHandle: { type: String}, // handle not including @, not the URL - phone: { type: String }, - textingOk: { type: Boolean, default: false}, // is the user OK with texting at the phone number provided? - slackName: { type: String } , // does the user input this? - isHflaGithubMember: { type: Boolean}, // pull from API once github handle in place? - githubPublic2FA: { type: Boolean }, // does the user have 2FA enabled on their github and membership set to public? - availability: { type: String } // availability to meet outside of hacknight times; string for now, more structured in future - //currentProject: { type: String } // no longer need this as we can get it from Project Team Member table - // password: { type: String, required: true } + ], + githubHandle: { type: String }, // handle not including @, not the URL + phone: { type: String }, + textingOk: { type: Boolean, default: false }, // is the user OK with texting at the phone number provided? + slackName: { type: String }, // does the user input this? + isHflaGithubMember: { type: Boolean }, // pull from API once github handle in place? + githubPublic2FA: { type: Boolean }, // does the user have 2FA enabled on their github and membership set to public? + availability: { type: String }, // availability to meet outside of hacknight times; string for now, more structured in future + //currentProject: { type: String } // no longer need this as we can get it from Project Team Member table + // password: { type: String, required: true } }); -userSchema.methods.serialize = function() { - return { - id: this._id, - name: { - firstName: this.name.firstName, - lastName: this.name.lastName - }, - email: this.email, - accessLevel: this.accessLevel, - createdDate: this.createdDate, - currentRole: this.currentRole, // will remove but need to update check-in form - desiredRole: this.desiredRole, // will remove but need to update check-in form - newMember: this.newMember, - currentJobTitle: this.currentRole, - desiredJobTitle: this.desiredRole, - skillsToMatch: this.skillsToMatch, - firstAttended: this.firstAttended, - attendanceReason: this.attendanceReason, - githubHandle: this.githubHandle, - projects: this.projects, - //currentProject: this.currentProject - phone: this.phone, - textingOk: this.textingOk, - slackName: this.slackName, - isHflaGithubMember: this.isHflaGithubMember, - githubPublic2FA: this.githubPublic2FA, - availability: this.availability - }; +userSchema.methods.serialize = function () { + return { + id: this._id, + name: { + firstName: this.name.firstName, + lastName: this.name.lastName, + }, + email: this.email, + accessLevel: this.accessLevel, + createdDate: this.createdDate, + currentRole: this.currentRole, // will remove but need to update check-in form + desiredRole: this.desiredRole, // will remove but need to update check-in form + newMember: this.newMember, + currentJobTitle: this.currentRole, + desiredJobTitle: this.desiredRole, + skillsToMatch: this.skillsToMatch, + firstAttended: this.firstAttended, + attendanceReason: this.attendanceReason, + githubHandle: this.githubHandle, + projects: this.projects, + //currentProject: this.currentProject + phone: this.phone, + textingOk: this.textingOk, + slackName: this.slackName, + isHflaGithubMember: this.isHflaGithubMember, + githubPublic2FA: this.githubPublic2FA, + availability: this.availability, + }; }; - - // userSchema.methods.generateHash = function(password) { // return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); // }; @@ -77,6 +81,6 @@ userSchema.methods.serialize = function() { // return bcrypt.compareSync(password, this.password); // }; -const User = mongoose.model('User', userSchema); +const User = mongoose.model("User", userSchema); -module.exports = User; \ No newline at end of file +module.exports = User; diff --git a/backend/package.json b/backend/package.json index fc5b0e443..49eca59ef 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,9 +18,9 @@ "concurrently": "^5.1.0", "debug": "^4.1.1", "jest": "^26.4.0", + "nodemon": "^2.0.2", "supertest": "^4.0.2", - "why-is-node-running": "^2.2.0", - "nodemon": "^2.0.2" + "why-is-node-running": "^2.2.0" }, "dependencies": { "@slack/bolt": "^2.2.3", @@ -29,6 +29,7 @@ "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "^4.17.1", + "express-validator": "^6.6.1", "googleapis": "^39.2.0", "helmet": "^3.22.0", "mongodb-memory-server": "^6.6.4", diff --git a/backend/routers/auth.router.js b/backend/routers/auth.router.js new file mode 100644 index 000000000..8b4312dfd --- /dev/null +++ b/backend/routers/auth.router.js @@ -0,0 +1,27 @@ +const { verifySignUp } = require("../middleware"); +const userController = require("../controllers/user.controller"); + +const express = require("express"); +const router = express.Router(); + +router.use(function (req, res, next) { + res.header( + "Access-Control-Allow-Headers", + "x-access-token, Origin, Content-Type, Accept" + ); + next(); +}); + +router.post( + "/signup", + [ + userController.validate("signupUser"), + verifySignUp.checkDuplicateEmail, + verifySignUp.checkRolesExisted, + ], + userController.createUser +); + +router.post("/signin", userController.signin); + +module.exports = router; diff --git a/backend/routers/auth.router.test.js b/backend/routers/auth.router.test.js index 0743cb72b..7942cce96 100644 --- a/backend/routers/auth.router.test.js +++ b/backend/routers/auth.router.test.js @@ -11,10 +11,47 @@ const CONFIG = require("../config/auth.config"); const db = require("../models"); const User = db.user; +const Role = db.role; + +function setupDBRoles() { + Role.collection.estimatedDocumentCount((err, count) => { + if (!err && count === 0) { + new Role({ + name: "APP_USER", + }).save((err) => { + if (err) { + console.log("error", err); + } + + console.log("added 'user' to roles collection"); + }); + + new Role({ + name: "APP_ADMIN", + }).save((err) => { + if (err) { + console.log("error", err); + } + + console.log("added 'moderator' to roles collection"); + }); + + new Role({ + name: "APP_SUPER_ADMIN", + }).save((err) => { + if (err) { + console.log("error", err); + } + + console.log("added 'admin' to roles collection"); + }); + } + }); +} // API Tests -describe("Test that we can create a user using Auth", () => { - test("Submit a user and retrieve that user", async () => { +describe("Test that we can create a user using /user routes", () => { + test("POST a user and retrieve that user from /user", async () => { // Test Data const submittedData = { name: { firstName: "test_first", lastName: "test_last" }, @@ -44,3 +81,73 @@ describe("Test that we can create a user using Auth", () => { expect(APIData.name === submittedData.name); }); }); + +describe("Test user can sign up through API", () => { + test("A POST with invalid name data should return a 400 and error message.", async () => { + // Test Data + const badUserData = { + firstName: "test_first", + lastName: "test_last", + email: "test@test.com", + }; + const res = await request + .post("/api/auth/signup") + .send(badUserData) + .set("Accept", "application/json"); + + expect(res.status).toBe(422); + const errorMessage = JSON.parse(res.text); + + expect(errorMessage.errors).toEqual([ + { msg: "Invalid value", param: "name.firstName", location: "body" }, + { msg: "Invalid value", param: "name.lastName", location: "body" }, + ]); + }); + test("A POST valid data should return a 200 and success message.", async () => { + setupDBRoles(); + // Test Data + const goodUserData = { + name: { firstName: "testname", lastName: "testlast" }, + email: "test@test.com", + }; + + const goodUserDataJSON = JSON.stringify(goodUserData); + const res = await request + .post("/api/auth/signup") + .send(goodUserData) + .set("Accept", "application/json"); + + expect(res.status).toBe(200); + expect(JSON.parse(res.text).message).toEqual( + "User was registered successfully!" + ); + }); + test("A POST of an already used email returns a 400 and an error message.", async () => { + setupDBRoles(); + // Test Data + const userOneWithSameEmail = { + name: { firstName: "one", lastName: "two" }, + email: "test@test.com", + }; + + const userTwoWithSameEmail = { + name: { firstName: "three", lastName: "four" }, + email: "test@test.com", + }; + + const _ = await request + .post("/api/auth/signup") + .send(userOneWithSameEmail) + .set("Accept", "application/json"); + + const res2 = await request + .post("/api/auth/signup") + .send(userTwoWithSameEmail) + .set("Accept", "application/json"); + + expect(res2.status).toBe(400); + expect(JSON.parse(res2.text).message).toEqual( + "Failed! Email is already in use!" + ); + }); +}); diff --git a/backend/server.js b/backend/server.js index 7507cc5ba..f8b9dfd30 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,6 +1,9 @@ const app = require("./app"); const mongoose = require("mongoose"); +const db = require("./models"); +const Role = db.role; + // Load config variables const { DATABASE_URL, PORT } = require("./config/database"); @@ -46,8 +49,45 @@ async function closeServer() { }); } +function initial() { + Role.collection.estimatedDocumentCount((err, count) => { + if (!err && count === 0) { + new Role({ + name: "APP_USER", + }).save((err) => { + if (err) { + console.log("error", err); + } + + console.log("added 'user' to roles collection"); + }); + + new Role({ + name: "APP_ADMIN", + }).save((err) => { + if (err) { + console.log("error", err); + } + + console.log("added 'moderator' to roles collection"); + }); + + new Role({ + name: "APP_SUPER_ADMIN", + }).save((err) => { + if (err) { + console.log("error", err); + } + + console.log("added 'admin' to roles collection"); + }); + } + }); +} + if (require.main === module) { runServer().catch((err) => console.error(err)); + initial(); } module.exports = { app, runServer, closeServer }; diff --git a/backend/yarn.lock b/backend/yarn.lock index df45290b6..a3d00bc47 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -2018,6 +2018,14 @@ expect@^26.4.2: jest-message-util "^26.3.0" jest-regex-util "^26.0.0" +express-validator@^6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-6.6.1.tgz#c53046f615d27fcb78be786e018dcd60bd9c6c5c" + integrity sha512-+MrZKJ3eGYXkNF9p9Zf7MS7NkPJFg9MDYATU5c80Cf4F62JdLBIjWxy6481tRC0y1NnC9cgOw8FuN364bWaGhA== + dependencies: + lodash "^4.17.19" + validator "^13.1.1" + express@^4.16.4, express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -5608,6 +5616,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validator@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.1.1.tgz#f8811368473d2173a9d8611572b58c5783f223bf" + integrity sha512-8GfPiwzzRoWTg7OV1zva1KvrSemuMkv07MA9TTl91hfhe+wKrsrgVN4H2QSFd/U/FhiU3iWPYVgvbsOGwhyFWw== + vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" From 0a2fccdc70d374bb12b3573691b7fbd47c0cfc52 Mon Sep 17 00:00:00 2001 From: Nick Beaird Date: Thu, 3 Sep 2020 16:42:47 -0700 Subject: [PATCH 05/10] Get a working signup end point --- backend/controllers/user.controller.js | 46 +++---------- backend/middleware/authJwt.js | 93 ++++++++++++++++++++++++++ backend/routers/auth.router.test.js | 10 --- backend/server.js | 10 --- 4 files changed, 103 insertions(+), 56 deletions(-) create mode 100644 backend/middleware/authJwt.js diff --git a/backend/controllers/user.controller.js b/backend/controllers/user.controller.js index e5f11fe25..8b24e04d9 100644 --- a/backend/controllers/user.controller.js +++ b/backend/controllers/user.controller.js @@ -32,6 +32,7 @@ exports.createUser = (req, res) => { lastName: req.body.lastName, }, email: req.body.email, + accessLevel: "user", }); user.save((err, user) => { @@ -39,49 +40,22 @@ exports.createUser = (req, res) => { res.status(500).send({ message: err }); return; } + Role.findOne({ name: "APP_USER" }, (err, role) => { + if (err) { + res.status(500).send({ message: err }); + return; + } - if (req.body.roles) { - Role.find( - { - name: { $in: req.body.roles }, - }, - (err, roles) => { - if (err) { - res.status(500).send({ message: err }); - return; - } - - user.roles = roles.map((role) => role._id); - user.save((err) => { - if (err) { - res.status(500).send({ message: err }); - return; - } - - res.send({ - message: "User was registered successfully with specified role!", - }); - }); - } - ); - } else { - Role.findOne({ name: "APP_USER" }, (err, role) => { + user.roles = [role._id]; + user.save((err) => { if (err) { res.status(500).send({ message: err }); return; } - user.roles = [role._id]; - user.save((err) => { - if (err) { - res.status(500).send({ message: err }); - return; - } - - res.send({ message: "User was registered successfully!" }); - }); + res.send({ message: "User was registered successfully!" }); }); - } + }); }); }; diff --git a/backend/middleware/authJwt.js b/backend/middleware/authJwt.js new file mode 100644 index 000000000..f514088a2 --- /dev/null +++ b/backend/middleware/authJwt.js @@ -0,0 +1,93 @@ +const jwt = require("jsonwebtoken"); +const config = require("../config/auth.config.js"); + +const db = require("../models"); +const User = db.user; +const Role = db.role; + +verifyToken = (req, res, next) => { + let token = req.headers["x-access-token"]; + + if (!token) { + return res.status(403).send({ message: "No token provided!" }); + } + + jwt.verify(token, config.secret, (err, decoded) => { + if (err) { + return res.status(401).send({ message: "Unauthorized!" }); + } + req.userId = decoded.id; + next(); + }); +}; + +isSuperAdmin = (req, res, next) => { + User.findById(req.userId).exec((err, user) => { + if (err) { + res.status(500).send({ message: err }); + return; + } + + Role.find( + { + _id: { $in: user.roles }, + }, + (err, roles) => { + if (err) { + res.status(500).send({ message: err }); + return; + } + + for (let i = 0; i < roles.length; i++) { + if (roles[i].name === "APP_SUPER_ADMIN") { + next(); + return; + } + } + + res + .status(403) + .send({ message: "Require Application Super Admin Role!" }); + return; + } + ); + }); +}; + +isAdmin = (req, res, next) => { + User.findById(req.userId).exec((err, user) => { + if (err) { + res.status(500).send({ message: err }); + return; + } + + Role.find( + { + _id: { $in: user.roles }, + }, + (err, roles) => { + if (err) { + res.status(500).send({ message: err }); + return; + } + + for (let i = 0; i < roles.length; i++) { + if (roles[i].name === "APP_ADMIN") { + next(); + return; + } + } + + res.status(403).send({ message: "Require Application Admin Role!" }); + return; + } + ); + }); +}; + +const authJwt = { + verifyToken, + isAdmin, + isSuperAdmin, +}; +module.exports = authJwt; diff --git a/backend/routers/auth.router.test.js b/backend/routers/auth.router.test.js index 7942cce96..1f0971472 100644 --- a/backend/routers/auth.router.test.js +++ b/backend/routers/auth.router.test.js @@ -35,16 +35,6 @@ function setupDBRoles() { console.log("added 'moderator' to roles collection"); }); - - new Role({ - name: "APP_SUPER_ADMIN", - }).save((err) => { - if (err) { - console.log("error", err); - } - - console.log("added 'admin' to roles collection"); - }); } }); } diff --git a/backend/server.js b/backend/server.js index f8b9dfd30..45f79bd40 100644 --- a/backend/server.js +++ b/backend/server.js @@ -71,16 +71,6 @@ function initial() { console.log("added 'moderator' to roles collection"); }); - - new Role({ - name: "APP_SUPER_ADMIN", - }).save((err) => { - if (err) { - console.log("error", err); - } - - console.log("added 'admin' to roles collection"); - }); } }); } From 7bc56b85f3eb5d153fe6966e18b0466ef3f85d54 Mon Sep 17 00:00:00 2001 From: Nick Beaird Date: Thu, 3 Sep 2020 20:00:55 -0700 Subject: [PATCH 06/10] Refactor API validation --- backend/controllers/user.controller.js | 24 ++++++++++++------------ backend/routers/auth.router.js | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/backend/controllers/user.controller.js b/backend/controllers/user.controller.js index 8b24e04d9..2a915deed 100644 --- a/backend/controllers/user.controller.js +++ b/backend/controllers/user.controller.js @@ -7,25 +7,25 @@ var jwt = require("jsonwebtoken"); const { body, validationResult } = require("express-validator"); -exports.validate = (method) => { - switch (method) { - case "signupUser": { - return [ - body("name.firstName").not().isEmpty().trim().escape(), - body("name.lastName").not().isEmpty().trim().escape(), - body("email", "Invalid email").exists().isEmail().normalizeEmail(), - ]; - } - } -}; +exports.validateCreateUserAPICall = async (req, res, next) => { + await body("name.firstName").not().isEmpty().trim().escape().run(req); + await body("name.lastName").not().isEmpty().trim().escape().run(req); + await body("email", "Invalid email") + .exists() + .isEmail() + .normalizeEmail() + .run(req); -exports.createUser = (req, res) => { // Finds the validation errors in this request and wraps them in an object with handy functions const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ errors: errors.array() }); } + next(); +}; + +exports.createUser = (req, res) => { const user = new User({ name: { firstName: req.body.firstName, diff --git a/backend/routers/auth.router.js b/backend/routers/auth.router.js index 8b4312dfd..d140016d1 100644 --- a/backend/routers/auth.router.js +++ b/backend/routers/auth.router.js @@ -15,7 +15,7 @@ router.use(function (req, res, next) { router.post( "/signup", [ - userController.validate("signupUser"), + userController.validateCreateUserAPICall, verifySignUp.checkDuplicateEmail, verifySignUp.checkRolesExisted, ], From 5044020d822039571c586372621982e793e49413 Mon Sep 17 00:00:00 2001 From: Nick Beaird Date: Fri, 4 Sep 2020 14:52:07 -0700 Subject: [PATCH 07/10] Update tests --- backend/config/auth.config.js | 4 ++-- backend/config/auth.config.test.js | 6 +++++ backend/routers/auth.router.test.js | 34 ++--------------------------- 3 files changed, 10 insertions(+), 34 deletions(-) create mode 100644 backend/config/auth.config.test.js diff --git a/backend/config/auth.config.js b/backend/config/auth.config.js index 672156f4a..e040fe725 100644 --- a/backend/config/auth.config.js +++ b/backend/config/auth.config.js @@ -1,5 +1,5 @@ module.exports = { - secret: + SECRET: "c0d7d0716e4cecffe9dcc77ff90476d98f5aace08ea40f5516bd982b06401021191f0f24cd6759f7d8ca41b64f68d0b3ad19417453bddfd1dbe8fcb197245079", - custom_request_header: process.env.CUSTOM_REQUEST_HEADER, + CUSTOM_REQUEST_HEADER: process.env.CUSTOM_REQUEST_HEADER, }; diff --git a/backend/config/auth.config.test.js b/backend/config/auth.config.test.js new file mode 100644 index 000000000..b3522b8d7 --- /dev/null +++ b/backend/config/auth.config.test.js @@ -0,0 +1,6 @@ +const { expectCt } = require("helmet"); + +test("Environment variables are working as expected", () => { + const backendUrl = process.env.REACT_APP_PROXY; + expect(backendUrl === `http://localhost:${process.env.BACKEND_PORT}`); +}); diff --git a/backend/routers/auth.router.test.js b/backend/routers/auth.router.test.js index 1f0971472..ab7e9e4e2 100644 --- a/backend/routers/auth.router.test.js +++ b/backend/routers/auth.router.test.js @@ -5,39 +5,10 @@ const request = supertest(app); const { setupDB } = require("../setup-test"); setupDB("api-auth"); -// const authRouter = require("./auth.router"); - const CONFIG = require("../config/auth.config"); const db = require("../models"); const User = db.user; -const Role = db.role; - -function setupDBRoles() { - Role.collection.estimatedDocumentCount((err, count) => { - if (!err && count === 0) { - new Role({ - name: "APP_USER", - }).save((err) => { - if (err) { - console.log("error", err); - } - - console.log("added 'user' to roles collection"); - }); - - new Role({ - name: "APP_ADMIN", - }).save((err) => { - if (err) { - console.log("error", err); - } - - console.log("added 'moderator' to roles collection"); - }); - } - }); -} // API Tests describe("Test that we can create a user using /user routes", () => { @@ -48,7 +19,7 @@ describe("Test that we can create a user using /user routes", () => { email: "test@test.com", }; let headers = {}; - headers["x-customrequired-header"] = CONFIG.custom_request_header; + headers["x-customrequired-header"] = CONFIG.CUSTOM_REQUEST_HEADER; // Add an event with a project using the API. const res = await request @@ -94,7 +65,7 @@ describe("Test user can sign up through API", () => { ]); }); test("A POST valid data should return a 200 and success message.", async () => { - setupDBRoles(); + // setupDBRoles(); // Test Data const goodUserData = { name: { firstName: "testname", lastName: "testlast" }, @@ -113,7 +84,6 @@ describe("Test user can sign up through API", () => { ); }); test("A POST of an already used email returns a 400 and an error message.", async () => { - setupDBRoles(); // Test Data const userOneWithSameEmail = { name: { firstName: "one", lastName: "two" }, From 86186a0216aa97081cbfedb2ae7b175d37c08281 Mon Sep 17 00:00:00 2001 From: Nick Beaird Date: Fri, 4 Sep 2020 14:54:13 -0700 Subject: [PATCH 08/10] Add middleware --- backend/middleware/authJwt.js | 78 ++---------------------------- backend/middleware/index.js | 4 +- backend/middleware/verifySignup.js | 38 --------------- backend/middleware/verifyUser.js | 21 ++++++++ 4 files changed, 27 insertions(+), 114 deletions(-) delete mode 100644 backend/middleware/verifySignup.js create mode 100644 backend/middleware/verifyUser.js diff --git a/backend/middleware/authJwt.js b/backend/middleware/authJwt.js index f514088a2..870e181c5 100644 --- a/backend/middleware/authJwt.js +++ b/backend/middleware/authJwt.js @@ -1,93 +1,23 @@ const jwt = require("jsonwebtoken"); -const config = require("../config/auth.config.js"); +const CONFIG = require("../config/auth.config.js"); -const db = require("../models"); -const User = db.user; -const Role = db.role; - -verifyToken = (req, res, next) => { +function verifyToken(req, res, next) { let token = req.headers["x-access-token"]; if (!token) { return res.status(403).send({ message: "No token provided!" }); } - jwt.verify(token, config.secret, (err, decoded) => { + jwt.verify(token, CONFIG.SECRET, (err, decoded) => { if (err) { return res.status(401).send({ message: "Unauthorized!" }); } req.userId = decoded.id; next(); }); -}; - -isSuperAdmin = (req, res, next) => { - User.findById(req.userId).exec((err, user) => { - if (err) { - res.status(500).send({ message: err }); - return; - } - - Role.find( - { - _id: { $in: user.roles }, - }, - (err, roles) => { - if (err) { - res.status(500).send({ message: err }); - return; - } - - for (let i = 0; i < roles.length; i++) { - if (roles[i].name === "APP_SUPER_ADMIN") { - next(); - return; - } - } - - res - .status(403) - .send({ message: "Require Application Super Admin Role!" }); - return; - } - ); - }); -}; - -isAdmin = (req, res, next) => { - User.findById(req.userId).exec((err, user) => { - if (err) { - res.status(500).send({ message: err }); - return; - } - - Role.find( - { - _id: { $in: user.roles }, - }, - (err, roles) => { - if (err) { - res.status(500).send({ message: err }); - return; - } - - for (let i = 0; i < roles.length; i++) { - if (roles[i].name === "APP_ADMIN") { - next(); - return; - } - } - - res.status(403).send({ message: "Require Application Admin Role!" }); - return; - } - ); - }); -}; +} const authJwt = { verifyToken, - isAdmin, - isSuperAdmin, }; module.exports = authJwt; diff --git a/backend/middleware/index.js b/backend/middleware/index.js index 4faeec45a..dfdf5190d 100644 --- a/backend/middleware/index.js +++ b/backend/middleware/index.js @@ -1,7 +1,7 @@ const authJwt = require("./authJwt"); -const verifySignUp = require("./verifySignUp"); +const verifyUser = require("./verifyUser"); module.exports = { authJwt, - verifySignUp, + verifyUser, }; diff --git a/backend/middleware/verifySignup.js b/backend/middleware/verifySignup.js deleted file mode 100644 index b8b204ed5..000000000 --- a/backend/middleware/verifySignup.js +++ /dev/null @@ -1,38 +0,0 @@ -const db = require("../models"); -const ROLES = db.ROLES; -const User = db.user; - -checkDuplicateEmail = function (req, res, next) { - User.findOne({ email: req.body.email }).then((user) => { - if (user) { - res.status(400).send({ - message: "Failed! Email is already in use!", - }); - - return; - } - next(); - }); -}; - -checkRolesExisted = (req, res, next) => { - if (req.body.roles) { - for (let i = 0; i < req.body.roles.length; i++) { - if (!ROLES.includes(req.body.roles[i])) { - res.status(400).send({ - message: `Failed! Role ${req.body.roles[i]} does not exist!`, - }); - return; - } - } - } - - next(); -}; - -const verifySignUp = { - checkDuplicateEmail, - checkRolesExisted, -}; - -module.exports = verifySignUp; diff --git a/backend/middleware/verifyUser.js b/backend/middleware/verifyUser.js new file mode 100644 index 000000000..192039774 --- /dev/null +++ b/backend/middleware/verifyUser.js @@ -0,0 +1,21 @@ +const db = require("../models"); +const User = db.user; + +checkDuplicateEmail = function (req, res, next) { + User.findOne({ email: req.body.email }).then((user) => { + if (user) { + res.status(400).send({ + message: "Failed! Email is already in use!", + }); + + return; + } + next(); + }); +}; + +const verifyUser = { + checkDuplicateEmail, +}; + +module.exports = verifyUser; From 4e79c052ae871364f4efd3d918a4048aeb6179e9 Mon Sep 17 00:00:00 2001 From: Nick Beaird Date: Fri, 4 Sep 2020 16:21:25 -0700 Subject: [PATCH 09/10] Get basic Magic Link working --- backend/controllers/email.controller.js | 77 +++++++++ backend/controllers/user.controller.js | 139 ++++++++-------- backend/middleware/verifyUser.js | 26 ++- backend/package.json | 5 +- backend/routers/auth.router.js | 14 +- backend/routers/auth.router.test.js | 86 +++++++++- backend/yarn.lock | 204 ++++++++++++------------ docker-compose.yml | 43 +---- 8 files changed, 377 insertions(+), 217 deletions(-) create mode 100644 backend/controllers/email.controller.js diff --git a/backend/controllers/email.controller.js b/backend/controllers/email.controller.js new file mode 100644 index 000000000..652e46b4b --- /dev/null +++ b/backend/controllers/email.controller.js @@ -0,0 +1,77 @@ +const nodemailer = require("nodemailer"); +const { google } = require("googleapis"); +const OAuth2 = google.auth.OAuth2; + +const jwt = require("jsonwebtoken"); + +const CLIENT_ID = process.env.GMAIL_CLIENT_ID; +const SECRET_ID = process.env.GMAIL_SECRET_ID; +const REFRESH_TOKEN = process.env.GMAIL_REFRESH_TOKEN; +const EMAIL_ACCOUNT = process.env.GMAIL_EMAIL; + +async function mailServer(email, token) { + const oauth2Client = new OAuth2( + CLIENT_ID, // ClientID + SECRET_ID, // Client Secret + "https://developers.google.com/oauthplayground" // Redirect URL + ); + + oauth2Client.setCredentials({ + refresh_token: REFRESH_TOKEN, + }); + const accessToken = oauth2Client.getAccessToken(); + + let smtpTransport; + if (process.env.NODE_ENV === "test") { + // Send mail to Mailhog Docker container + smtpTransport = nodemailer.createTransport({ + host: "127.0.0.1", + port: 1025, + auth: { + user: "user", + pass: "password", + }, + }); + } else { + smtpTransport = nodemailer.createTransport({ + service: "gmail", + auth: { + type: "OAuth2", + user: EMAIL_ACCOUNT, + clientId: CLIENT_ID, + clientSecret: SECRET_ID, + refreshToken: REFRESH_TOKEN, + accessToken: accessToken, + }, + }); + } + const appUrl = process.env.REACT_APP_PROXY; + const encodedToken = encodeURIComponent(token); + const emailLink = `https://tinyurl.com/2drxdk/auth/me?token=${encodedToken}`; + const encodedUri = encodeURI(emailLink); + const mailOptions = { + from: EMAIL_ACCOUNT, + to: email, + subject: "VRMS Magic link 🎩 !", + html: ` + LOGIN HERE + `, + text: `Magic link: ${encodedUri}`, + }; + + if (process.env.NODE_ENV === "test") { + smtpTransport.sendMail(mailOptions, (error, response) => { + console.log("email sent"); + smtpTransport.close(); + }); + } else { + smtpTransport.sendMail(mailOptions, (error, response) => { + error ? console.log(error) : console.log(response); + smtpTransport.close(); + }); + } +} + +exports.sendUserEmailSigninLink = async (email, token) => { + await mailServer(email, token); +}; diff --git a/backend/controllers/user.controller.js b/backend/controllers/user.controller.js index 2a915deed..89da00db7 100644 --- a/backend/controllers/user.controller.js +++ b/backend/controllers/user.controller.js @@ -1,31 +1,18 @@ -const config = require("../config/auth.config"); -const db = require("../models"); -const User = db.user; -const Role = db.role; +const CONFIG = require("../config/auth.config"); +const DB = require("../models"); +const emailController = require("./email.controller"); +const User = DB.user; var jwt = require("jsonwebtoken"); const { body, validationResult } = require("express-validator"); -exports.validateCreateUserAPICall = async (req, res, next) => { - await body("name.firstName").not().isEmpty().trim().escape().run(req); - await body("name.lastName").not().isEmpty().trim().escape().run(req); - await body("email", "Invalid email") - .exists() - .isEmail() - .normalizeEmail() - .run(req); +function generateAccessToken(user) { + // expires after half and hour (1800 seconds = 30 minutes) + return jwt.sign({ id: user.id }, CONFIG.SECRET, { expiresIn: "1800s" }); +} - // Finds the validation errors in this request and wraps them in an object with handy functions - const errors = validationResult(req); - - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }); - } - next(); -}; - -exports.createUser = (req, res) => { +function createUser(req, res) { const user = new User({ name: { firstName: req.body.firstName, @@ -39,55 +26,79 @@ exports.createUser = (req, res) => { if (err) { res.status(500).send({ message: err }); return; + } else { + return res + .status(200) + .send({ message: "User was registered successfully!" }); } - Role.findOne({ name: "APP_USER" }, (err, role) => { - if (err) { - res.status(500).send({ message: err }); - return; - } - - user.roles = [role._id]; - user.save((err) => { - if (err) { - res.status(500).send({ message: err }); - return; - } - - res.send({ message: "User was registered successfully!" }); - }); - }); }); -}; -exports.signin = (req, res) => { - User.findOne({ - email: req.body.email, - }) - .populate("roles", "-__v") - .exec((err, user) => { - if (err) { - res.status(500).send({ message: err }); - return; - } + const jsonToken = generateAccessToken(user); + emailController.sendUserEmailSigninLink(req.body.email, jsonToken); +} + +function signin(req, res) { + const { email } = req.body; + console.log(email); + User.findOne({ email }) + .then((user) => { if (!user) { - return res.status(404).send({ message: "User Not found." }); + res.status(401).send({ message: "User not authorized" }); + } else { + const jsonToken = generateAccessToken(user); + emailController.sendUserEmailSigninLink(req.body.email, jsonToken); + return res + .status(200) + .send({ message: "User login link sent to email!" }); } + }) + .catch((err) => { + console.log(err); - var token = jwt.sign({ id: user.id }, config.secret, { - expiresIn: 86400, // 24 hours - }); + res.status(400).send({ message: "User email not found." }); + }); +} - var authorities = []; +async function validateCreateUserAPICall(req, res, next) { + await body("name.firstName").not().isEmpty().trim().escape().run(req); + await body("name.lastName").not().isEmpty().trim().escape().run(req); + await body("email", "Invalid email") + .exists() + .isEmail() + .normalizeEmail() + .run(req); - for (let i = 0; i < user.roles.length; i++) { - authorities.push("ROLE_" + user.roles[i].name.toUpperCase()); - } - res.status(200).send({ - id: user._id, - email: user.email, - roles: authorities, - accessToken: token, - }); - }); + // Finds the validation errors in this request and wraps them in an object with handy functions + const errors = validationResult(req); + + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }); + } + next(); +} + +async function validateSigninUserAPICall(req, res, next) { + await body("email", "Invalid email") + .exists() + .isEmail() + .normalizeEmail() + .run(req); + + // Finds the validation errors in this request and wraps them in an object with handy functions + const errors = validationResult(req); + + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }); + } + next(); +} + +userController = { + validateCreateUserAPICall, + validateSigninUserAPICall, + createUser, + signin, }; + +module.exports = userController; diff --git a/backend/middleware/verifyUser.js b/backend/middleware/verifyUser.js index 192039774..8b41e1239 100644 --- a/backend/middleware/verifyUser.js +++ b/backend/middleware/verifyUser.js @@ -1,7 +1,7 @@ const db = require("../models"); const User = db.user; -checkDuplicateEmail = function (req, res, next) { +function checkDuplicateEmail(req, res, next) { User.findOne({ email: req.body.email }).then((user) => { if (user) { res.status(400).send({ @@ -12,10 +12,32 @@ checkDuplicateEmail = function (req, res, next) { } next(); }); -}; +} + +function isAdmin(req, res, next) { + User.findOne({ email: req.body.email }).then((user) => { + if (!user) { + res.status(400).send({ + message: "User does not exist", + }); + } else { + const role = user.accessLevel; + if (role === "admin") { + next(); + } else { + next( + res.status(401).send({ + message: "Invalid permissions", + }) + ); + } + } + }); +} const verifyUser = { checkDuplicateEmail, + isAdmin, }; module.exports = verifyUser; diff --git a/backend/package.json b/backend/package.json index 49eca59ef..6e24c2028 100644 --- a/backend/package.json +++ b/backend/package.json @@ -30,13 +30,14 @@ "dotenv": "^8.2.0", "express": "^4.17.1", "express-validator": "^6.6.1", - "googleapis": "^39.2.0", + "googleapis": "^59.0.0", "helmet": "^3.22.0", "mongodb-memory-server": "^6.6.4", "mongoose": "^5.10.0", "morgan": "^1.10.0", "node-cron": "^2.0.3", - "node-fetch": "^2.6.0" + "node-fetch": "^2.6.0", + "nodemailer": "^6.4.11" }, "directories": { "test": "test" diff --git a/backend/routers/auth.router.js b/backend/routers/auth.router.js index d140016d1..a07b4ca1e 100644 --- a/backend/routers/auth.router.js +++ b/backend/routers/auth.router.js @@ -1,4 +1,4 @@ -const { verifySignUp } = require("../middleware"); +const { verifyUser } = require("../middleware"); const userController = require("../controllers/user.controller"); const express = require("express"); @@ -14,14 +14,14 @@ router.use(function (req, res, next) { router.post( "/signup", - [ - userController.validateCreateUserAPICall, - verifySignUp.checkDuplicateEmail, - verifySignUp.checkRolesExisted, - ], + [userController.validateCreateUserAPICall, verifyUser.checkDuplicateEmail], userController.createUser ); -router.post("/signin", userController.signin); +router.post( + "/signin", + [userController.validateSigninUserAPICall, verifyUser.isAdmin], + userController.signin +); module.exports = router; diff --git a/backend/routers/auth.router.test.js b/backend/routers/auth.router.test.js index ab7e9e4e2..04dac828c 100644 --- a/backend/routers/auth.router.test.js +++ b/backend/routers/auth.router.test.js @@ -72,7 +72,6 @@ describe("Test user can sign up through API", () => { email: "test@test.com", }; - const goodUserDataJSON = JSON.stringify(goodUserData); const res = await request .post("/api/auth/signup") .send(goodUserData) @@ -111,3 +110,88 @@ describe("Test user can sign up through API", () => { ); }); }); + +describe("Test user can sign in through API", () => { + test("A POST with an admin user returns a 200 and sends a Magic Link.", async () => { + // Test Data + + // Create user in DB + const goodUserData = { + name: { + firstName: "Free", + lastName: "Mason", + }, + email: "test@test.com", + accessLevel: "admin", + }; + await User.create(goodUserData); + + // POST to the DB with that same data. + const res = await request + .post("/api/auth/signin") + .send(goodUserData) + .set("Accept", "application/json"); + + expect(res.status).toBe(200); + expect(JSON.parse(res.text).message).toEqual( + "User login link sent to email!" + ); + }); + + test("A POST with an non admin user returns 401 and helpful error message.", async () => { + // Test Data + + // Create user in DB + const notValidPermission = { + name: { + firstName: "Free", + lastName: "Mason", + }, + email: "test@test.com", + accessLevel: "user", + }; + await User.create(notValidPermission); + + // POST to the DB with that same data. + const res = await request + .post("/api/auth/signin") + .send(notValidPermission) + .set("Accept", "application/json"); + + expect(res.status).toBe(401); + expect(JSON.parse(res.text).message).toEqual("Invalid permissions"); + }); + + test("A POST with non-valid email returns a 422 and a helpful error message.", async () => { + // Test Data + + // Create user in DB + const notValidEmailPayload = { + name: { + firstName: "Free", + lastName: "Mason", + }, + email: "test", + accessLevel: "admin", + }; + await User.create(notValidEmailPayload); + + // POST to the DB with that same data. + const res = await request + .post("/api/auth/signin") + .send(notValidEmailPayload) + .set("Accept", "application/json"); + + expect(res.status).toBe(422); + const errorMessage = JSON.parse(res.text); + + expect(errorMessage.errors).toEqual([ + { + value: "test", + msg: "Invalid email", + param: "email", + location: "body", + }, + ]); + }); +}); diff --git a/backend/yarn.lock b/backend/yarn.lock index a3d00bc47..ce75ae5e9 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -846,13 +846,6 @@ agent-base@6: dependencies: debug "4" -agent-base@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" - integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== - dependencies: - es6-promisify "^5.0.0" - ajv@^6.12.3: version "6.12.4" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" @@ -960,6 +953,11 @@ array.prototype.map@^1.0.1: es-array-method-boxes-properly "^1.0.0" is-string "^1.0.4" +arrify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -1791,7 +1789,7 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -ecdsa-sig-formatter@1.0.11: +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== @@ -1881,18 +1879,6 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= - dependencies: - es6-promise "^4.0.3" - escape-goat@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" @@ -2281,23 +2267,24 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -gaxios@^1.0.2, gaxios@^1.0.4, gaxios@^1.2.1, gaxios@^1.2.2: - version "1.8.4" - resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-1.8.4.tgz#e08c34fe93c0a9b67a52b7b9e7a64e6435f9a339" - integrity sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw== +gaxios@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-3.1.0.tgz#95f65f5a335f61aff602fe124cfdba8524f765fa" + integrity sha512-DDTn3KXVJJigtz+g0J3vhcfbDbKtAroSTxauWsdnP57sM5KZ3d2c/3D9RKFJ86s43hfw6WULg6TXYw/AYiBlpA== dependencies: abort-controller "^3.0.0" extend "^3.0.2" - https-proxy-agent "^2.2.1" + https-proxy-agent "^5.0.0" + is-stream "^2.0.0" node-fetch "^2.3.0" -gcp-metadata@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-1.0.0.tgz#5212440229fa099fc2f7c2a5cdcb95575e9b2ca6" - integrity sha512-Q6HrgfrCQeEircnNP3rCcEgiDv7eF9+1B+1MMgpE190+/+0mjQR8PxeOaRgxZWmdDAF9EIryHB9g1moPiw1SbQ== +gcp-metadata@^4.1.0: + version "4.1.4" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.1.4.tgz#3adadb9158c716c325849ee893741721a3c09e7e" + integrity sha512-5J/GIH0yWt/56R3dNaNWPGQ/zXsZOddYECfJaqxFWgrZ9HC2Kvc5vl9upOgUUHKzURjAVf2N+f6tEJiojqXUuA== dependencies: - gaxios "^1.0.2" - json-bigint "^0.3.0" + gaxios "^3.0.0" + json-bigint "^1.0.0" gensync@^1.0.0-beta.1: version "1.0.0-beta.1" @@ -2376,48 +2363,47 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -google-auth-library@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-3.1.2.tgz#ff2f88cd5cd2118a57bd3d5ad3c093c8837fc350" - integrity sha512-cDQMzTotwyWMrg5jRO7q0A4TL/3GWBgO7I7q5xGKNiiFf9SmGY/OJ1YsLMgI2MVHHsEGyrqYnbnmV1AE+Z6DnQ== +google-auth-library@^6.0.0: + version "6.0.6" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-6.0.6.tgz#5102e5c643baab45b4c16e9752cd56b8861f3a82" + integrity sha512-fWYdRdg55HSJoRq9k568jJA1lrhg9i2xgfhVIMJbskUmbDpJGHsbv9l41DGhCDXM21F9Kn4kUwdysgxSYBYJUw== dependencies: + arrify "^2.0.0" base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" fast-text-encoding "^1.0.0" - gaxios "^1.2.1" - gcp-metadata "^1.0.0" - gtoken "^2.3.2" - https-proxy-agent "^2.2.1" - jws "^3.1.5" - lru-cache "^5.0.0" - semver "^5.5.0" + gaxios "^3.0.0" + gcp-metadata "^4.1.0" + gtoken "^5.0.0" + jws "^4.0.0" + lru-cache "^6.0.0" -google-p12-pem@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-1.0.4.tgz#b77fb833a2eb9f7f3c689e2e54f095276f777605" - integrity sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA== +google-p12-pem@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.0.3.tgz#673ac3a75d3903a87f05878f3c75e06fc151669e" + integrity sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA== dependencies: - node-forge "^0.8.0" - pify "^4.0.0" + node-forge "^0.10.0" -googleapis-common@^0.7.0: - version "0.7.2" - resolved "https://registry.yarnpkg.com/googleapis-common/-/googleapis-common-0.7.2.tgz#a694f55d979cb7c2eac21a0e0439af12f9b418ba" - integrity sha512-9DEJIiO4nS7nw0VE1YVkEfXEj8x8MxsuB+yZIpOBULFSN9OIKcUU8UuKgSZFU4lJmRioMfngktrbkMwWJcUhQg== +googleapis-common@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/googleapis-common/-/googleapis-common-4.4.0.tgz#b806e41c4e883f22b68769aafb3ed11802919091" + integrity sha512-Bgrs8/1OZQFFIfVuX38L9t48rPAkVUXttZy6NzhhXxFOEMSHgfFIjxou7RIXOkBHxmx2pVwct9WjKkbnqMYImQ== dependencies: - gaxios "^1.2.2" - google-auth-library "^3.0.0" - pify "^4.0.0" - qs "^6.5.2" + extend "^3.0.2" + gaxios "^3.0.0" + google-auth-library "^6.0.0" + qs "^6.7.0" url-template "^2.0.8" - uuid "^3.2.1" + uuid "^8.0.0" -googleapis@^39.2.0: - version "39.2.0" - resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-39.2.0.tgz#5c81f721e9da2e80cb0b25821ed60d3bc200c3da" - integrity sha512-66X8TG1B33zAt177sG1CoKoYHPP/B66tEpnnSANGCqotMuY5gqSQO8G/0gqHZR2jRgc5CHSSNOJCnpI0SuDxMQ== +googleapis@^59.0.0: + version "59.0.0" + resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-59.0.0.tgz#aec5c6f5b0b347c9c6996d45fa8ad8176e585fae" + integrity sha512-GV/E4KRN89a4GxSk7D7cwUfRYgcJHR05sOgm/WGdwc/u8dxNXG5lWmz9gF5ZwFGk2yKtVxL4VZNn4zBuZ6rmGg== dependencies: - google-auth-library "^3.0.0" - googleapis-common "^0.7.0" + google-auth-library "^6.0.0" + googleapis-common "^4.4.0" got@^9.6.0: version "9.6.0" @@ -2446,16 +2432,15 @@ growly@^1.3.0: resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= -gtoken@^2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-2.3.3.tgz#8a7fe155c5ce0c4b71c886cfb282a9060d94a641" - integrity sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw== +gtoken@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.0.3.tgz#b76ef8e9a2fed6fef165e47f7d05b60c498e4d05" + integrity sha512-Nyd1wZCMRc2dj/mAD0LlfQLcAO06uKdpKJXvK85SGrF5+5+Bpfil9u/2aw35ltvEHjvl0h5FMKN5knEU+9JrOg== dependencies: - gaxios "^1.0.4" - google-p12-pem "^1.0.0" - jws "^3.1.5" + gaxios "^3.0.0" + google-p12-pem "^3.0.0" + jws "^4.0.0" mime "^2.2.0" - pify "^4.0.0" har-schema@^2.0.0: version "2.0.0" @@ -2630,14 +2615,6 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -https-proxy-agent@^2.2.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" - integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== - dependencies: - agent-base "^4.3.0" - debug "^3.1.0" - https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" @@ -3467,10 +3444,10 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -json-bigint@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-0.3.1.tgz#0c1729d679f580d550899d6a2226c228564afe60" - integrity sha512-DGWnSzmusIreWlEupsUelHrhwmPPE+FiQvg+drKfk2p+bdEYa5mp4PJ8JsCWqae0M2jQNb0HPvnwvf1qOTThzQ== +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== dependencies: bignumber.js "^9.0.0" @@ -3546,7 +3523,16 @@ jwa@^1.4.1: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" -jws@^3.1.5, jws@^3.2.2: +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== @@ -3554,6 +3540,14 @@ jws@^3.1.5, jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + kareem@2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.1.tgz#def12d9c941017fabfb00f873af95e9c99e1be87" @@ -3697,12 +3691,12 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== -lru-cache@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: - yallist "^3.0.2" + yallist "^4.0.0" make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" @@ -4053,10 +4047,10 @@ node-fetch@^2.3.0, node-fetch@^2.6.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== -node-forge@^0.8.0: - version "0.8.5" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.8.5.tgz#57906f07614dc72762c84cef442f427c0e1b86ee" - integrity sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q== +node-forge@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== node-int64@^0.4.0: version "0.4.0" @@ -4080,6 +4074,11 @@ node-notifier@^8.0.0: uuid "^8.3.0" which "^2.0.2" +nodemailer@^6.4.11: + version "6.4.11" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.11.tgz#1f00b4ffd106403f17c03f3d43d5945b2677046c" + integrity sha512-BVZBDi+aJV4O38rxsUh164Dk1NCqgh6Cm0rQSb9SK/DHGll/DrCMnycVDD7msJgZCnmVa8ASo8EZzR7jsgTukQ== + nodemon@^2.0.2: version "2.0.4" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.4.tgz#55b09319eb488d6394aa9818148c0c2d1c04c416" @@ -4397,11 +4396,6 @@ pify@^3.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= -pify@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - pirates@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" @@ -4515,7 +4509,7 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== -qs@^6.5.1, qs@^6.5.2: +qs@^6.5.1, qs@^6.7.0: version "6.9.4" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== @@ -5589,12 +5583,12 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@8.3.0, uuid@^8.2.0, uuid@^8.3.0: +uuid@8.3.0, uuid@^8.0.0, uuid@^8.2.0, uuid@^8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== -uuid@^3.2.1, uuid@^3.3.2: +uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -5789,10 +5783,10 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yargs-parser@^13.1.2: version "13.1.2" diff --git a/docker-compose.yml b/docker-compose.yml index 3314ef7d9..b92207eb8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,38 +1,9 @@ -# version: '3' -# services: -# ############### -# # NODE SERVER # -# ############### -# server: -# build: -# context: . -# dockerfile: Dockerfile -# expose: -# - "3000" -# - "4000" -# volumes: -# - .:/usr/src/app -# environment: -# API_HOST: "http://localhost:4000" -# ports: -# - "3000:4000" +version: '3' -# ############# -# # REACT APP # -# ############# -# # web: -# # build: ./client -# # expose: -# # - "3000" -# # environment: -# # - REACT_APP_PORT="3000" -# # ports: -# # - "3000:3000" -# # volumes: -# # - ./client/src:/client/src -# # - ./client/public:/client/public -# # links: -# # - server - - +services: + mailhog: + image: mailhog/mailhog + ports: + - 1025:1025 # smtp server + - 8025:8025 # web ui From 0776f69e8336adcd62480828d21fed261218a6f4 Mon Sep 17 00:00:00 2001 From: Nick Beaird Date: Fri, 4 Sep 2020 16:21:55 -0700 Subject: [PATCH 10/10] Cleanup extra fields --- backend/models/index.js | 2 -- backend/models/user.model.js | 6 ------ 2 files changed, 8 deletions(-) diff --git a/backend/models/index.js b/backend/models/index.js index 21ab00e81..daebe462f 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -15,6 +15,4 @@ db.recurringEvent = require("./recurringEvent.model"); db.role = require("./role.model"); db.user = require("./user.model"); -db.ROLES = ["user", "admin", "moderator"]; - module.exports = db; diff --git a/backend/models/user.model.js b/backend/models/user.model.js index 6161432b5..73c01d919 100644 --- a/backend/models/user.model.js +++ b/backend/models/user.model.js @@ -10,12 +10,6 @@ const userSchema = mongoose.Schema({ }, email: { type: String, unique: true }, accessLevel: { type: String, default: "user" }, - roles: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "Role", - }, - ], createdDate: { type: Date, default: Date.now }, currentRole: { type: String }, // will remove but need to update check-in form desiredRole: { type: String }, // will remove but need to update check-in form