diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7832c6c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*.{js}] +charset = utf-8 + +# 4 space indentation +[*.js] +indent_style = tab +indent_size = 4 + +# Matches the exact files either package.json or .travis.yml +[{package.json}] +indent_style = space +indent_size = 2 diff --git a/api/.eslintrc.json b/api/.eslintrc.json new file mode 100644 index 0000000..5dc16b9 --- /dev/null +++ b/api/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": "eslint:recommended", + "env": { + "node": true, + "browser": false, + "es6": true + }, + "parserOptions": { + "ecmaVersion": 2018 + }, + "rules": { + "semi": [ + "error" + ], + "no-trailing-spaces": [ + "error" + ], + "object-curly-newline": [ + "error" + ], + "object-property-newline": [ + "error" + ], + "quotes": [ + "error", + "backtick" + ], + "indent": [ + "error", + "tab" + ] + } +} diff --git a/api/app.js b/api/app.js index d478c04..7b6da2e 100644 --- a/api/app.js +++ b/api/app.js @@ -1,16 +1,16 @@ -const express = require('express') -const app = express() -const passport = require('passport') +const express = require(`express`); +const app = express(); +const passport = require(`passport`); const { - users -} = require('./controllers') + users +} = require(`./controllers`); /** * Configure Passport */ -try { require('./config/passport')(passport) } -catch (error) { console.log(error) } +try { require(`./config/passport`)(passport); } +catch (error) { console.log(error); } /** * Configure Express.js Middleware @@ -18,26 +18,26 @@ catch (error) { console.log(error) } // Enable CORS app.use(function (req, res, next) { - res.header('Access-Control-Allow-Origin', '*') - res.header('Access-Control-Allow-Methods', '*') - res.header('Access-Control-Allow-Headers', '*') - res.header('x-powered-by', 'serverless-express') - next() -}) + res.header(`Access-Control-Allow-Origin`, `*`); + res.header(`Access-Control-Allow-Methods`, `*`); + res.header(`Access-Control-Allow-Headers`, `*`); + res.header(`x-powered-by`, `serverless-express`); + next(); +}); // Initialize Passport and restore authentication state, if any, from the session -app.use(passport.initialize()) -app.use(passport.session()) +app.use(passport.initialize()); +app.use(passport.session()); // Enable JSON use -app.use(express.json()) +app.use(express.json()); // Since Express doesn't support error handling of promises out of the box, // this handler enables that const asyncHandler = fn => (req, res, next) => { - return Promise - .resolve(fn(req, res, next)) - .catch(next); + return Promise + .resolve(fn(req, res, next)) + .catch(next); }; /** @@ -45,37 +45,37 @@ const asyncHandler = fn => (req, res, next) => { */ app.options(`*`, (req, res) => { - res.status(200).send() -}) + res.status(200).send(); +}); -app.post(`/users/register`, asyncHandler(users.register)) +app.post(`/users/register`, asyncHandler(users.register)); -app.post(`/users/login`, asyncHandler(users.login)) +app.post(`/users/login`, asyncHandler(users.login)); app.get(`/test/`, (req, res) => { - res.status(200).send('Request received') -}) + res.status(200).send(`Request received`); +}); /** * Routes - Protected */ -app.post(`/user`, passport.authenticate('jwt', { session: false }), asyncHandler(users.get)) +app.post(`/user`, passport.authenticate(`jwt`, { session: false }), asyncHandler(users.get)); /** * Routes - Catch-All */ app.get(`/*`, (req, res) => { - res.status(404).send('Route not found') -}) + res.status(404).send(`Route not found`); +}); /** * Error Handler */ -app.use(function (err, req, res, next) { - console.error(err) - res.status(500).json({ error: `Internal Serverless Error - "${err.message}"` }) -}) +app.use(function (err, req, res) { + console.error(err); + res.status(500).json({ error: `Internal Serverless Error - "${err.message}"` }); +}); -module.exports = app \ No newline at end of file +module.exports = app; diff --git a/api/config/passport.js b/api/config/passport.js index 552bc09..a714720 100644 --- a/api/config/passport.js +++ b/api/config/passport.js @@ -2,26 +2,25 @@ * Config: Passport.js */ -const StrategyJWT = require('passport-jwt').Strategy -const ExtractJWT = require('passport-jwt').ExtractJwt -const { users } = require('../models') -const { comparePassword } = require('../utils') +const StrategyJWT = require(`passport-jwt`).Strategy; +const ExtractJWT = require(`passport-jwt`).ExtractJwt; +const { users } = require(`../models`); module.exports = (passport) => { - const options = {} - options.jwtFromRequest = ExtractJWT.fromAuthHeaderAsBearerToken() - options.secretOrKey = process.env.tokenSecret + const options = {}; + options.jwtFromRequest = ExtractJWT.fromAuthHeaderAsBearerToken(); + options.secretOrKey = process.env.tokenSecret; - passport.use(new StrategyJWT(options, async (jwtPayload, done) => { - let user - try { user = await users.getById(jwtPayload.id) } - catch (error) { - console.log(error) - return done(error, null) - } + passport.use(new StrategyJWT(options, async (jwtPayload, done) => { + let user; + try { user = await users.getById(jwtPayload.id); } + catch (error) { + console.log(error); + return done(error, null); + } - if (!user) { return done(null, false) } - return done(null, user) - })) -} \ No newline at end of file + if (!user) { return done(null, false); } + return done(null, user); + })); +}; diff --git a/api/controllers/index.js b/api/controllers/index.js index 34f82d6..94c6e9b 100644 --- a/api/controllers/index.js +++ b/api/controllers/index.js @@ -1,5 +1,5 @@ -const users = require('./users') +const users = require(`./users`); module.exports = { - users, -} \ No newline at end of file + users, +}; \ No newline at end of file diff --git a/api/controllers/users.js b/api/controllers/users.js index 4a042df..32a9ca4 100644 --- a/api/controllers/users.js +++ b/api/controllers/users.js @@ -2,80 +2,84 @@ * Controllers: Users */ -const jwt = require('jsonwebtoken') -const { users } = require('../models') -const { comparePassword } = require('../utils') +const jwt = require(`jsonwebtoken`); +const { users } = require(`../models`); +const { comparePassword } = require(`../utils`); /** * Save - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res * @param {*} next */ const register = async (req, res, next) => { + try { + await users.register(req.body); + } catch (error) { + return res.status(400).json({ error: error.message }); + } - try { - await users.register(req.body) - } catch (error) { - return res.status(400).json({ error: error.message }) - } + let user; + try { + user = await users.getByEmail(req.body.email); + } catch (error) { + console.log(error); + return next(error, null); + } - let user - try { - user = await users.getByEmail(req.body.email) - } catch (error) { - console.log(error) - return next(error, null) - } + const token = jwt.sign(user, process.env.tokenSecret, { + expiresIn: 604800 // 1 week + }); - const token = jwt.sign(user, process.env.tokenSecret, { - expiresIn: 604800 // 1 week - }) - - res.json({ message: 'Authentication successful', token }) -} + res.json({ + message: `Authentication successful`, + token + }); +}; /** * Sign a user in - * @param {*} req - * @param {*} res - * @param {*} next + * @param {*} req + * @param {*} res + * @param {*} next */ -const login = async (req, res, next) => { +const login = async (req, res) => { + let user; + try { user = await users.getByEmail(req.body.email); } + catch (error) { return done(error, null); } - let user - try { user = await users.getByEmail(req.body.email) } - catch (error) { return done(error, null) } + if (!user) { + return res.status(404).send({ error: `Authentication failed. User not found.` }); + } - if (!user) { - return res.status(404).send({ error: 'Authentication failed. User not found.' }) - } + const isCorrect = comparePassword(req.body.password, user.password); + if (!isCorrect) { + return res.status(401).send({ error: `Authentication failed. Wrong password.` }); + } - const isCorrect = comparePassword(req.body.password, user.password) - if (!isCorrect) { - return res.status(401).send({ error: 'Authentication failed. Wrong password.' }) - } + const token = jwt.sign(user, process.env.tokenSecret, { + expiresIn: 604800 // 1 week + }); - const token = jwt.sign(user, process.env.tokenSecret, { - expiresIn: 604800 // 1 week - }) - - res.json({ message: 'Authentication successful', token }) -} + res.json({ + message: `Authentication successful`, + token + }); +}; /** * Get a user - * @param {*} req - * @param {*} res - * @param {*} next + * @param {*} req + * @param {*} res + * @param {*} next */ -const get = async (req, res, next) => { - const user = users.convertToPublicFormat(req.user) - res.json({ user }) -} +const get = async (req, res) => { + const user = users.convertToPublicFormat(req.user); + res.json({ user }); +}; module.exports = { - register, - login, - get, -} \ No newline at end of file + register, + login, + get, +}; diff --git a/api/models/index.js b/api/models/index.js index 34f82d6..94c6e9b 100644 --- a/api/models/index.js +++ b/api/models/index.js @@ -1,5 +1,5 @@ -const users = require('./users') +const users = require(`./users`); module.exports = { - users, -} \ No newline at end of file + users, +}; \ No newline at end of file diff --git a/api/models/users.js b/api/models/users.js index b29e60f..05cc9d1 100644 --- a/api/models/users.js +++ b/api/models/users.js @@ -1,136 +1,136 @@ +"use strict"; /** * Model: Users */ -const AWS = require('aws-sdk') -const shortid = require('shortid') -const utils = require('../utils') +const AWS = require(`aws-sdk`); +const shortid = require(`shortid`); +const utils = require(`../utils`); const dynamodb = new AWS.DynamoDB.DocumentClient({ - region: process.env.AWS_REGION -}) + region: process.env.AWS_REGION +}); /** * Register user * @param {string} user.email User email * @param {string} user.password User password */ -const register = async(user = {}) => { +const register = async (user = {}) => { - // Validate - if (!user.email) { - throw new Error(`"email" is required`) - } - if (!user.password) { - throw new Error(`"password" is required`) - } - if (!utils.validateEmailAddress(user.email)) { - throw new Error(`"${user.email}" is not a valid email address`) - } + // Validate + if (!user.email) throw new Error(`"email" is required`); + if (!user.password) { + throw new Error(`"password" is required`); + } + if (!utils.validateEmailAddress(user.email)) { + throw new Error(`"${user.email}" is not a valid email address`); + } - // Check if user is already registered - const existingUser = await getByEmail(user.email) - if (existingUser) { - throw new Error(`A user with email "${user.email}" is already registered`) - } + // Check if user is already registered + const existingUser = await getByEmail(user.email); + if (existingUser) { + throw new Error(`A user with email "${user.email}" is already registered`); + } - user.password = utils.hashPassword(user.password) + user.password = utils.hashPassword(user.password); - // Save - const params = { - TableName: process.env.db, - Item: { - hk: user.email, - sk: 'user', - sk2: shortid.generate(), - createdAt: Date.now(), - updatedAt: Date.now(), - password: user.password, - } - } + // Save + const params = { + TableName: process.env.db, + Item: { + hk: user.email, + sk: `user`, + sk2: shortid.generate(), + createdAt: Date.now(), + updatedAt: Date.now(), + password: user.password, + } + }; - await dynamodb.put(params).promise() -} + await dynamodb.put(params).promise(); +}; /** * Get user by email address * @param {string} email */ -const getByEmail = async(email) => { +const getByEmail = async (email) => { - // Validate - if (!email) { - throw new Error(`"email" is required`) - } - if (!utils.validateEmailAddress(email)) { - throw new Error(`"${email}" is not a valid email address`) - } + // Validate + if (!email) { + throw new Error(`"email" is required`); + } + if (!utils.validateEmailAddress(email)) { + throw new Error(`"${email}" is not a valid email address`); + } - // Query - const params = { - TableName: process.env.db, - KeyConditionExpression: 'hk = :hk', - ExpressionAttributeValues: { ':hk': email } - } + // Query + const params = { + TableName: process.env.db, + KeyConditionExpression: `hk = :hk`, + ExpressionAttributeValues: { ':hk': email } + }; - let user = await dynamodb.query(params).promise() + let user = await dynamodb.query(params).promise(); - user = user.Items && user.Items[0] ? user.Items[0] : null - if (user) { - user.id = user.sk2 - user.email = user.hk - } - return user -} + user = user.Items && user.Items[0] ? user.Items[0] : null; + if (user) { + user.id = user.sk2; + user.email = user.hk; + } + return user; +}; /** * Get user by id * @param {string} id */ -const getById = async(id) => { +const getById = async (id) => { - // Validate - if (!id) { - throw new Error(`"id" is required`) - } + // Validate + if (!id) { + throw new Error(`"id" is required`); + } - // Query - const params = { - TableName: process.env.db, - IndexName: process.env.dbIndex1, - KeyConditionExpression: 'sk2 = :sk2 and sk = :sk', - ExpressionAttributeValues: { ':sk2': id, ':sk': 'user' } - } - let user = await dynamodb.query(params).promise() + // Query + const params = { + TableName: process.env.db, + IndexName: process.env.dbIndex1, + KeyConditionExpression: `sk2 = :sk2 and sk = :sk`, + ExpressionAttributeValues: { ':sk2': id, + ':sk': `user` } + }; + let user = await dynamodb.query(params).promise(); - user = user.Items && user.Items[0] ? user.Items[0] : null - if (user) { - user.id = user.sk2 - user.email = user.hk - } - return user -} + user = user.Items && user.Items[0] ? user.Items[0] : null; + if (user) { + user.id = user.sk2; + user.email = user.hk; + } + return user; +}; /** * Convert user record to public format * This hides the keys used for the dynamodb's single table design and returns human-readable properties. - * @param {*} user + * @param {*} user */ const convertToPublicFormat = (user = {}) => { - user.email = user.hk || null - user.id = user.sk2 || null - if (user.hk) delete user.hk - if (user.sk) delete user.sk - if (user.sk2) delete user.sk2 - if (user.password) delete user.password - return user -} + user.email = user.hk || null; + user.id = user.sk2 || null; + if (user.hk) delete user.hk; + if (user.sk) delete user.sk; + if (user.sk2) delete user.sk2; + if (user.password) delete user.password; + return user; +}; module.exports = { - register, - getByEmail, - getById, - convertToPublicFormat, -} + register, + getByEmail, + getById, + convertToPublicFormat, +}; diff --git a/api/package.json b/api/package.json index ea75df5..05fc0a9 100644 --- a/api/package.json +++ b/api/package.json @@ -1,5 +1,5 @@ { - "name": "serverless-fullstack-app-api", + "name": "forgetmenot-api", "version": "1.0.0", "description": "", "main": "app.js", @@ -11,8 +11,11 @@ "passport-jwt": "^4.0.0", "shortid": "^2.2.15" }, - "devDependencies": {}, + "devDependencies": { + "eslint": "^7.12.1" + }, "scripts": { + "pretest": "eslint --ignore-path ../.gitignore .", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", diff --git a/api/utils/db.js b/api/utils/db.js new file mode 100644 index 0000000..e69de29 diff --git a/api/utils/index.js b/api/utils/index.js index 12a7749..15e9d18 100644 --- a/api/utils/index.js +++ b/api/utils/index.js @@ -1,35 +1,35 @@ /** - * Utils + * Utils */ -const bcrypt = require('bcryptjs') +const bcrypt = require(`bcryptjs`); /** * Validate email address */ const validateEmailAddress = (email) => { - var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - return re.test(String(email).toLowerCase()) -} + var re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return re.test(String(email).toLowerCase()); +}; /** * Hash password - * @param {*} user + * @param {*} user */ const hashPassword = (password) => { - const salt = bcrypt.genSaltSync(10) - return bcrypt.hashSync(password, salt) -} + const salt = bcrypt.genSaltSync(10); + return bcrypt.hashSync(password, salt); +}; /** * Compare password */ const comparePassword = (candidatePassword, trustedPassword) => { - return bcrypt.compareSync(candidatePassword, trustedPassword) -} + return bcrypt.compareSync(candidatePassword, trustedPassword); +}; module.exports = { - hashPassword, - comparePassword, - validateEmailAddress -} \ No newline at end of file + hashPassword, + comparePassword, + validateEmailAddress +};