├── .travis.yml ├── source ├── interfaces │ └── user.ts ├── routes │ └── user.ts ├── models │ └── user.ts ├── middleware │ └── extractJWT.ts ├── functions │ └── signJTW.ts ├── config │ ├── logging.ts │ └── config.ts ├── server.ts └── controllers │ └── user.ts ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── package.json └── tsconfig.json /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 -------------------------------------------------------------------------------- /source/interfaces/user.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose'; 2 | 3 | export default interface IUser extends Document { 4 | username: string; 5 | password: string; 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | build/**/* 3 | certs/**/* 4 | build/**/* 5 | 6 | *.jks 7 | *.p8 8 | *.p12 9 | *.key 10 | *.mobileprovision 11 | *.orig.* 12 | 13 | package-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 200, 4 | "proseWrap": "always", 5 | "tabWidth": 4, 6 | "useTabs": false, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "semi": true 11 | } -------------------------------------------------------------------------------- /source/routes/user.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import controller from '../controllers/user'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/validate', extractJWT, controller.validateToken); 7 | router.post('/register', controller.register); 8 | router.post('/login', controller.login); 9 | router.get('/get/all', controller.getAllUsers); 10 | 11 | export = router; 12 | -------------------------------------------------------------------------------- /source/models/user.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import IUser from '../interfaces/user'; 3 | 4 | const UserSchema: Schema = new Schema( 5 | { 6 | username: { type: String, required: true, unique: true }, 7 | password: { type: String, required: true } 8 | }, 9 | { 10 | timestamps: true 11 | } 12 | ); 13 | 14 | export default mongoose.model('User', UserSchema); 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "[javascript]": { 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | }, 6 | "[typescript]": { 7 | "editor.formatOnSave": true, 8 | "editor.formatOnPaste": true 9 | }, 10 | "[markdown]": { 11 | "editor.formatOnSave": true, 12 | "editor.wordWrap": "on", 13 | "editor.renderWhitespace": "all", 14 | "editor.acceptSuggestionOnEnter": "off" 15 | } 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "source/server.ts", 6 | "scripts": { 7 | "build": "rm -rf build && prettier --write source/ && tsc" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "bcryptjs": "^2.4.3", 13 | "body-parser": "^1.19.0", 14 | "dotenv": "^8.2.0", 15 | "express": "^4.17.1", 16 | "jsonwebtoken": "^8.5.1", 17 | "mongoose": "^5.10.15" 18 | }, 19 | "devDependencies": { 20 | "@types/bcryptjs": "^2.4.2", 21 | "@types/body-parser": "^1.19.0", 22 | "@types/dotenv": "^8.2.0", 23 | "@types/express": "^4.17.8", 24 | "@types/jsonwebtoken": "^8.5.0", 25 | "@types/mongoose": "^5.10.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /source/middleware/extractJWT.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import config from '../config/config'; 3 | import logging from '../config/logging'; 4 | import { Request, Response, NextFunction } from 'express'; 5 | 6 | const NAMESPACE = 'Auth'; 7 | 8 | const extractJWT = (req: Request, res: Response, next: NextFunction) => { 9 | logging.info(NAMESPACE, 'Validating token'); 10 | 11 | let token = req.headers.authorization?.split(' ')[1]; 12 | 13 | if (token) { 14 | jwt.verify(token, config.server.token.secret, (error, decoded) => { 15 | if (error) { 16 | return res.status(404).json({ 17 | message: error, 18 | error 19 | }); 20 | } else { 21 | res.locals.jwt = decoded; 22 | next(); 23 | } 24 | }); 25 | } else { 26 | return res.status(401).json({ 27 | message: 'Unauthorized' 28 | }); 29 | } 30 | }; 31 | 32 | export default extractJWT; 33 | -------------------------------------------------------------------------------- /source/functions/signJTW.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import config from '../config/config'; 3 | import logging from '../config/logging'; 4 | import IUser from '../interfaces/user'; 5 | 6 | const NAMESPACE = 'Auth'; 7 | 8 | const signJWT = (user: IUser, callback: (error: Error | null, token: string | null) => void): void => { 9 | var timeSinceEpoch = new Date().getTime(); 10 | var expirationTime = timeSinceEpoch + Number(config.server.token.expireTime) * 100000; 11 | var expirationTimeInSeconds = Math.floor(expirationTime / 1000); 12 | 13 | logging.info(NAMESPACE, `Attempting to sign token for ${user._id}`); 14 | 15 | try { 16 | jwt.sign( 17 | { 18 | username: user.username 19 | }, 20 | config.server.token.secret, 21 | { 22 | issuer: config.server.token.issuer, 23 | algorithm: 'HS256', 24 | expiresIn: expirationTimeInSeconds 25 | }, 26 | (error, token) => { 27 | if (error) { 28 | callback(error, null); 29 | } else if (token) { 30 | callback(null, token); 31 | } 32 | } 33 | ); 34 | } catch (error) { 35 | logging.error(NAMESPACE, error.message, error); 36 | callback(error, null); 37 | } 38 | }; 39 | 40 | export default signJWT; 41 | -------------------------------------------------------------------------------- /source/config/logging.ts: -------------------------------------------------------------------------------- 1 | const info = (namespace: string, message: string, object?: any) => { 2 | if (object) { 3 | console.info(`[${getTimeStamp()}] [INFO] [${namespace}] ${message}`, object); 4 | } else { 5 | console.info(`[${getTimeStamp()}] [INFO] [${namespace}] ${message}`); 6 | } 7 | }; 8 | 9 | const warn = (namespace: string, message: string, object?: any) => { 10 | if (object) { 11 | console.warn(`[${getTimeStamp()}] [WARN] [${namespace}] ${message}`, object); 12 | } else { 13 | console.warn(`[${getTimeStamp()}] [WARN] [${namespace}] ${message}`); 14 | } 15 | }; 16 | 17 | const error = (namespace: string, message: string, object?: any) => { 18 | if (object) { 19 | console.error(`[${getTimeStamp()}] [ERROR] [${namespace}] ${message}`, object); 20 | } else { 21 | console.error(`[${getTimeStamp()}] [ERROR] [${namespace}] ${message}`); 22 | } 23 | }; 24 | 25 | const debug = (namespace: string, message: string, object?: any) => { 26 | if (object) { 27 | console.debug(`[${getTimeStamp()}] [DEBUG] [${namespace}] ${message}`, object); 28 | } else { 29 | console.debug(`[${getTimeStamp()}] [DEBUG] [${namespace}] ${message}`); 30 | } 31 | }; 32 | 33 | const getTimeStamp = (): string => { 34 | return new Date().toISOString(); 35 | }; 36 | 37 | export default { 38 | info, 39 | warn, 40 | error, 41 | debug 42 | }; 43 | -------------------------------------------------------------------------------- /source/config/config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | dotenv.config(); 4 | 5 | const MONGO_OPTIONS = { 6 | useUnifiedTopology: true, 7 | useNewUrlParser: true, 8 | socketTimeoutMS: 30000, 9 | keepAlive: true, 10 | poolSize: 50, 11 | autoIndex: false, 12 | retryWrites: true 13 | }; 14 | 15 | const MONGO_USERNAME = process.env.MONGO_USERNAME || 'superuser'; 16 | const MONGO_PASSWORD = process.env.MONGO_USERNAME || 'supersecretpassword1'; 17 | const MONGO_HOST = process.env.MONGO_URL || `cluster0.menvh.mongodb.net/sample?w=majority`; 18 | 19 | const MONGO = { 20 | host: MONGO_HOST, 21 | password: MONGO_PASSWORD, 22 | username: MONGO_USERNAME, 23 | options: MONGO_OPTIONS, 24 | url: `mongodb+srv://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOST}` 25 | }; 26 | 27 | const SERVER_HOSTNAME = process.env.SERVER_HOSTNAME || 'localhost'; 28 | const SERVER_PORT = process.env.SERVER_PORT || 1337; 29 | const SERVER_TOKEN_EXPIRETIME = process.env.SERVER_TOKEN_EXPIRETIME || 3600; 30 | const SERVER_TOKEN_ISSUER = process.env.SERVER_TOKEN_ISSUER || 'coolIssuer'; 31 | const SERVER_TOKEN_SECRET = process.env.SERVER_TOKEN_SECRET || 'superencryptedsecret'; 32 | 33 | const SERVER = { 34 | hostname: SERVER_HOSTNAME, 35 | port: SERVER_PORT, 36 | token: { 37 | expireTime: SERVER_TOKEN_EXPIRETIME, 38 | issuer: SERVER_TOKEN_ISSUER, 39 | secret: SERVER_TOKEN_SECRET 40 | } 41 | }; 42 | 43 | const config = { 44 | mongo: MONGO, 45 | server: SERVER 46 | }; 47 | 48 | export default config; 49 | -------------------------------------------------------------------------------- /source/server.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import bodyParser from 'body-parser'; 3 | import express from 'express'; 4 | import logging from './config/logging'; 5 | import config from './config/config'; 6 | import userRoutes from './routes/user'; 7 | import mongoose from 'mongoose'; 8 | 9 | const NAMESPACE = 'Server'; 10 | const router = express(); 11 | 12 | /** Connect to Mongo */ 13 | mongoose 14 | .connect(config.mongo.url, config.mongo.options) 15 | .then((result) => { 16 | logging.info(NAMESPACE, 'Mongo Connected'); 17 | }) 18 | .catch((error) => { 19 | logging.error(NAMESPACE, error.message, error); 20 | }); 21 | 22 | /** Log the request */ 23 | router.use((req, res, next) => { 24 | /** Log the req */ 25 | logging.info(NAMESPACE, `METHOD: [${req.method}] - URL: [${req.url}] - IP: [${req.socket.remoteAddress}]`); 26 | 27 | res.on('finish', () => { 28 | /** Log the res */ 29 | logging.info(NAMESPACE, `METHOD: [${req.method}] - URL: [${req.url}] - STATUS: [${res.statusCode}] - IP: [${req.socket.remoteAddress}]`); 30 | }); 31 | 32 | next(); 33 | }); 34 | 35 | /** Parse the body of the request */ 36 | router.use(bodyParser.urlencoded({ extended: true })); 37 | router.use(bodyParser.json()); 38 | 39 | /** Rules of our API */ 40 | router.use((req, res, next) => { 41 | res.header('Access-Control-Allow-Origin', '*'); 42 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); 43 | 44 | if (req.method == 'OPTIONS') { 45 | res.header('Access-Control-Allow-Methods', 'PUT, POST, PATCH, DELETE, GET'); 46 | return res.status(200).json({}); 47 | } 48 | 49 | next(); 50 | }); 51 | 52 | /** Routes go here */ 53 | router.use('/users', userRoutes); 54 | 55 | /** Error handling */ 56 | router.use((req, res, next) => { 57 | const error = new Error('Not found'); 58 | 59 | res.status(404).json({ 60 | message: error.message 61 | }); 62 | }); 63 | 64 | const httpServer = http.createServer(router); 65 | 66 | httpServer.listen(config.server.port, () => logging.info(NAMESPACE, `Server is running ${config.server.hostname}:${config.server.port}`)); 67 | -------------------------------------------------------------------------------- /source/controllers/user.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import mongoose from 'mongoose'; 3 | import bcryptjs from 'bcryptjs'; 4 | import logging from '../config/logging'; 5 | import User from '../models/user'; 6 | import signJWT from '../functions/signJTW'; 7 | 8 | const NAMESPACE = 'User'; 9 | 10 | const validateToken = (req: Request, res: Response, next: NextFunction) => { 11 | logging.info(NAMESPACE, 'Token validated, user authorized.'); 12 | 13 | return res.status(200).json({ 14 | message: 'Token(s) validated' 15 | }); 16 | }; 17 | 18 | const register = (req: Request, res: Response, next: NextFunction) => { 19 | let { username, password } = req.body; 20 | 21 | bcryptjs.hash(password, 10, (hashError, hash) => { 22 | if (hashError) { 23 | return res.status(401).json({ 24 | message: hashError.message, 25 | error: hashError 26 | }); 27 | } 28 | 29 | const _user = new User({ 30 | _id: new mongoose.Types.ObjectId(), 31 | username, 32 | password: hash 33 | }); 34 | 35 | return _user 36 | .save() 37 | .then((user) => { 38 | return res.status(201).json({ 39 | user 40 | }); 41 | }) 42 | .catch((error) => { 43 | return res.status(500).json({ 44 | message: error.message, 45 | error 46 | }); 47 | }); 48 | }); 49 | }; 50 | 51 | const login = (req: Request, res: Response, next: NextFunction) => { 52 | let { username, password } = req.body; 53 | 54 | User.find({ username }) 55 | .exec() 56 | .then((users) => { 57 | if (users.length !== 1) { 58 | return res.status(401).json({ 59 | message: 'Unauthorized' 60 | }); 61 | } 62 | 63 | bcryptjs.compare(password, users[0].password, (error, result) => { 64 | if (error) { 65 | return res.status(401).json({ 66 | message: 'Password Mismatch' 67 | }); 68 | } else if (result) { 69 | signJWT(users[0], (_error, token) => { 70 | if (_error) { 71 | return res.status(500).json({ 72 | message: _error.message, 73 | error: _error 74 | }); 75 | } else if (token) { 76 | return res.status(200).json({ 77 | message: 'Auth successful', 78 | token: token, 79 | user: users[0] 80 | }); 81 | } 82 | }); 83 | } 84 | }); 85 | }) 86 | .catch((err) => { 87 | console.log(err); 88 | res.status(500).json({ 89 | error: err 90 | }); 91 | }); 92 | }; 93 | 94 | const getAllUsers = (req: Request, res: Response, next: NextFunction) => { 95 | User.find() 96 | .select('-password') 97 | .exec() 98 | .then((users) => { 99 | return res.status(200).json({ 100 | users: users, 101 | count: users.length 102 | }); 103 | }) 104 | .catch((error) => { 105 | return res.status(500).json({ 106 | message: error.message, 107 | error 108 | }); 109 | }); 110 | }; 111 | 112 | export default { validateToken, register, login, getAllUsers }; 113 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./build", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | } 69 | } 70 | --------------------------------------------------------------------------------