added new code
This commit is contained in:
parent
f8baeba66d
commit
ff1f6bf560
25
.env
Normal file
25
.env
Normal 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
25
.eslintrc.js
Normal 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
6
.gitignore
vendored
@ -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
20
.prettierrc.json
Normal 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"
|
||||||
|
}
|
||||||
35
README.md
35
README.md
@ -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
18
database.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
51
keys/accessToken/private.key
Normal file
51
keys/accessToken/private.key
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
14
keys/accessToken/public.key
Normal file
14
keys/accessToken/public.key
Normal 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-----
|
||||||
51
keys/refreshToken/private.key
Normal file
51
keys/refreshToken/private.key
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
14
keys/refreshToken/public.key
Normal file
14
keys/refreshToken/public.key
Normal 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-----
|
||||||
53
migrations/20230424202313-users-table.js
Normal file
53
migrations/20230424202313-users-table.js
Normal 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
|
||||||
|
};
|
||||||
1
migrations/sqls/20230424202313-users-table-down.sql
Normal file
1
migrations/sqls/20230424202313-users-table-down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS users;
|
||||||
11
migrations/sqls/20230424202313-users-table-up.sql
Normal file
11
migrations/sqls/20230424202313-users-table-up.sql
Normal 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
53
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
spec/support/jasmine.json
Normal file
7
spec/support/jasmine.json
Normal 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
52
sql/schema.sql
Normal 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
2
sql/script.sh
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
psql -U root -d postgres -f schema.sql
|
||||||
28
src/configs/index.ts
Normal file
28
src/configs/index.ts
Normal 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;
|
||||||
6
src/controllers/auth/index.ts
Normal file
6
src/controllers/auth/index.ts
Normal 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 };
|
||||||
22
src/controllers/auth/login.ts
Normal file
22
src/controllers/auth/login.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
31
src/controllers/auth/refreshToken.ts
Normal file
31
src/controllers/auth/refreshToken.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
2
src/controllers/auth/register.ts
Normal file
2
src/controllers/auth/register.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { createUser } from '../users/createUser';
|
||||||
|
export { createUser };
|
||||||
19
src/controllers/auth/updatePassword.ts
Normal file
19
src/controllers/auth/updatePassword.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
22
src/controllers/users/createUser.ts
Normal file
22
src/controllers/users/createUser.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
19
src/controllers/users/deleteUser.ts
Normal file
19
src/controllers/users/deleteUser.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
19
src/controllers/users/getUser.ts
Normal file
19
src/controllers/users/getUser.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
5
src/controllers/users/index.ts
Normal file
5
src/controllers/users/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { getUser } from './getUser';
|
||||||
|
import { updateUser } from './updateUser';
|
||||||
|
import { deleteUser } from './deleteUser';
|
||||||
|
|
||||||
|
export { getUser, updateUser, deleteUser };
|
||||||
19
src/controllers/users/updateUser.ts
Normal file
19
src/controllers/users/updateUser.ts
Normal 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
17
src/database/index.ts
Normal 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
22
src/helpers/reporter.ts
Normal 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]
|
||||||
|
})
|
||||||
|
);
|
||||||
5
src/middlewares/validation/auth/index.ts
Normal file
5
src/middlewares/validation/auth/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { validateRegister } from './validateRegister';
|
||||||
|
import { validateLogin } from './validateLogin';
|
||||||
|
import { validateUpdatePassword } from './validateUpdatePassword';
|
||||||
|
|
||||||
|
export { validateRegister, validateLogin, validateUpdatePassword };
|
||||||
35
src/middlewares/validation/auth/validateLogin.ts
Normal file
35
src/middlewares/validation/auth/validateLogin.ts
Normal 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)
|
||||||
|
];
|
||||||
2
src/middlewares/validation/auth/validateRegister.ts
Normal file
2
src/middlewares/validation/auth/validateRegister.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { validateCreateUser } from '../users/validateCreateUser';
|
||||||
|
export const validateRegister = validateCreateUser;
|
||||||
19
src/middlewares/validation/auth/validateUpdatePassword.ts
Normal file
19
src/middlewares/validation/auth/validateUpdatePassword.ts
Normal 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)
|
||||||
|
];
|
||||||
5
src/middlewares/validation/users/index.ts
Normal file
5
src/middlewares/validation/users/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { validateGetUser } from './validateGetUser';
|
||||||
|
import { validateUpdateUser } from './validateUpdateUser';
|
||||||
|
import { validateDeleteUser } from './validateDeleteUser';
|
||||||
|
|
||||||
|
export { validateGetUser, validateUpdateUser, validateDeleteUser };
|
||||||
46
src/middlewares/validation/users/validateCreateUser.ts
Normal file
46
src/middlewares/validation/users/validateCreateUser.ts
Normal 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)
|
||||||
|
];
|
||||||
12
src/middlewares/validation/users/validateDeleteUser.ts
Normal file
12
src/middlewares/validation/users/validateDeleteUser.ts
Normal 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)
|
||||||
|
];
|
||||||
12
src/middlewares/validation/users/validateGetUser.ts
Normal file
12
src/middlewares/validation/users/validateGetUser.ts
Normal 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)
|
||||||
|
];
|
||||||
44
src/middlewares/validation/users/validateUpdateUser.ts
Normal file
44
src/middlewares/validation/users/validateUpdateUser.ts
Normal 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)
|
||||||
|
];
|
||||||
17
src/middlewares/validation/validationResult.ts
Normal file
17
src/middlewares/validation/validationResult.ts
Normal 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();
|
||||||
|
};
|
||||||
2
src/middlewares/verifyToken.ts
Normal file
2
src/middlewares/verifyToken.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { verifyAccessToken } from '../utils/token';
|
||||||
|
export const verifyToken = verifyAccessToken;
|
||||||
89
src/models/auth.ts
Normal file
89
src/models/auth.ts
Normal 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;
|
||||||
94
src/models/tests/userSpec.ts
Normal file
94
src/models/tests/userSpec.ts
Normal 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
79
src/models/user.ts
Normal 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
25
src/routes/api/auth.ts
Normal 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
3
src/routes/api/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import auth from './auth';
|
||||||
|
import users from './users';
|
||||||
|
export { auth, users };
|
||||||
17
src/routes/api/users.ts
Normal file
17
src/routes/api/users.ts
Normal 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
9
src/routes/index.ts
Normal 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
48
src/server.ts
Normal 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
8
src/utils/password.ts
Normal 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
179
src/utils/token.ts
Normal 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
14
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user