Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ee551a0
feat: initial docker, react, apollo, graphql setup
spielhoelle Sep 4, 2020
0c637fe
feat: basic login interface, validator, toast
spielhoelle Sep 4, 2020
02caed4
fix: nodemon for backend in docker container
spielhoelle Sep 5, 2020
048846f
feat: basic authentication
spielhoelle Sep 5, 2020
7de5aea
feat: add pre-production nginx middleware
spielhoelle Sep 5, 2020
0a29cb5
feat: basic employee list view
spielhoelle Sep 5, 2020
f2737f0
chore: review-model, seeed proper example data
spielhoelle Sep 5, 2020
4a2cd25
feat: render assoc. reviews
spielhoelle Sep 5, 2020
187f90c
feat: basic nav styling, responsive
spielhoelle Sep 5, 2020
97e6366
feat: destroyEmployee, purposedriven scope
spielhoelle Sep 6, 2020
f9925b1
chore: scope auth-logic in seperated module
spielhoelle Sep 6, 2020
c635bfd
feat: create employee
spielhoelle Sep 6, 2020
7e1f2b3
chore: pw-role defaults, rm logging
spielhoelle Sep 7, 2020
860e859
chore: silence mongo db docker logs
spielhoelle Sep 7, 2020
c883cbc
feat: update employee = complete CRUD cycle
spielhoelle Sep 7, 2020
188472a
feat: crud review, reference empl. to review
spielhoelle Sep 8, 2020
95c6ca7
feat: impr. ux/ui, require fields, loadingstates
spielhoelle Sep 8, 2020
37cd5f9
fix: missing dependency
spielhoelle Sep 8, 2020
b8503a1
feat: add feedback to review
spielhoelle Sep 8, 2020
d0db215
feat: error fallsback, rm unused modules
spielhoelle Sep 8, 2020
83ac6e8
chore: fill db with associated feedback
spielhoelle Sep 8, 2020
b3e97b1
feat: add feedback to employee reviews
spielhoelle Sep 8, 2020
ff00077
chore: scope queries
spielhoelle Sep 8, 2020
795fc4b
fix: reflect logged in state nav items
spielhoelle Sep 8, 2020
08a69fb
feat: secure frontend routing
spielhoelle Sep 8, 2020
04e70fe
docs: add basic hints to clarify code, cleanup
spielhoelle Sep 8, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
.env
.idea
.vscode
15 changes: 15 additions & 0 deletions .graphqlconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "Untitled GraphQL Schema",
"schemaPath": "schema.graphql",
"extensions": {
"endpoints": {
"Default GraphQL Endpoint": {
"url": "http://localhost:8080",
"headers": {
"user-agent": "JS GraphQL"
},
"introspect": false
}
}
}
}
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,27 @@ Design a web application that allows employees to submit feedback toward each ot
* Assumptions you make given limited requirements
* Technology and design choices
* Identify areas of your strengths

## Start application in docker
* make sure docker daemon is running
* make sure that the folder-location is mounted in dockers ressources/file-sharing options
* start all containers with `docker-compose up`
* stop them with `docker-compose down` or ^+c in the terminal
* frontend runs on localhost:3000
* backend runs on localhost:8080

## Docs
The code is documented using TODO comment syntax to easily find documentation-objects. Do a matchcase-fulltext search for `TODO` to find all relevant documentation.

## TODOs

### Frontend
- [ ] Submit forms with enter
- [ ] Unify frontend tables and find HOC abstraction possibilities
- [ ] Split reducer and global state in appropriate partial scopes to minimize re-renders (employee reducer, review reducer)
- [ ] remove react warnings about unused variables

### Backend
- [ ] Improve mongodb table relations, validations, default values and model-concerns
- [ ] Security considerations like deeper password-hashing, encryption between docker instances, properly restricted routes for user-roles
- [ ] Deployment and scaling considerations
2 changes: 2 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.env
22 changes: 22 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM node:10.5.0
RUN mkdir -p /home/node/node_modules
WORKDIR /home/node
COPY package*.json ./
RUN npm install --production

ARG CACHEBUST=1
COPY . .

ARG DATABASE_HOST
ENV DATABASE_HOST $DATABASE_HOST

ARG DATABASE_PORT
ENV DATABASE_PORT $DATABASE_PORT

ARG DATABASE_NAME
ENV DATABASE_NAME $DATABASE_NAME

ARG PORT
ENV PORT $PORT

CMD [ "npm", "start" ]
65 changes: 65 additions & 0 deletions backend/app/auth/authController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const bcrypt = require("bcrypt");
const Employee = require("../employees/Employee");
const saltRounds = 10;
const jwt = require("jsonwebtoken");
// TODO create a unified response bottleneck. Unveils full power if app grows and response differs from request to request
const ro = require("../../helpers/response-object");
const uuidv4 = require("uuid/v4");
const sendVerificationMail = require("../../helpers/mailer");
const Boom = require("boom");
const salt = bcrypt.genSaltSync(saltRounds);

// TODO create a unified response bottleneck. Unveils full power if app grows and response differs from request to request
const employeeObject = employee => {
const payload = {
id: employee.id,
email: employee.email,
name: employee.name,
subscription: employee.subscription,
role: employee.role
};

const token = jwt.sign(payload, process.env.SECRET, { expiresIn: 86400 });
return { jwtToken: token };
};
module.exports.signin = async (req, res) => {
try {
if (!req.body) {
return Boom.badData(`Not enough data send to server. Provide email + password`);
} else if (!req.body.email) {
return Boom.badData(`Email missing`);
} else {
const employee = await Employee.findOne({ email: req.body.email });
if (!employee) {
return Boom.notFound(`Employee not found`);
} else if (employee) {
if (!employee.verifiedAt) {
try {
const employeeToken = uuidv4(4);
await sendVerificationMail(req, employeeToken);
employee.verificationToken = employeeToken;
Employee.update(employee);

return Boom.forbidden(`Employee not verified yet, check your emails for a verification link`);
} catch (e) {
console.log("Error in authController.signin", e);
}
} else {
const hashedPW = bcrypt.compareSync(req.body.password, employee.password); // true
if (!hashedPW) {
return Boom.unauthorized("Authentication failed. Wrong password.");
} else {
return employeeObject(employee);
}
}
}
}
} catch (error) {
console.log(error);
}
};
// TODO currently unused - later purpose for polling interval if token expired
module.exports.refreshtoken = (req, res, next) => {
const employee = req.employee;
return res.json(ro(201, `Welcome`, employeeObject(employee)));
};
10 changes: 10 additions & 0 deletions backend/app/auth/authRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const Router = require("restify-router").Router;
const router = new Router();
const validateToken = require("../../helpers/validateToken");
const AuthController = require("./authController");

router.post("signin", AuthController.signin);

router.get("refresh_token", validateToken, AuthController.refreshtoken);

module.exports = router;
21 changes: 21 additions & 0 deletions backend/app/auth/authSchema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { gql } = require('apollo-server');
const AuthController = require("../auth/authController");

module.exports.authTypeDefs = gql`
extend type Mutation {
login(email: String, password: String): LoginSuccess
}
type LoginSuccess {
jwtToken: String
}
`;

module.exports.authResolvers = {
Query: {
},
Mutation: {
login: async (_, payload, context) => {
return AuthController.signin({ body: { email: payload.email, password: payload.password } }, context)
},
}
}
60 changes: 60 additions & 0 deletions backend/app/employees/Employee.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
// Employee Schema
const EmployeeSchema = mongoose.Schema({
email: {
type: String,
index: true
},
name: {
type: String
},
password: {
type: String
},
role: {
type: String,
default: "user"
},
verificationToken: {
type: String,
default: false
},
createdAt: {
type: Date,
default: Date.now()
},
verifiedAt: {
type: Date
}
});

const Employee = (module.exports = mongoose.model("Employee", EmployeeSchema));

// TODO currently still unused - later keep model-logic on adequate level (not in controller/view etc.)
module.exports.createEmployee = (newEmployee, callback) => {
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(newEmployee.password, salt, (err, hash) => {
newEmployee.password = hash;
newEmployee.save(callback);
});
});
};
// TODO currently still unused - later keep model-logic on adequate level (not in controller/view etc.)
module.exports.getEmployeeByEmail = (email, callback) => {
const query = { email: email };
Employee.findOne(query, callback);
};

// TODO currently still unused - later keep model-logic on adequate level (not in controller/view etc.)
module.exports.getEmployeeById = function(id, callback) {
Employee.findById(id, callback);
};

// TODO currently still unused - later keep model-logic on adequate level (not in controller/view etc.)
module.exports.comparePassword = (candidatePassword, hash, callback) => {
bcrypt.compare(candidatePassword, hash, (err, isMatch) => {
if (err) throw err;
callback(null, isMatch);
});
};
59 changes: 59 additions & 0 deletions backend/app/employees/employeeSchema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const { gql } = require('apollo-server');
const Employee = require("./Employee");

module.exports.employeeTypeDefs = gql`
extend type Query {
employees: [Employee]
update(title: Int): Employee
employee(id: Int): Employee
}
extend type Mutation {
addEmployee(employee: EmployeeData): Employee
destroyEmployee(id: ID): ID
}
type Employee {
id: String,
email: String,
name: String,
role: String,
password: String,
createdAt: String,
verifiedAt: String,
}
input EmployeeData {
email: String,
name: String,
role: String,
id: String
}
type Response {
id: String
}
`;

module.exports.employeeResolvers = {
Query: {
employees: () => Employee.find({}).exec({}),
},
Mutation: {
addEmployee: async (_, payload, context) => {
let createdEmployee
try {
if (payload.employee.id) {
createdEmployee = await Employee.findByIdAndUpdate(payload.employee.id, payload.employee)
return createdEmployee
} else {
createdEmployee = await Employee.create({ ...payload.employee, password: "password" })
return createdEmployee
}
} catch (error) {
console.log('error', error);
return error
}
},
destroyEmployee: async (_, payload, context) => {
await Employee.findByIdAndDelete(payload.id)
return payload.id
},
}
}
19 changes: 19 additions & 0 deletions backend/app/feedbacks/Feedback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const mongoose = require("mongoose");
// Feedback Schema
const FeedbackSchema = mongoose.Schema({
text: {
type: String,
},
review: {
type: mongoose.Schema.ObjectId, ref: "Review"
},
employee: {
type: mongoose.Schema.ObjectId, ref: "Employee"
},
createdAt: {
type: Date,
default: Date.now()
},
});

const Feedback = (module.exports = mongoose.model("Feedback", FeedbackSchema));
42 changes: 42 additions & 0 deletions backend/app/feedbacks/feedbackSchema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const { gql } = require('apollo-server');
const Feedback = require("./Feedback");

module.exports.feedbackTypeDefs = gql`
extend type Query {
feedbacks: [Feedback]
}
extend type Mutation {
addFeedback(feedback: FeedbackData): Feedback
}
type Feedback {
id: String,
text: String,
review: Review,
employee: Employee,
createdAt: String!
}
input FeedbackData {
text: String,
review: String,
employee: String,
}
`;

module.exports.feedbackResolvers = {
Query: {
feedbacks: async () => await Feedback.find({}).populate('review').populate('employee').exec({}),
},
Mutation: {
addFeedback: async (_, payload, context) => {
try {
let createdFeedback = await Feedback.create({ ...payload.feedback })
console.log('createdFeedback', createdFeedback);
createdFeedback = await createdFeedback.populate('review').populate('employee').execPopulate()
return createdFeedback
} catch (error) {
console.log('error', error);
return error
}
},
}
}
17 changes: 17 additions & 0 deletions backend/app/reviews/Review.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const mongoose = require("mongoose");
// Review Schema
const ReviewSchema = mongoose.Schema({
score: {
type: String,
},
employee: {
type: mongoose.Schema.ObjectId, ref: "Employee",
index: true
},
createdAt: {
type: Date,
default: Date.now()
},
});

const Review = (module.exports = mongoose.model("Review", ReviewSchema));
Loading