├── .env.example ├── src ├── index.js ├── utils │ ├── password.js │ ├── token.js │ ├── secrets.js │ └── logger.js ├── middlewares │ ├── asyncHandler.js │ ├── validatorHandler.js │ └── checkEmail.js ├── database │ ├── scripts │ │ ├── dbDown.js │ │ ├── dbUp.js │ │ └── tablesUp.js │ └── queries.js ├── config │ ├── db.config.init.js │ └── db.config.js ├── routes │ └── auth.route.js ├── app.js ├── validators │ └── auth.js ├── models │ └── user.model.js └── controllers │ └── auth.controller.js ├── package.json ├── .gitignore └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | DB_HOST=database-host 2 | DB_USER=database-user 3 | DB_PASS=database-password 4 | DB_NAME=database-name 5 | JWT_SECRET_KEY=jwtsecret 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | const { logger } = require('./utils/logger'); 3 | 4 | const PORT = process.env.PORT || 3000; 5 | 6 | app.listen(PORT, () => { 7 | logger.info(`Running on PORT ${PORT}`); 8 | }); 9 | -------------------------------------------------------------------------------- /src/utils/password.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcryptjs'); 2 | 3 | const hash = (password) => bcrypt.hashSync(password, bcrypt.genSaltSync(10)); 4 | 5 | const compare = (password, hashedPassword) => bcrypt.compareSync(password, hashedPassword); 6 | 7 | module.exports = { 8 | hash, 9 | compare 10 | } -------------------------------------------------------------------------------- /src/middlewares/asyncHandler.js: -------------------------------------------------------------------------------- 1 | const asyncHandler = (cb) => async (req, res, next) => { 2 | try { 3 | await cb(req, res, next); 4 | } catch (err) { 5 | return res.status(500).json({ 6 | status: 'error', 7 | message: err.message 8 | }); 9 | } 10 | return true; 11 | } 12 | 13 | module.exports = { 14 | asyncHandler 15 | } -------------------------------------------------------------------------------- /src/database/scripts/dbDown.js: -------------------------------------------------------------------------------- 1 | const { logger } = require('../../utils/logger'); 2 | const { dropDB: dropDBQuery } = require('../queries'); 3 | 4 | (() => { 5 | require('../../config/db.config.init').query(dropDBQuery, (err, _) => { 6 | if (err) { 7 | logger.error(err.message); 8 | return; 9 | } 10 | logger.info('DB Dropped!'); 11 | process.exit(0); 12 | }); 13 | })(); -------------------------------------------------------------------------------- /src/middlewares/validatorHandler.js: -------------------------------------------------------------------------------- 1 | 2 | const validatorHandler = (req, res, next, schema) => { 3 | const { error } = schema.validate(req.body); 4 | 5 | if (error) { 6 | res.status(400).json({ 7 | status: 'error', 8 | message: error.details[0].message.replace('/[^a-zA-Z0-9 ]/g', '') 9 | }); 10 | return; 11 | } 12 | next(); 13 | }; 14 | 15 | module.exports = validatorHandler; -------------------------------------------------------------------------------- /src/config/db.config.init.js: -------------------------------------------------------------------------------- 1 | const mysql = require('mysql'); 2 | const { logger } = require('../utils/logger'); 3 | const { DB_HOST, DB_USER, DB_PASS } = require('../utils/secrets'); 4 | 5 | const connection = mysql.createConnection({ 6 | host: DB_HOST, 7 | user: DB_USER, 8 | password: DB_PASS 9 | }); 10 | 11 | connection.connect((err) => { 12 | if (err) logger.error(err.message); 13 | }); 14 | 15 | module.exports = connection; -------------------------------------------------------------------------------- /src/database/scripts/dbUp.js: -------------------------------------------------------------------------------- 1 | const { logger } = require('../../utils/logger'); 2 | const { createDB: createDBQuery } = require('../queries'); 3 | 4 | (() => { 5 | require('../../config/db.config.init').query(createDBQuery, (err, _) => { 6 | if (err) { 7 | logger.error(err.message); 8 | return; 9 | } 10 | logger.info('DB created!'); 11 | process.exit(0); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/database/scripts/tablesUp.js: -------------------------------------------------------------------------------- 1 | const { logger } = require('../../utils/logger'); 2 | const { createTableUSers: createTableUSersQuery } = require('../queries'); 3 | 4 | (() => { 5 | require('../../config/db.config').query(createTableUSersQuery, (err, _) => { 6 | if (err) { 7 | logger.error(err.message); 8 | return; 9 | } 10 | logger.info('Table users created!'); 11 | process.exit(0); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/utils/token.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const { JWT_SECRET_KEY } = require('../utils/secrets'); 3 | const { logger } = require('./logger'); 4 | 5 | const generate = (id) => jwt.sign({ id }, JWT_SECRET_KEY, { expiresIn: '1d'}); 6 | 7 | const decode = (token) => { 8 | try { 9 | return jwt.verify(token, JWT_SECRET_KEY) 10 | } catch (error) { 11 | logger.error(error); 12 | } 13 | }; 14 | 15 | module.exports = { 16 | generate, 17 | decode 18 | } -------------------------------------------------------------------------------- /src/config/db.config.js: -------------------------------------------------------------------------------- 1 | const mysql = require('mysql'); 2 | const { logger } = require('../utils/logger'); 3 | const { DB_HOST, DB_USER, DB_PASS, DB_NAME } = require('../utils/secrets'); 4 | 5 | const connection = mysql.createConnection({ 6 | host: DB_HOST, 7 | user: DB_USER, 8 | password: DB_PASS, 9 | database: DB_NAME 10 | }); 11 | 12 | connection.connect((err) => { 13 | if (err) logger.error(err.message); 14 | else logger.info('Database connected') 15 | }); 16 | 17 | module.exports = connection; -------------------------------------------------------------------------------- /src/middlewares/checkEmail.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/user.model'); 2 | 3 | const checkEmail = (req, res, next) => { 4 | const { email } = req.body; 5 | User.findByEmail(email, (_, data) => { 6 | if (data) { 7 | res.status(400).send({ 8 | status: 'error', 9 | message: `A user with email address '${email}' already exits` 10 | }); 11 | return; 12 | } 13 | next(); 14 | }); 15 | } 16 | 17 | module.exports = checkEmail; -------------------------------------------------------------------------------- /src/routes/auth.route.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { asyncHandler } = require('../middlewares/asyncHandler'); 3 | const checkEmail = require('../middlewares/checkEmail'); 4 | const { signup: signupValidator, signin: signinValidator } = require('../validators/auth'); 5 | const authController = require('../controllers/auth.controller'); 6 | 7 | 8 | router.route('/signup') 9 | .post(signupValidator, asyncHandler(checkEmail), asyncHandler(authController.signup)); 10 | 11 | router.route('/signin') 12 | .post(signinValidator, asyncHandler(authController.signin)); 13 | 14 | module.exports = router; -------------------------------------------------------------------------------- /src/utils/secrets.js: -------------------------------------------------------------------------------- 1 | require('dotenv/config'); 2 | 3 | const { logger } = require('./logger'); 4 | 5 | const { 6 | DB_HOST, 7 | DB_USER, 8 | DB_PASS, 9 | DB_NAME, 10 | JWT_SECRET_KEY 11 | } = process.env; 12 | 13 | const requiredCredentials = [ 14 | 'DB_HOST', 15 | 'DB_USER', 16 | 'DB_PASS', 17 | 'DB_NAME', 18 | 'JWT_SECRET_KEY' 19 | ]; 20 | 21 | for (const credential of requiredCredentials) { 22 | if (process.env[credential] === undefined) { 23 | logger.error(`Missing required crendential: ${credential}`); 24 | process.exit(1); 25 | } 26 | } 27 | 28 | module.exports = { 29 | DB_HOST, 30 | DB_USER, 31 | DB_PASS, 32 | DB_NAME, 33 | JWT_SECRET_KEY 34 | }; 35 | -------------------------------------------------------------------------------- /src/database/queries.js: -------------------------------------------------------------------------------- 1 | const { DB_NAME } = require('../utils/secrets') 2 | 3 | const createDB = `CREATE DATABASE IF NOT EXISTS ${DB_NAME}`; 4 | 5 | const dropDB = `DROP DATABASE IF EXISTS ${DB_NAME}`; 6 | 7 | const createTableUSers = ` 8 | CREATE TABLE IF NOT EXISTS users ( 9 | id INT PRIMARY KEY AUTO_INCREMENT, 10 | firstname VARCHAR(50) NULL, 11 | lastname VARCHAR(50) NULL, 12 | email VARCHAR(255) NOT NULL UNIQUE, 13 | password VARCHAR(255) NOT NULL, 14 | created_on TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) 15 | ) 16 | `; 17 | 18 | const createNewUser = ` 19 | INSERT INTO users VALUES(null, ?, ?, ?, ?, NOW()) 20 | `; 21 | 22 | const findUserByEmail = ` 23 | SELECT * FROM users WHERE email = ? 24 | `; 25 | 26 | module.exports = { 27 | createDB, 28 | dropDB, 29 | createTableUSers, 30 | createNewUser, 31 | findUserByEmail 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth-node-mysql", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "node src/index.js", 8 | "start:dev": "nodemon", 9 | "db:up": "node src/database/scripts/dbUp.js", 10 | "db:down": "node src/database/scripts/dbDown.js", 11 | "tables:up": "node src/database/scripts/tablesUp.js", 12 | "db:init": "npm run db:up && npm run tables:up" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "bcryptjs": "^2.4.3", 19 | "cors": "^2.8.5", 20 | "dotenv": "^16.0.1", 21 | "express": "^4.18.1", 22 | "joi": "^17.6.0", 23 | "jsonwebtoken": "^8.5.1", 24 | "morgan": "^1.10.0", 25 | "mysql": "^2.18.1" 26 | }, 27 | "devDependencies": { 28 | "nodemon": "^2.0.16", 29 | "winston": "^3.7.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const morgan = require('morgan'); 3 | const cors = require('cors'); 4 | 5 | const authRoute = require('./routes/auth.route'); 6 | 7 | const { httpLogStream } = require('./utils/logger'); 8 | 9 | const app = express(); 10 | 11 | app.use(express.json()); 12 | app.use(express.urlencoded({ extended: false })); 13 | app.use(morgan('dev')); 14 | app.use(morgan('combined', { stream: httpLogStream })); 15 | app.use(cors()); 16 | 17 | app.use('/api/auth', authRoute); 18 | 19 | app.get('/', (req, res) => { 20 | res.status(200).send({ 21 | status: "success", 22 | data: { 23 | message: "API working fine" 24 | } 25 | }); 26 | }); 27 | 28 | app.use((err, req, res, next) => { 29 | res.status(err.statusCode || 500).send({ 30 | status: "error", 31 | message: err.message 32 | }); 33 | next(); 34 | }); 35 | 36 | module.exports = app; -------------------------------------------------------------------------------- /src/validators/auth.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const validatorHandler = require('../middlewares/validatorHandler'); 3 | 4 | const signup = (req, res, next) => { 5 | const schema = Joi.object().keys({ 6 | firstname: Joi.string() 7 | .trim() 8 | .alphanum() 9 | .min(3) 10 | .max(50) 11 | .required(), 12 | lastname: Joi.string() 13 | .trim() 14 | .alphanum() 15 | .min(3) 16 | .max(50) 17 | .required(), 18 | email: Joi.string() 19 | .trim() 20 | .email() 21 | .required(), 22 | password: Joi.string() 23 | .trim() 24 | .pattern(new RegExp('^[a-zA-Z0-9]{6,30}$')) 25 | .required() 26 | }); 27 | validatorHandler(req, res, next, schema); 28 | }; 29 | 30 | const signin = (req, res, next) => { 31 | const schema = Joi.object().keys({ 32 | email: Joi.string() 33 | .trim() 34 | .email() 35 | .required(), 36 | password: Joi.string() 37 | .trim() 38 | .pattern(new RegExp('^[a-zA-Z0-9]{6,30}$')) 39 | .required() 40 | }); 41 | validatorHandler(req, res, next, schema); 42 | }; 43 | 44 | module.exports = { 45 | signup, 46 | signin 47 | }; -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | require('dotenv/config'); 5 | 6 | const levels = { 7 | error: 0, 8 | warn: 1, 9 | info: 2, 10 | http: 3, 11 | debug: 4 12 | }; 13 | 14 | const colors = { 15 | error: 'red', 16 | warn: 'yellow', 17 | info: 'green', 18 | http: 'magenta', 19 | debug: 'orange' 20 | }; 21 | 22 | winston.addColors(colors); 23 | 24 | const transports = [ 25 | new winston.transports.File({ 26 | filename: 'logs/errors.log', 27 | level: 'error', 28 | }), 29 | new winston.transports.File({ 30 | filename: 'logs/combined.log' 31 | }) 32 | ]; 33 | 34 | const logger = winston.createLogger({ 35 | level: 'info', 36 | levels, 37 | format: winston.format.json(), 38 | transports 39 | }); 40 | 41 | if (process.env.NODE_ENV !== 'production') { 42 | logger.add( 43 | new winston.transports.Console({ 44 | format: winston.format.combine( 45 | winston.format.colorize(), 46 | winston.format.simple() 47 | ) 48 | }) 49 | ); 50 | } 51 | 52 | const httpLogStream = fs.createWriteStream(path.join(__dirname, '../../', 'logs', 'http_logs.log')); 53 | 54 | module.exports = { 55 | httpLogStream, 56 | logger 57 | } 58 | -------------------------------------------------------------------------------- /src/models/user.model.js: -------------------------------------------------------------------------------- 1 | const db = require('../config/db.config'); 2 | const { createNewUser: createNewUserQuery, findUserByEmail: findUserByEmailQuery } = require('../database/queries'); 3 | const { logger } = require('../utils/logger'); 4 | 5 | class User { 6 | constructor(firstname, lastname, email, password) { 7 | this.firstname = firstname; 8 | this.lastname = lastname; 9 | this.email = email; 10 | this.password = password; 11 | } 12 | 13 | static create(newUser, cb) { 14 | db.query(createNewUserQuery, 15 | [ 16 | newUser.firstname, 17 | newUser.lastname, 18 | newUser.email, 19 | newUser.password 20 | ], (err, res) => { 21 | if (err) { 22 | logger.error(err.message); 23 | cb(err, null); 24 | return; 25 | } 26 | cb(null, { 27 | id: res.insertId, 28 | firstname: newUser.firstname, 29 | lastname: newUser.lastname, 30 | email: newUser.email 31 | }); 32 | }); 33 | } 34 | 35 | static findByEmail(email, cb) { 36 | db.query(findUserByEmailQuery, email, (err, res) => { 37 | if (err) { 38 | logger.error(err.message); 39 | cb(err, null); 40 | return; 41 | } 42 | if (res.length) { 43 | cb(null, res[0]); 44 | return; 45 | } 46 | cb({ kind: "not_found" }, null); 47 | }) 48 | } 49 | } 50 | 51 | module.exports = User; -------------------------------------------------------------------------------- /src/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/user.model'); 2 | const { hash: hashPassword, compare: comparePassword } = require('../utils/password'); 3 | const { generate: generateToken } = require('../utils/token'); 4 | 5 | exports.signup = (req, res) => { 6 | const { firstname, lastname, email, password } = req.body; 7 | const hashedPassword = hashPassword(password.trim()); 8 | 9 | const user = new User(firstname.trim(), lastname.trim(), email.trim(), hashedPassword); 10 | 11 | User.create(user, (err, data) => { 12 | if (err) { 13 | res.status(500).send({ 14 | status: "error", 15 | message: err.message 16 | }); 17 | } else { 18 | const token = generateToken(data.id); 19 | res.status(201).send({ 20 | status: "success", 21 | data: { 22 | token, 23 | data 24 | } 25 | }); 26 | } 27 | }); 28 | }; 29 | 30 | exports.signin = (req, res) => { 31 | const { email, password } = req.body; 32 | User.findByEmail(email.trim(), (err, data) => { 33 | if (err) { 34 | if (err.kind === "not_found") { 35 | res.status(404).send({ 36 | status: 'error', 37 | message: `User with email ${email} was not found` 38 | }); 39 | return; 40 | } 41 | res.status(500).send({ 42 | status: 'error', 43 | message: err.message 44 | }); 45 | return; 46 | } 47 | if (data) { 48 | if (comparePassword(password.trim(), data.password)) { 49 | const token = generateToken(data.id); 50 | res.status(200).send({ 51 | status: 'success', 52 | data: { 53 | token, 54 | firstname: data.firstname, 55 | lastname: data.lastname, 56 | email: data.email 57 | } 58 | }); 59 | return; 60 | } 61 | res.status(401).send({ 62 | status: 'error', 63 | message: 'Incorrect password' 64 | }); 65 | } 66 | }); 67 | 68 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # OS 107 | .DS_Store 108 | 109 | # IDEs and editors 110 | /.idea 111 | .project 112 | .classpath 113 | .c9/ 114 | *.launch 115 | .settings/ 116 | *.sublime-workspace 117 | 118 | # IDE - VSCode 119 | .vscode/* 120 | !.vscode/settings.json 121 | !.vscode/tasks.json 122 | !.vscode/launch.json 123 | !.vscode/extensions.json 124 | 125 | **/.DS_Store 126 | *.log.json 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeJS Auth REST API example with Express, Mysql, and JWT 2 | 3 | ## Features 4 | 1. User can sign up 5 | 2. User can sign in 6 | 7 | ## API endpoints 8 | 9 | 1. `POST /api/auth/signup`: Creates a new user 10 | 2. `POST /api/auth/signin`: Logs in a user 11 | 12 | ## Body Payload Specification 13 | Signup expects 14 | 15 | ```js 16 | { 17 | firstname: string, 18 | lastname: string, 19 | email: string, 20 | password: string 21 | } 22 | ``` 23 | 24 | Signin expects 25 | 26 | ```js 27 | { 28 | email: string, 29 | password: string 30 | } 31 | ``` 32 | ## Tools 33 | * NodeJS/Express: Server 34 | * MySQL: Storage 35 | * JWT: Token based authentication 36 | * bcryptjs: Password security 37 | * winston/morgan: Logs 38 | * Joi: Validations 39 | 40 | ## Available scripts 41 | * `start`: Starts the server with node 42 | * `start:dev`: Starts the server in watch mode 43 | * `db:up`: Creates the database 44 | * `db:down`: Drops the database 45 | * `tables:up`: Creates database tables 46 | * `db:init`: Creates both the database and tables 47 | 48 | ## Getting started 49 | 50 | You can either fork this repository or clone it by starting your terminal, then change the directory to where you would like to save it and run 51 | 52 | ```sh 53 | git clone https://github.com/desirekaleba/node-mysql-jwt-auth.git 54 | ``` 55 | Change to the newly downloaded directory with 56 | 57 | ```sh 58 | cd node-mysql-jwt-auth 59 | ``` 60 | 61 | Rename the file named `.env.example` to `.env` and update the variable values with valid ones 62 | 63 | Install the required dependencies with 64 | 65 | ```sh 66 | npm install 67 | ``` 68 | 69 | Initialize the database with 70 | 71 | ```sh 72 | npm run db:init 73 | ``` 74 | 75 | Start the app with 76 | 77 | ```sh 78 | npm start 79 | ``` 80 | 81 | You can also start it in watch mode with 82 | 83 | ```sh 84 | npm run start:dev 85 | ``` 86 | 87 | ## Folder structure 88 | ```sh 89 | . 90 | ├── README.md 91 | ├── package-lock.json 92 | ├── package.json 93 | └── src 94 | ├── app.js 95 | ├── config 96 | │ ├── db.config.init.js 97 | │ └── db.config.js 98 | ├── controllers 99 | │ └── auth.controller.js 100 | ├── database 101 | │ ├── queries.js 102 | │ └── scripts 103 | │ ├── dbDown.js 104 | │ ├── dbUp.js 105 | │ └── tablesUp.js 106 | ├── index.js 107 | ├── middlewares 108 | │ ├── asyncHandler.js 109 | │ ├── checkEmail.js 110 | │ └── validatorHandler.js 111 | ├── models 112 | │ └── user.model.js 113 | ├── routes 114 | │ └── auth.route.js 115 | ├── utils 116 | │ ├── logger.js 117 | │ ├── password.js 118 | │ ├── secrets.js 119 | │ └── token.js 120 | └── validators 121 | └── auth.js 122 | ``` --------------------------------------------------------------------------------