-
Notifications
You must be signed in to change notification settings - Fork 42
Distinguish user and admin #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
da75583
7fbddd7
fc6d3f6
dd836a4
4db0046
3fad9a8
7ddc6d1
9f69097
e526e20
3e2b611
8a651ac
24edc5f
02f9238
eee1eb4
7d430de
3cfd6ba
caaac7f
63c5774
c3d36a8
cb4021b
304d9d7
c83bc15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| 'use strict' | ||
|
|
||
| const Admin = require('../models/admin.model') | ||
| const jwt = require('jsonwebtoken') | ||
| const config = require('../config') | ||
| const httpStatus = require('http-status') | ||
| const generateToken = require('../models/utils/findAndGenerateToken') | ||
|
|
||
| exports.register = async (req, res, next) => { | ||
| try { | ||
| const admin = new Admin(req.body) | ||
| const savedAdmin = await admin.save() | ||
| res.status(httpStatus.CREATED) | ||
| res.send(savedAdmin.transform()) | ||
| } catch (error) { | ||
| return next(Admin.checkDuplicateEmailError(error)) | ||
| } | ||
| } | ||
|
|
||
| exports.login = async (req, res, next) => { | ||
| try { | ||
| const admin = await generateToken(req.body, 'admin') | ||
| const payload = { sub: admin.id } | ||
| const token = jwt.sign(payload, config.secret) | ||
| return res.json({ message: 'OK', token: token }) | ||
| } catch (error) { | ||
| next(error) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,16 @@ | ||
| 'use strict' | ||
|
|
||
| const User = require('../models/user.model') | ||
| const passport = require('passport') | ||
| const APIError = require('../utils/APIError') | ||
| const httpStatus = require('http-status') | ||
| const bluebird = require('bluebird') | ||
|
|
||
| // handleJWT with roles | ||
| const handleJWT = (req, res, next, roles) => async (err, user, info) => { | ||
| const handleJWT = (req, res, next, role) => async (err, user, info) => { | ||
| const error = err || info | ||
| const logIn = bluebird.promisify(req.logIn) | ||
| const apiError = new APIError( | ||
| error ? error.message : 'Unauthorized', | ||
| httpStatus.UNAUTHORIZED | ||
| error ? error.message : 'Unauthorized', httpStatus.UNAUTHORIZED | ||
| ) | ||
|
|
||
| // log user in | ||
|
|
@@ -24,7 +22,7 @@ const handleJWT = (req, res, next, roles) => async (err, user, info) => { | |
| } | ||
|
|
||
| // see if user is authorized to do the action | ||
| if (!roles.includes(user.role)) { | ||
| if (role && role.includes('admin') && !user.admin) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method was used to add new roles to user model. Not just admin. There maybe an editor, moderator etc as many as possible. In route definition we can add which roles can see the resouece. Like say you want a protected route only for admins and editors. we can simply put
|
||
| return next(new APIError('Forbidden', httpStatus.FORBIDDEN)) | ||
| } | ||
|
|
||
|
|
@@ -34,11 +32,11 @@ const handleJWT = (req, res, next, roles) => async (err, user, info) => { | |
| } | ||
|
|
||
| // exports the middleware | ||
| const authorize = (roles = User.roles) => (req, res, next) => | ||
| const authorize = (role) => (req, res, next) => | ||
| passport.authenticate( | ||
| 'jwt', | ||
| { session: false }, | ||
| handleJWT(req, res, next, roles) | ||
| handleJWT(req, res, next, role) | ||
| )(req, res, next) | ||
|
|
||
| module.exports = authorize | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| 'use strict' | ||
| const mongoose = require('mongoose') | ||
| const bcrypt = require('bcrypt-nodejs') | ||
| const Schema = mongoose.Schema | ||
| const hashPass = require('./utils/hashPass') | ||
| const checkDuplicateEmailError = require('./utils/checkDuplicateEmailError') | ||
|
|
||
| const adminSchema = new Schema({ | ||
| email: { | ||
| type: String, | ||
| required: true, | ||
| unique: true, | ||
| lowercase: true | ||
| }, | ||
| password: { | ||
| type: String, | ||
| required: true, | ||
| minlength: 4, | ||
| maxlength: 128 | ||
| }, | ||
| name: { | ||
| type: String, | ||
| maxlength: 50 | ||
| }, | ||
| admin: { | ||
| type: Boolean, | ||
| default: true | ||
| } | ||
| }, { | ||
| timestamps: true | ||
| }) | ||
|
|
||
| hashPass(adminSchema) | ||
|
|
||
| adminSchema.method({ | ||
| transform () { | ||
| const transformed = {} | ||
| const fields = ['id', 'name', 'email', 'createdAt'] | ||
| fields.forEach((field) => { transformed[field] = this[field] }) | ||
| return transformed | ||
| }, | ||
|
|
||
| passwordMatches (password) { | ||
| return bcrypt.compareSync(password, this.password) | ||
| } | ||
| }) | ||
|
|
||
| adminSchema.statics = { checkDuplicateEmailError } | ||
|
|
||
| module.exports = mongoose.model('Admin', adminSchema) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,12 @@ | ||
| 'use strict' | ||
|
|
||
| const mongoose = require('mongoose') | ||
| const bcrypt = require('bcrypt-nodejs') | ||
| const httpStatus = require('http-status') | ||
| const APIError = require('../utils/APIError') | ||
| const transporter = require('../services/transporter') | ||
| const config = require('../config') | ||
| const Schema = mongoose.Schema | ||
|
|
||
| const roles = [ | ||
| 'user', 'admin' | ||
| ] | ||
| const hashPass = require('./utils/hashPass') | ||
| const checkDuplicateEmailError = require('./utils/checkDuplicateEmailError') | ||
|
|
||
| const userSchema = new Schema({ | ||
| email: { | ||
|
|
@@ -35,37 +32,20 @@ const userSchema = new Schema({ | |
| active: { | ||
| type: Boolean, | ||
| default: false | ||
| }, | ||
| role: { | ||
| type: String, | ||
| default: 'user', | ||
| enum: roles | ||
| } | ||
| }, { | ||
| timestamps: true | ||
| }) | ||
|
|
||
| userSchema.pre('save', async function save (next) { | ||
| try { | ||
| if (!this.isModified('password')) { | ||
| return next() | ||
| } | ||
|
|
||
| this.password = bcrypt.hashSync(this.password) | ||
|
|
||
| return next() | ||
| } catch (error) { | ||
| return next(error) | ||
| } | ||
| }) | ||
| hashPass(userSchema) | ||
|
|
||
| userSchema.post('save', async function saved (doc, next) { | ||
| try { | ||
| const mailOptions = { | ||
| from: 'noreply', | ||
| to: this.email, | ||
| subject: 'Confirm creating account', | ||
| html: `<div><h1>Hello new user!</h1><p>Click <a href="${config.hostname}/api/auth/confirm?key=${this.activationKey}">link</a> to activate your new account.</p></div><div><h1>Hello developer!</h1><p>Feel free to change this template ;).</p></div>` | ||
| html: `<div><h1>Hello new user!</h1><p>Click <a href="${config.hostname}/api/user/confirm?key=${this.activationKey}">link</a> to activate your new account.</p></div><div><h1>Hello developer!</h1><p>Feel free to change this template ;).</p></div>` | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. config.hostname is not available, this will not work
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a config param. Check in config file
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But it seems I've forgotten to add it to .env.example somehow
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I didn't know but wanted to keep this change so I renamed it to BASE_URI in next PR. Sorry for confusion.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added hostname because imagine we are running in something like kubernets. In that scenario when sending emails we should use the web address not the internal IP. But I see the name is more confusing, we should rename it to something like
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, we can rename it and when this PR will be merged I'll resolve conflicts in second one. |
||
| } | ||
|
|
||
| transporter.sendMail(mailOptions, function (error, info) { | ||
|
|
@@ -83,14 +63,10 @@ userSchema.post('save', async function saved (doc, next) { | |
| }) | ||
|
|
||
| userSchema.method({ | ||
| transform () { | ||
| transform: function () { | ||
| const transformed = {} | ||
| const fields = ['id', 'name', 'email', 'createdAt', 'role'] | ||
|
|
||
| fields.forEach((field) => { | ||
| transformed[field] = this[field] | ||
| }) | ||
|
|
||
| const fields = ['id', 'name', 'email', 'createdAt'] | ||
| fields.forEach(field => { transformed[field] = this[field] }) | ||
| return transformed | ||
| }, | ||
|
|
||
|
|
@@ -99,39 +75,6 @@ userSchema.method({ | |
| } | ||
| }) | ||
|
|
||
| userSchema.statics = { | ||
| roles, | ||
|
|
||
| checkDuplicateEmailError (err) { | ||
| if (err.code === 11000) { | ||
| var error = new Error('Email already taken') | ||
| error.errors = [{ | ||
| field: 'email', | ||
| location: 'body', | ||
| messages: ['Email already taken'] | ||
| }] | ||
| error.status = httpStatus.CONFLICT | ||
| return error | ||
| } | ||
|
|
||
| return err | ||
| }, | ||
|
|
||
| async findAndGenerateToken (payload) { | ||
| const { email, password } = payload | ||
| if (!email) throw new APIError('Email must be provided for login') | ||
|
|
||
| const user = await this.findOne({ email }).exec() | ||
| if (!user) throw new APIError(`No user associated with ${email}`, httpStatus.NOT_FOUND) | ||
|
|
||
| const passwordOK = await user.passwordMatches(password) | ||
|
|
||
| if (!passwordOK) throw new APIError(`Password mismatch`, httpStatus.UNAUTHORIZED) | ||
|
|
||
| if (!user.active) throw new APIError(`User not activated`, httpStatus.UNAUTHORIZED) | ||
|
|
||
| return user | ||
| } | ||
| } | ||
| userSchema.statics = { checkDuplicateEmailError } | ||
|
|
||
| module.exports = mongoose.model('User', userSchema) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| 'use strict' | ||
|
|
||
| const httpStatus = require('http-status') | ||
|
|
||
| function checkDuplicateEmailError (err) { | ||
| if (err.code === 11000) { | ||
| var error = new Error('Email already taken') | ||
| error.errors = [{ | ||
| field: 'email', | ||
| location: 'body', | ||
| messages: ['Email already taken'] | ||
| }] | ||
| error.status = httpStatus.CONFLICT | ||
| return error | ||
| } | ||
|
|
||
| return err | ||
| } | ||
|
|
||
| module.exports = checkDuplicateEmailError |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| 'use strict' | ||
|
|
||
| const httpStatus = require('http-status') | ||
| const APIError = require('../../utils/APIError') | ||
| const User = require('../user.model') | ||
| const Admin = require('../admin.model') | ||
|
|
||
| async function findAndGenerateToken (payload, from) { | ||
| const { email, password } = payload | ||
| if (!email) throw new APIError('Email must be provided for login') | ||
| let user | ||
|
|
||
| if (from === 'admin') { | ||
| user = await Admin.findOne({ email }).exec() | ||
| } | ||
| if (from === 'user') { | ||
| user = await User.findOne({ email }).exec() | ||
| } | ||
|
|
||
| if (!user) throw new APIError(`No user associated with ${email}`, httpStatus.NOT_FOUND) | ||
|
|
||
| const passwordOK = await user.passwordMatches(password) | ||
|
|
||
| if (!passwordOK) throw new APIError(`Password mismatch`, httpStatus.UNAUTHORIZED) | ||
| if (from === 'user' && !user.active) { | ||
| throw new APIError(`User not activated`, httpStatus.UNAUTHORIZED) | ||
| } | ||
|
|
||
| return user | ||
| } | ||
|
|
||
| module.exports = findAndGenerateToken |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| 'use strict' | ||
|
|
||
| const bcrypt = require('bcrypt-nodejs') | ||
|
|
||
| const hashPass = (schema) => { | ||
| schema.pre('save', async function save (next) { | ||
| try { | ||
| if (!this.isModified('password')) { | ||
| return next() | ||
| } | ||
|
|
||
| this.password = bcrypt.hashSync(this.password) | ||
|
|
||
| return next() | ||
| } catch (error) { | ||
| return next(error) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| module.exports = hashPass |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| 'use strict' | ||
|
|
||
| const express = require('express') | ||
| const router = express.Router() | ||
| const adminController = require('../../controllers/admin.controller') | ||
| const auth = require('../../middlewares/authorization') | ||
|
|
||
| router.post('/login', adminController.login) | ||
| router.post('/register', auth(['admin']), adminController.register) | ||
|
|
||
| module.exports = router |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a new line at the end of file