added new code

This commit is contained in:
adhamhaddad 2023-04-25 00:30:11 +02:00
parent f8baeba66d
commit ff1f6bf560
53 changed files with 3477 additions and 6 deletions

25
.env Normal file
View File

@ -0,0 +1,25 @@
ENV=dev
HOST=localhost
PORT=3000
# DATABASE
POSTGRES_URI=localhost
POSTGRES_PORT=5432
POSTGRES_DB=authentication
POSTGRES_DB_TEST=authentication_test
POSTGRES_USER=admin
POSTGRES_PASSWORD=admin
# BCRYPT
SECRET_PEPPER=github@adhamhaddad
SALT_ROUNDS=10
# JWT
JWT_SECRET_ACCESS_TOKEN=
JWT_SECRET_REFRESH_TOKEN=
JWT_ACCESS_TOKEN_EXPIRATION=5m
JWT_REFRESH_TOKEN_EXPIRATION=1y
# URL
BACKEND_HOST=http://127.0.0.1:8000
FRONTEND_HOST=http://127.0.0.1:3000

25
.eslintrc.js Normal file
View File

@ -0,0 +1,25 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 13,
sourceType: 'module',
},
plugins: ['@typescript-eslint', 'prettier'],
rules: {
'prettier/prettier': 1,
quotes: ['error', 'single'],
'no-console': 0,
'no-var': 'error',
'prefer-const': 'error',
},
};

6
.gitignore vendored
View File

@ -72,12 +72,6 @@ web_modules/
# Yarn Integrity file # Yarn Integrity file
.yarn-integrity .yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/) # parcel-bundler cache (https://parceljs.org/)
.cache .cache

20
.prettierrc.json Normal file
View File

@ -0,0 +1,20 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"singleAttributePerLine": false,
"bracketSameLine": false,
"jsxSingleQuote": true,
"printWidth": 80,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"vueIndentScriptAndStyle": false,
"trailingComma" : "none"
}

View File

@ -1,2 +1,37 @@
# Nodejs-Refresh-Token # Nodejs-Refresh-Token
## Description
Full Authentication & Authorization using nodejs, bcrypt, jsonwebtoken, pg thats integrate accessToken, refreshToken with advanced way Full Authentication & Authorization using nodejs, bcrypt, jsonwebtoken, pg thats integrate accessToken, refreshToken with advanced way
## Dependencies
- Node v14.15.1 (LTS) or more recent. While older versions can work it is advisable to keep node to latest LTS version
- npm 6.14.8 (LTS) or more recent, Yarn can work but was not tested for this project
## Installation
### Database setup
1. Open postgres terminal with: `psql postgres`
1- `CREATE DATABASE authentication;`
2- `CREATE ROLE admin WITH PASSWORD 'admin';`
3- `ALTER ROLE admin WITH SUPERUSER CREATEROLE CREATEDB LOGIN;`
4- `GRANT ALL PRIVILEGES ON DATABASE authentication TO admin;`
2. Second to install the node_modules run `npm install` or `yarn`. After installation is done start the api in dev mode with `npm run dev` or `yarn dev`.
## Unit Tests
No Unit test available now.
## Built With
- [Node](https://nodejs.org) - Javascript Runtime
- [Express](https://expressjs.com/) - Javascript API Framework
- [PostgreSQL](https://www.postgresql.org/) - Open Source Relational Database

18
database.json Normal file
View File

@ -0,0 +1,18 @@
{
"dev": {
"driver": "pg",
"host": "localhost",
"port": 5432,
"database": "authentication",
"user": "admin",
"password": "admin"
},
"test": {
"driver": "pg",
"host": "localhost",
"port": 5432,
"database": "authentication_test",
"user": "admin",
"password": "admin"
}
}

View File

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0RBJ7/i1GKYguWdey1Pz
an/gFtbUIa7z/TJWphZFEXpj1BeAoNVLQgJVsPZ/GHqvPWal5vhdAbZEAK/Yf7FG
wpcYzEXNmecrFDjYEhb5bs6GfpEvdjZxkZ+eM82XYDRmK9WgQmuqUiwcMlOkbcVw
2VHflnBq0x0zYXTBO4CVMyLWvvmi25+c25AOSRq+QhhFQFHz2xD1gnBv1sgo7SVk
P9RpaH454T4NTZ6C9loff/N8LN+AAous9FAZ/3PC9hicr5jkXI7w9x0wzfZkcEpC
jFk1deyZwcJ0U86ujd2J5uDuBzhcPBO45mvfDs0VZ39wpgxQlmSUqFpFOrejjU1L
Oxjdd6iCkA9NSow98A/F/AbSKHbVmjr1WIv1lrgjO6OldQiHv19GoDxTxTGhVLqQ
Sn0YXmOk1Zk8O0kuZt5cfkF1kuxQUBV/AKCKkNsWH6grBCdqIB7xNoUXK5EOiSMH
wx1vA9LhIi1cCOxUBhgCpXMla8VjEbOnU8NIRGoATdodKNaDTxib1N7XpkofSlpK
Px7YfGK5t/z1ODDpQ1vvHIb7cXJW+rnFvOZXt62m5HDdGyQW9Qd9yhwd6qL0eurd
4JaU78OfG2ussEi7M3qV12yiEW7V1Gpr7XjOf/GBx3GhW7oHqKOZKCanZv5xhliv
5rkLSnnmcLHxm/m0pGTr96kCAwEAAQ==
-----END PUBLIC KEY-----

View File

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
HRQBAoIBAQC6zs1cEhJpQqaCPz9ib/7KIf2eoXVq0x8zixoL4YHYL2nxV5GyhAl0
IaPOkuyZTdkKnVXSk3GSLmhDNA3mfHovjV3AoBEhmw3D+6b/n2irSgZeXhWZKYrI
e4NSobKVVf2ier9bO3UuQi8TZjvzdq04lMkDCTOKljTKvzOJsfnlb/4zidHNEngO
yrs7a0b6ytmJkvjw/HqVVxQ5CtNOjCcEcUflcoDvovbF0+zYLGn9U047Qsdr17kG
6Y4c5D854534rWTb7RXLzD6O83xpl97J3vYMU4tz5NiyETOd7UR88v/bhUrLkJnL
gUEf0ZZ0/hrfUWfx8NvV8OQEv2CsPtHnAoIBAA9DS48xnG83uv5hcKS077M4j7gk
B/QvPQ559hb2c0MJKDdmjqIHpgyPUaJ4QnQIfHS281r3/ryxi0znBUKBAPfjqzdT
jqjvTZFiMijYAJNzva3oAgRxtTcx6EDOGsIc8TDxGAyz96r4o69czpnDU+7fwwIc
RKBt1HlXdRxE4sBzvYfynEBpUb8kVNRJS3JucK+1VkCaLXExI+XlcsGwI9m0W5/s
qiKSiECCT6lo8w+chBnoiHNbhqwK5H9V8B8lij/J2Ip6HIX8gOWaS7jYLEmtmxUB
Ddh0cp6Y7NbZ2/NCXJg7tWsrN7h3oGmEaa115pzN+vEmnDe5M8ms36DQ9rQ=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsWgVuFLjnxUgUbXn7ayy
PfeAatQ9q8Pn9ogA9gs+L7RwixKo3ijFuKl/GireTZ2xMXVPPposLF/g9B4Gkq+G
RluL4eNRV49W+lQQl57fUfuUBlsmCEAlXRbhNnjXCeKc/bwH3J1uIrBGqqTdqAmd
QUZCOHreUp1k9LzU00CYezA6OkaqfzQbNUb7IqO+xI/frJf5yr9r2NlggXvc+phY
U4qQefHzSoIPZgq13M4KYMs3N5LjaHi1BgmBDFr39nzPsZIYe6vvVPoVfpmuVIa0
/ICfjuqDPoG8UbQrdJXOEy/CkF82kTedIdG+XsKL+xCjdq04k0lJOs3ePFqFdOsJ
C/ZIzYVPGyS+wfanxsyQqnblf/75NR+XNcpOm58OXT9WyDRd4E4OTRxyDKsF3XGy
m49/sd0n3pFOmWshOqPySowIUUgLIj5N2+CNCM+QOlnE5mMnQQEWNFENRGcgDl4U
DUm19gEzmqP9Relo//u5FLpC11T0lnbLHsVXRQD602onDX6YMY7vijyPNau1bFBj
8Dvlkbn2V46aagVW9Io24DJiaT0RCyYr9GsxTLiiyTg/w94CZZYfI8MsveXK9RNW
KxrGyCOaiDdQC1q5aAyS44Xx6dWG/Oow2p528Rq6DMugQ1GTZ7guOVWosjGHQS/5
S8HRlCMuN4xNjabYilEu+wMCAwEAAQ==
-----END PUBLIC KEY-----

View File

@ -0,0 +1,53 @@
'use strict';
var dbm;
var type;
var seed;
var fs = require('fs');
var path = require('path');
var Promise;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function(options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
Promise = options.Promise;
};
exports.up = function(db) {
var filePath = path.join(__dirname, 'sqls', '20230424202313-users-table-up.sql');
return new Promise( function( resolve, reject ) {
fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){
if (err) return reject(err);
console.log('received data: ' + data);
resolve(data);
});
})
.then(function(data) {
return db.runSql(data);
});
};
exports.down = function(db) {
var filePath = path.join(__dirname, 'sqls', '20230424202313-users-table-down.sql');
return new Promise( function( resolve, reject ) {
fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){
if (err) return reject(err);
console.log('received data: ' + data);
resolve(data);
});
})
.then(function(data) {
return db.runSql(data);
});
};
exports._meta = {
"version": 1
};

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS users;

View File

@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
username VARCHAR(100) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at DATE,
deleted_at DATE
);

53
package.json Normal file
View File

@ -0,0 +1,53 @@
{
"dependencies": {
"bcrypt": "^5.1.0",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-validator": "^6.14.2",
"helmet": "^6.1.5",
"jsonwebtoken": "^9.0.0",
"morgan": "^1.10.0",
"pg": "^8.10.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/cors": "^2.8.13",
"@types/dotenv": "^8.2.0",
"@types/eslint-config-prettier": "^6.11.0",
"@types/eslint-plugin-prettier": "^3.1.0",
"@types/express": "^4.17.17",
"@types/express-validator": "^3.0.0",
"@types/helmet": "^4.0.0",
"@types/jasmine": "^4.3.1",
"@types/jsonwebtoken": "^9.0.1",
"@types/morgan": "^1.9.4",
"@types/node": "^18.16.0",
"@types/nodemon": "^1.19.2",
"@types/pg": "^8.6.6",
"@types/prettier": "^2.7.2",
"@types/typescript": "^2.0.0",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"jasmine": "^4.6.0",
"jasmine-spec-reporter": "^7.0.0",
"nodemon": "^2.0.22",
"prettier": "^2.8.8",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
},
"scripts": {
"prettier": "prettier --write src/**/*.ts && prettier --write src/*.ts",
"lint": "eslint --fix --ext .ts .",
"format": "npm run prettier && npm run lint",
"build": "tsc",
"jasmine": "jasmine",
"migrate:up": "db-migrate up",
"migrate:down": "db-migrate up",
"migrate:reset": "db-migrate reset",
"test": "export ENV=test && tsc && db-migrate up --env test && jasmine && db-migrate reset --env test",
"start": "tsc && node dist/server.ts",
"dev": "nodemon src/server.ts"
}
}

View File

@ -0,0 +1,7 @@
{
"spec_dir": "dist",
"spec_files": ["**/**/*[sS]pec.js"],
"helpers": ["helpers/**/*.js"],
"stopSpecOnExpectationFailure": false,
"random": false
}

52
sql/schema.sql Normal file
View File

@ -0,0 +1,52 @@
SET client_min_messages = warning;
-- -------------------------
-- Database authentication
-- -------------------------
DROP DATABASE IF EXISTS authentication;
--
--
CREATE DATABASE authentication;
-- -------------------------
-- Database authentication_test
-- -------------------------
DROP DATABASE IF EXISTS authentication_test;
--
--
CREATE DATABASE authentication_test;
-- -------------------------
-- Role admin
-- -------------------------
-- DROP ROLE IF EXISTS admin;
--
--
-- CREATE ROLE admin WITH PASSWORD 'admin';
-- -------------------------
-- Alter Role admin
-- -------------------------
-- ALTER ROLE admin WITH SUPERUSER CREATEROLE CREATEDB LOGIN;
-- -------------------------
-- Database GRANT PRIVILEGES
-- -------------------------
GRANT ALL PRIVILEGES ON DATABASE authentication TO admin;
GRANT ALL PRIVILEGES ON DATABASE authentication_test TO admin;
-- -------------------------
-- Connect to authentication database
-- -------------------------
\c authentication;
-- -------------------------
-- Table users
-- -------------------------
DROP TABLE IF EXISTS users;
--
--
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
username VARCHAR(100) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at DATE,
deleted_at DATE
);

2
sql/script.sh Normal file
View File

@ -0,0 +1,2 @@
#!/bin/bash
psql -U root -d postgres -f schema.sql

28
src/configs/index.ts Normal file
View File

@ -0,0 +1,28 @@
import dotenv from 'dotenv';
dotenv.config();
const database =
process.env.ENV === 'dev'
? process.env.POSTGRES_DB
: process.env.POSTGRES_DB_TEST;
const configs = {
env: process.env.ENV,
host: process.env.HOST,
port: Number(process.env.PORT),
db_host: process.env.POSTGRES_URI,
db_port: Number(process.env.POSTGRES_PORT),
db_name: database,
db_user: process.env.POSTGRES_USER,
db_password: process.env.POSTGRES_PASSWORD,
salt: Number(process.env.SALT_ROUNDS),
pepper: process.env.SECRET_PEPPER,
access_token: process.env.JWT_SECRET_ACCESS_TOKEN,
refresh_token: process.env.JWT_SECRET_REFRESH_TOKEN,
access_expires: process.env.JWT_ACCESS_TOKEN_EXPIRATION,
refresh_expires: process.env.JWT_REFRESH_TOKEN_EXPIRATION,
backend_host: process.env.BACKEND_HOST,
frontend_host: process.env.FRONTEND_HOST
};
export default configs;

View File

@ -0,0 +1,6 @@
import { createUser } from './register';
import { authUser } from './login';
import { updatePassword } from './updatePassword';
import { refreshToken } from './refreshToken';
export { createUser, authUser, updatePassword, refreshToken };

View File

@ -0,0 +1,22 @@
import { Request, Response } from 'express';
import Auth from '../../models/auth';
import { signAccessToken, signRefreshToken } from '../../utils/token';
const auth = new Auth();
export const authUser = async (req: Request, res: Response) => {
try {
const response = await auth.authUser(req.body);
const accessToken = await signAccessToken(response);
const refreshToken = await signRefreshToken(response);
res.status(200).json({
status: true,
data: { user: { ...response }, tokens: { accessToken, refreshToken } },
message: 'User authenticated successfully.'
});
} catch (error) {
res.status(400).json({
message: (error as Error).message
});
}
};

View File

@ -0,0 +1,31 @@
import { Request, Response } from 'express';
import { verifyRefreshToken, signAccessToken } from '../../utils/token';
export const refreshToken = async (req: Request, res: Response) => {
try {
const { refreshToken: token } = req.body;
if (!token) throw 'Bad request';
const payload = await verifyRefreshToken(token);
const accessToken = await signAccessToken({
// @ts-ignore
id: payload?.id,
// @ts-ignore
first_name: payload?.first_name,
// @ts-ignore
last_name: payload?.last_name,
// @ts-ignore
username: payload?.username,
// @ts-ignore
email: payload?.email
});
res.status(200).json({
data: { accessToken }
});
} catch (error) {
res.status(401).json({
status: false,
message: error
});
}
};

View File

@ -0,0 +1,2 @@
import { createUser } from '../users/createUser';
export { createUser };

View File

@ -0,0 +1,19 @@
import { Request, Response } from 'express';
import Auth from '../../models/auth';
const auth = new Auth();
export const updatePassword = async (req: Request, res: Response) => {
try {
const response = await auth.updatePassword(req.params.id, req.body);
res.status(204).json({
status: true,
data: response,
message: 'Password changed successfully.'
});
} catch (error) {
res.status(400).json({
error: (error as Error).message
});
}
};

View File

@ -0,0 +1,22 @@
import { Request, Response } from 'express';
import User from '../../models/user';
import { signAccessToken, signRefreshToken } from '../../utils/token';
const user = new User();
export const createUser = async (req: Request, res: Response) => {
try {
const response = await user.createUser(req.body);
const accessToken = await signAccessToken(response);
const refreshToken = await signRefreshToken(response);
res.status(201).json({
status: true,
data: { user: { ...response }, tokens: { accessToken, refreshToken } },
message: 'User created successfully.'
});
} catch (error) {
res.status(400).json({
error: (error as Error).message
});
}
};

View File

@ -0,0 +1,19 @@
import { Request, Response } from 'express';
import User from '../../models/user';
const user = new User();
export const deleteUser = async (req: Request, res: Response) => {
try {
const response = await user.deleteUser(req.params.id);
res.status(200).json({
status: true,
data: response,
message: 'User deleted successfully.'
});
} catch (error) {
res.status(400).json({
message: (error as Error).message
});
}
};

View File

@ -0,0 +1,19 @@
import { Request, Response } from 'express';
import User from '../../models/user';
const user = new User();
export const getUser = async (req: Request, res: Response) => {
try {
const response = await user.getUser(req.params.id);
res.status(200).json({
status: true,
data: response,
message: 'User fetched successfully.'
});
} catch (error) {
res.status(400).json({
message: (error as Error).message
});
}
};

View File

@ -0,0 +1,5 @@
import { getUser } from './getUser';
import { updateUser } from './updateUser';
import { deleteUser } from './deleteUser';
export { getUser, updateUser, deleteUser };

View File

@ -0,0 +1,19 @@
import { Request, Response } from 'express';
import User from '../../models/user';
const user = new User();
export const updateUser = async (req: Request, res: Response) => {
try {
const response = await user.updateUser(req.params.id, req.body);
res.status(203).json({
status: true,
data: response,
message: 'User updated successfully.'
});
} catch (error) {
res.status(400).json({
message: (error as Error).message
});
}
};

17
src/database/index.ts Normal file
View File

@ -0,0 +1,17 @@
import { Pool, PoolClient } from 'pg';
import configs from '../configs';
const pool = new Pool({
host: configs.db_host,
port: configs.db_port,
database: configs.db_name,
user: configs.db_user,
password: configs.db_password
});
export default {
connect: async (): Promise<PoolClient> => {
const client = await pool.connect();
return client;
}
};

22
src/helpers/reporter.ts Normal file
View File

@ -0,0 +1,22 @@
import {
DisplayProcessor,
SpecReporter,
StacktraceOption
} from 'jasmine-spec-reporter';
import SuiteInfo = jasmine.SuiteInfo;
class CustomProcessor extends DisplayProcessor {
public displayJasmineStarted(info: SuiteInfo, log: string): string {
return `${log}`;
}
}
jasmine.getEnv().clearReporters();
jasmine.getEnv().addReporter(
new SpecReporter({
spec: {
displayStacktrace: StacktraceOption.NONE
},
customProcessors: [CustomProcessor]
})
);

View File

@ -0,0 +1,5 @@
import { validateRegister } from './validateRegister';
import { validateLogin } from './validateLogin';
import { validateUpdatePassword } from './validateUpdatePassword';
export { validateRegister, validateLogin, validateUpdatePassword };

View File

@ -0,0 +1,35 @@
import { Request, Response, NextFunction } from 'express';
import { body, oneOf } from 'express-validator';
import { validate } from '../validationResult';
export const validateLogin = [
oneOf(
[
body('username')
.exists()
.withMessage('username is missing from the body')
.notEmpty()
.withMessage('username is empty')
.isString()
.withMessage('username must be string'),
body('email')
.exists()
.withMessage('Email is missing from the body')
.notEmpty()
.withMessage('Email is empty')
.isEmail()
.withMessage('Email is not valid')
.normalizeEmail()
.withMessage('Email is not normalized')
],
'Must be at least one of email or username'
),
body('password')
.exists()
.withMessage('password is missing from the body')
.notEmpty()
.withMessage('password is empty')
.isLength({ min: 8 })
.withMessage('password must be at least 8 characters long'),
(req: Request, res: Response, next: NextFunction) => validate(req, res, next)
];

View File

@ -0,0 +1,2 @@
import { validateCreateUser } from '../users/validateCreateUser';
export const validateRegister = validateCreateUser;

View File

@ -0,0 +1,19 @@
import { Request, Response, NextFunction } from 'express';
import { check, body } from 'express-validator';
import { validate } from '../validationResult';
export const validateUpdatePassword = [
check('id')
.exists()
.withMessage('id is missing from the parameters')
.notEmpty()
.withMessage('id is empty'),
body('password')
.exists()
.withMessage('password is missing from the body')
.notEmpty()
.withMessage('password is empty')
.isLength({ min: 8 })
.withMessage('password must be at least 8 characters long'),
(req: Request, res: Response, next: NextFunction) => validate(req, res, next)
];

View File

@ -0,0 +1,5 @@
import { validateGetUser } from './validateGetUser';
import { validateUpdateUser } from './validateUpdateUser';
import { validateDeleteUser } from './validateDeleteUser';
export { validateGetUser, validateUpdateUser, validateDeleteUser };

View File

@ -0,0 +1,46 @@
import { Request, Response, NextFunction } from 'express';
import { body } from 'express-validator';
import { validate } from '../validationResult';
export const validateCreateUser = [
body('first_name')
.exists()
.withMessage("first_name does'nt exists in the body.")
.notEmpty()
.withMessage('first_name is empty')
.isString()
.isLength({ min: 5, max: 50 })
.withMessage('first_name must be at least 5 and maximum 50 letters'),
body('last_name')
.exists()
.withMessage("last_name does'nt exists in the body.")
.notEmpty()
.withMessage('last_name is empty')
.isString()
.isLength({ min: 5, max: 50 })
.withMessage('last_name must be at least 5 and maximum 50 letters'),
body('username')
.exists()
.withMessage('username is missing from the body')
.notEmpty()
.withMessage('username is empty')
.isString()
.withMessage('username must be string'),
body('email')
.exists()
.withMessage('Email is missing from the body')
.notEmpty()
.withMessage('Email is empty')
.isEmail()
.withMessage('Email is not valid')
.normalizeEmail()
.withMessage('Email is not normalized'),
body('password')
.exists()
.withMessage('password is missing from the body')
.notEmpty()
.withMessage('password is empty')
.isLength({ min: 8 })
.withMessage('password must be at least 8 characters long'),
(req: Request, res: Response, next: NextFunction) => validate(req, res, next)
];

View File

@ -0,0 +1,12 @@
import { Request, Response, NextFunction } from 'express';
import { check } from 'express-validator';
import { validate } from '../validationResult';
export const validateDeleteUser = [
check('id')
.exists()
.withMessage('id is missing from the parameters')
.notEmpty()
.withMessage('id is empty'),
(req: Request, res: Response, next: NextFunction) => validate(req, res, next)
];

View File

@ -0,0 +1,12 @@
import { Request, Response, NextFunction } from 'express';
import { check } from 'express-validator';
import { validate } from '../validationResult';
export const validateGetUser = [
check('id')
.exists()
.withMessage('id is missing from the parameters')
.notEmpty()
.withMessage('id is empty'),
(req: Request, res: Response, next: NextFunction) => validate(req, res, next)
];

View File

@ -0,0 +1,44 @@
import { Request, Response, NextFunction } from 'express';
import { check, body } from 'express-validator';
import { validate } from '../validationResult';
export const validateUpdateUser = [
check('id')
.exists()
.withMessage('id is missing from the parameters')
.notEmpty()
.withMessage('id is empty'),
body('first_name')
.exists()
.withMessage("first_name does'nt exists in the body.")
.notEmpty()
.withMessage('first_name is empty')
.isString()
.isLength({ min: 5, max: 50 })
.withMessage('first_name must be at least 5 and maximum 50 letters'),
body('last_name')
.exists()
.withMessage("last_name does'nt exists in the body.")
.notEmpty()
.withMessage('last_name is empty')
.isString()
.isLength({ min: 5, max: 50 })
.withMessage('last_name must be at least 5 and maximum 50 letters'),
body('username')
.exists()
.withMessage('username is missing from the body')
.notEmpty()
.withMessage('username is empty')
.isString()
.withMessage('username must be string'),
body('email')
.exists()
.withMessage('Email is missing from the body')
.notEmpty()
.withMessage('Email is empty')
.isEmail()
.withMessage('Email is not valid')
.normalizeEmail()
.withMessage('Email is not normalized'),
(req: Request, res: Response, next: NextFunction) => validate(req, res, next)
];

View File

@ -0,0 +1,17 @@
import { Request, Response, NextFunction } from 'express';
import { validationResult } from 'express-validator';
export const validate = async (
req: Request,
res: Response,
next: NextFunction
) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
const errorArray = errors
.array()
.map((error) => ({ [error.param]: [error.msg] }));
return res.status(422).json({ errors: errorArray });
}
next();
};

View File

@ -0,0 +1,2 @@
import { verifyAccessToken } from '../utils/token';
export const verifyToken = verifyAccessToken;

89
src/models/auth.ts Normal file
View File

@ -0,0 +1,89 @@
import database from '../database';
import { PoolClient } from 'pg';
import { compare, hash as hashPass } from '../utils/password';
import { UserType } from './user';
type AuthType = {
email: string;
password: string;
};
type PasswordType = {
old_password: string;
new_password: string;
};
class Auth {
async withConnection<T>(
callback: (connection: PoolClient) => Promise<T>
): Promise<T> {
const connection = await database.connect();
try {
return await callback(connection);
} catch (error) {
throw new Error((error as Error).message);
} finally {
connection.release();
}
}
async authUser(u: AuthType): Promise<AuthType & UserType> {
return this.withConnection(async (connection: PoolClient) => {
const query = {
text: 'SELECT password FROM users WHERE email=$1',
values: [u.email]
};
const result = await connection.query(query);
if (result.rows.length) {
const { password: hash } = result.rows[0];
const check = await compare(u.password, hash);
if (check) {
const query = {
text: 'SELECT id, first_name, last_name, username, email FROM users WHERE email=$1',
values: [u.email]
};
const result = await connection.query(query);
return result.rows[0];
}
throw new Error('Password is incorrect.');
}
throw new Error("Email doesn't exists.");
});
}
async authMe(id: string): Promise<UserType & AuthType> {
return this.withConnection(async (connection: PoolClient) => {
const query = {
text: 'SELECT id, first_name, last_name, username, email FROM users WHERE id=$1',
values: [id]
};
const result = await connection.query(query);
return result.rows[0];
});
}
async updatePassword(
id: string,
p: AuthType & PasswordType
): Promise<UserType> {
return this.withConnection(async (connection: PoolClient) => {
const query = {
text: 'SELECT password FROM users WHERE id=$1',
values: [id]
};
const result = await connection.query(query);
if (result.rows.length) {
const { password: hash } = result.rows[0];
const check = await compare(p.old_password, hash);
if (check) {
const password = await hashPass(p.new_password);
const query = {
text: 'UPDATE users SET password=$2 WHERE id=$1 RETURNING id',
values: [id, password]
};
const result = await connection.query(query);
return result.rows[0];
}
throw new Error('Old password is incorrect.');
}
throw new Error("User id doesn't exists.");
});
}
}
export default Auth;

View File

@ -0,0 +1,94 @@
import database from '../../database';
import User, { UserType } from '../user';
const user = new User();
describe('User Model', () => {
describe('Test methods exists', () => {
it('expects all CRUD operation methods to be exists', () => {
expect(user.createUser).toBeDefined();
expect(user.getUser).toBeDefined();
expect(user.updateUser).toBeDefined();
expect(user.deleteUser).toBeDefined();
});
});
describe('Methods returns', () => {
const newUser1 = {
first_name: 'Adham',
last_name: 'Haddad',
username: 'adhamhaddad',
email: 'adhamhaddad.dev@gmail.com',
password: 'adham123'
} as UserType;
const updateUser1 = {
first_name: 'Adham',
last_name: 'Ashraf',
username: 'adhamhaddad1',
email: 'adhamhaddad.dev@gmail.com'
} as UserType;
beforeAll(async () => {
const connection = await database.connect();
try {
const query = {
text: 'DELETE FROM users;\nALTER SEQUENCE users_id_seq RESTART WITH 1'
};
await connection.query(query);
} finally {
connection.release();
}
});
afterAll(async () => {
const connection = await database.connect();
try {
const query = {
text: 'DELETE FROM users;\nALTER SEQUENCE users_id_seq RESTART WITH 1'
};
await connection.query(query);
} finally {
connection.release();
}
});
it('createUser method should return a new user', async () => {
const result = await user.createUser(newUser1);
expect(result).toEqual({
id: 1,
first_name: 'Adham',
last_name: 'Haddad',
username: 'adhamhaddad',
email: 'adhamhaddad.dev@gmail.com'
} as UserType);
});
it('getUser method should return Object of user', async () => {
const result = await user.getUser('1');
expect(result).toEqual({
id: 1,
first_name: 'Adham',
last_name: 'Haddad',
username: 'adhamhaddad',
email: 'adhamhaddad.dev@gmail.com'
} as UserType);
});
it('updateUser method should return object with new values', async () => {
const result = await user.updateUser('1', updateUser1);
expect(result).toEqual({
id: 1,
first_name: 'Adham',
last_name: 'Ashraf',
username: 'adhamhaddad1',
email: 'adhamhaddad.dev@gmail.com'
} as UserType);
});
it('deleteUser method should return object with deleted user id', async () => {
const result = await user.deleteUser('1');
expect(result).toEqual({
id: 1
} as UserType);
});
});
});

79
src/models/user.ts Normal file
View File

@ -0,0 +1,79 @@
import { PoolClient } from 'pg';
import database from '../database';
import { hash } from '../utils/password';
export type UserType = {
id: number;
first_name: string;
last_name: string;
username: string;
email: string;
password: string;
created_at: Date;
updated_at: Date;
delete_at: Date;
};
class User {
async withConnection<T>(
callback: (connection: PoolClient) => Promise<T>
): Promise<T> {
const connection = await database.connect();
try {
return await callback(connection);
} catch (error) {
throw new Error((error as Error).message);
} finally {
connection.release();
}
}
async createUser(u: UserType): Promise<UserType> {
return this.withConnection(async (connection: PoolClient) => {
const password = await hash(u.password);
const query = {
text: `
INSERT INTO users (first_name, last_name, username, email, password)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, first_name, last_name, username, email
`,
values: [u.first_name, u.last_name, u.username, u.email, password]
};
const result = await connection.query(query);
return result.rows[0];
});
}
async getUser(id: string): Promise<UserType> {
return this.withConnection(async (connection: PoolClient) => {
const query = {
text: 'SELECT id, first_name, last_name, username, email FROM users WHERE id=$1',
values: [id]
};
const result = await connection.query(query);
return result.rows[0];
});
}
async updateUser(id: string, u: UserType): Promise<UserType> {
return this.withConnection(async (connection: PoolClient) => {
const query = {
text: `
UPDATE users SET first_name=$2, last_name=$3, username=$4, email=$5, updated_at=CURRENT_TIMESTAMP
WHERE id=$1
RETURNING id, first_name, last_name, username, email
`,
values: [id, u.first_name, u.last_name, u.username, u.email]
};
const result = await connection.query(query);
return result.rows[0];
});
}
async deleteUser(id: string): Promise<UserType> {
return this.withConnection(async (connection: PoolClient) => {
const query = {
text: 'UPDATE users SET updated_at=CURRENT_TIMESTAMP, deleted_at=CURRENT_TIMESTAMP WHERE id=$1 RETURNING id',
values: [id]
};
const result = await connection.query(query);
return result.rows[0];
});
}
}
export default User;

25
src/routes/api/auth.ts Normal file
View File

@ -0,0 +1,25 @@
import { Router } from 'express';
import {
validateRegister,
validateLogin,
validateUpdatePassword
} from '../../middlewares/validation/auth';
import {
createUser,
authUser,
refreshToken,
updatePassword
} from '../../controllers/auth';
import { checkAccessToken } from '../../utils/token';
import { verifyToken } from '../../middlewares/verifyToken';
const router = Router();
router
.post('/register', validateRegister, createUser)
.post('/login', validateLogin, authUser)
.patch('/reset-password', validateUpdatePassword, verifyToken, updatePassword)
.post('/refresh-token', refreshToken)
.get('/check-token', checkAccessToken);
export default router;

3
src/routes/api/index.ts Normal file
View File

@ -0,0 +1,3 @@
import auth from './auth';
import users from './users';
export { auth, users };

17
src/routes/api/users.ts Normal file
View File

@ -0,0 +1,17 @@
import { Router } from 'express';
import {
validateGetUser,
validateUpdateUser,
validateDeleteUser
} from '../../middlewares/validation/users';
import { getUser, updateUser, deleteUser } from '../../controllers/users';
import { verifyToken } from '../../middlewares/verifyToken';
const router = Router();
router
.get('/:id', validateGetUser, verifyToken, getUser)
.patch('/:id', validateUpdateUser, verifyToken, updateUser)
.delete('/:id', validateDeleteUser, verifyToken, deleteUser);
export default router;

9
src/routes/index.ts Normal file
View File

@ -0,0 +1,9 @@
import { Router } from 'express';
import { auth, users } from './api';
const router = Router();
router.use('/auth', auth);
router.use('/users', users);
export default router;

48
src/server.ts Normal file
View File

@ -0,0 +1,48 @@
import express, { Application } from 'express';
import helmet from 'helmet';
import morgan from 'morgan';
import cors from 'cors';
import os from 'os';
import configs from './configs';
import router from './routes';
// Express App
const app: Application = express();
const port: number = configs.port || 8000;
const ip =
os.networkInterfaces()['wlan0']?.[0].address ||
os.networkInterfaces()['eth0']?.[0].address;
const corsOptions = {
allowedHeaders: [
'Origin',
'X-Requested-With',
'Content-Type',
'Accept',
'X-Access-Token',
'Authorization',
'Access-Control-Allow-Origin',
'Access-Control-Allow-Headers',
'Access-Control-Allow-Methods'
],
methods: 'GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE',
preflightContinue: true,
origin: '*'
};
// Middlewares
app.use(helmet());
app.use(cors(corsOptions));
app.use(morgan('common'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(router);
// Express Server
app.listen(port, () => {
console.log(`Backend server is listening on http://${ip}:${configs.port}`);
console.log('Press CTRL+C to stop the server.');
});
export default app;

8
src/utils/password.ts Normal file
View File

@ -0,0 +1,8 @@
import bcrypt from 'bcrypt';
import configs from '../configs';
export const hash = (password: string) =>
bcrypt.hash(`${configs.pepper}${password}${configs.pepper}`, configs.salt);
export const compare = (password: string, hash: string) =>
bcrypt.compare(`${configs.pepper}${password}${configs.pepper}`, hash);

179
src/utils/token.ts Normal file
View File

@ -0,0 +1,179 @@
import { Request, Response, NextFunction } from 'express';
import jwt, { SignOptions } from 'jsonwebtoken';
import fs from 'fs';
import path from 'path';
import configs from '../configs';
import Auth from '../models/auth';
type Payload = {
id: number;
first_name: string;
last_name: string;
username: string;
email: string;
};
const privateAccessKey = path.join(
__dirname,
'..',
'..',
'keys',
'accessToken',
'private.key'
);
const privateRefreshKey = path.join(
__dirname,
'..',
'..',
'keys',
'refreshToken',
'private.key'
);
const publicAccessKey = path.join(
__dirname,
'..',
'..',
'keys',
'accessToken',
'public.key'
);
const publicRefreshKey = path.join(
__dirname,
'..',
'..',
'keys',
'refreshToken',
'public.key'
);
export const signAccessToken = async (payload: Payload) => {
const options: SignOptions = {
algorithm: 'RS256',
expiresIn: configs.access_expires,
issuer: 'adhamhaddad',
audience: String(payload.id)
};
return new Promise((resolve, reject) => {
fs.readFile(privateAccessKey, { encoding: 'utf8' }, (err, key) => {
if (err) reject(err);
jwt.sign(payload, key, options, (err, token) => {
if (err) reject(err);
resolve(token);
});
});
});
};
export const verifyAccessToken = async (
req: Request,
res: Response,
next: NextFunction
) => {
const authorization = req.headers.authorization as string;
if (!authorization) {
return res.status(401).json({
status: false,
message: 'Not Authorized'
});
}
const token = authorization.split(' ')[1];
fs.readFile(publicAccessKey, 'utf8', (err, key) => {
if (err) err.message;
jwt.verify(token, key, { algorithms: ['RS256'] }, (err, payload) => {
if (err)
return res.status(401).json({
status: false,
message: (err as Error).message
});
return next();
});
});
};
export const signRefreshToken = async (payload: Payload) => {
const options: SignOptions = {
algorithm: 'RS256',
expiresIn: configs.refresh_expires,
issuer: 'adhamhaddad',
audience: String(payload.id)
};
return new Promise((resolve, reject) => {
fs.readFile(privateRefreshKey, { encoding: 'utf8' }, (err, key) => {
if (err) reject(err);
jwt.sign(payload, key, options, (err, token) => {
if (err) reject(err);
resolve(token);
});
});
});
};
export const verifyRefreshToken = async (refreshToken: string) => {
return new Promise((resolve, reject) => {
fs.readFile(publicRefreshKey, 'utf8', (err, key) => {
if (err) reject(err);
jwt.verify(
refreshToken,
key,
{ algorithms: ['RS256'] },
(err, payload) => {
if (err) reject(err);
resolve(payload);
}
);
});
});
};
export const checkAccessToken = async (req: Request, res: Response) => {
const auth = new Auth();
try {
const authorization = req.headers.authorization as string;
if (!authorization) {
res.status(401).json({
status: false,
message: 'Not Authorized'
});
}
const token = authorization.split(' ')[1];
const authMe = async (id: string) => await auth.authMe(id);
fs.readFile(publicAccessKey, 'utf8', (err, key) => {
if (err) err.message;
jwt.verify(token, key, { algorithms: ['RS256'] }, (err, payload) => {
if (err) {
const decode = jwt.decode(token, { complete: true });
// @ts-ignore
const id = decode?.payload?.aud;
const responseData = async () => {
const response = await authMe(id);
const accessToken = await signAccessToken(response);
return res.status(200).json({
status: true,
data: {
response,
accessToken
},
message: 'Access token generated successfully.'
});
};
responseData();
} else {
// @ts-ignore
const id = payload.id;
const responseData = async () => {
const response = await authMe(id);
res.status(200).json({
data: response
});
};
responseData();
}
});
});
} catch (err) {
res.status(500).json({
status: false,
message: (err as Error).message
});
}
};

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"rootDir": "./src",
"outDir": "./dist",
"removeComments": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"exclude": ["node_module", "./dist", "./uploads"]
}

2050
yarn.lock Normal file

File diff suppressed because it is too large Load Diff