diff --git a/.env b/.env
index 7af01b4..cb81047 100644
--- a/.env
+++ b/.env
@@ -1,8 +1,8 @@
-ENV=dev
+NODE_ENV=dev
HOST=localhost
PORT=3000
-# DATABASE
+# POSTGRES
POSTGRES_URI=localhost
POSTGRES_PORT=5432
POSTGRES_DB=authentication
@@ -10,6 +10,11 @@ POSTGRES_DB_TEST=authentication_test
POSTGRES_USER=admin
POSTGRES_PASSWORD=admin
+# REDIS
+REDIS_HOST=localhost
+REDIS_PORT=6379
+REDIS_PASSWORD=admin
+
# BCRYPT
SECRET_PEPPER=github@adhamhaddad
SALT_ROUNDS=10
@@ -17,8 +22,8 @@ SALT_ROUNDS=10
# JWT
JWT_SECRET_ACCESS_TOKEN=
JWT_SECRET_REFRESH_TOKEN=
-JWT_ACCESS_TOKEN_EXPIRATION=5m
-JWT_REFRESH_TOKEN_EXPIRATION=1y
+JWT_ACCESS_TOKEN_EXPIRATION=300
+JWT_REFRESH_TOKEN_EXPIRATION=86400
# URL
BACKEND_HOST=http://127.0.0.1:8000
diff --git a/README.md b/README.md
index 8819e0d..96d363c 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,12 @@ My starter kit includes the following features and benefits:
I hope that my starter kit can help simplify the process of building Node.js applications with authentication and token management features, and provide a solid foundation for your application development. Please feel free to use and customize the code, and share your feedback and contributions to the repository.
+# High Level Architecture
+
+Figure 1 represents the authentication architecture of this sample implementation. The architecture is comprised of two logical components; application user, and application services.
+
+
Figure 1: High Level Architecture
+
## Dependencies
- Node v14.15.1 (LTS) or more recent. While older versions can work it is advisable to keep node to latest LTS version
@@ -78,8 +84,9 @@ Open to view it in the browser.
## Built With
-- [Node](https://nodejs.org) - Javascript Runtime
-- [Express](https://expressjs.com/) - Javascript API Framework
-- [PostgreSQL](https://www.postgresql.org/) - Open Source Relational Database
+- [Node](https://nodejs.org) - Javascript runtime
+- [Express](https://expressjs.com/) - Javascript API framework
+- [PostgreSQL](https://www.postgresql.org/) - Open source Relational Database
+- [Redis](https://redis.io/) - Open source in-memory data store.
- [Jasmine](https://jasmine.github.io/) - Testing library
- [JWT](https://jwt.io/) - JSON Web Token for generates access and refresh tokens
diff --git a/documents/authenticate.png b/documents/authenticate.png
new file mode 100644
index 0000000..c1d8524
Binary files /dev/null and b/documents/authenticate.png differ
diff --git a/package.json b/package.json
index 62aa858..09b4e7c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,7 @@
{
"dependencies": {
"bcrypt": "^5.1.0",
+ "cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
@@ -8,10 +9,12 @@
"helmet": "^6.1.5",
"jsonwebtoken": "^9.0.0",
"morgan": "^1.10.0",
- "pg": "^8.10.0"
+ "pg": "^8.10.0",
+ "redis": "^4.6.5"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
+ "@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.13",
"@types/dotenv": "^8.2.0",
"@types/eslint-config-prettier": "^6.11.0",
@@ -26,7 +29,10 @@
"@types/nodemon": "^1.19.2",
"@types/pg": "^8.6.6",
"@types/prettier": "^2.7.2",
+ "@types/redis": "^4.0.11",
"@types/typescript": "^2.0.0",
+ "@typescript-eslint/eslint-plugin": "^5.59.1",
+ "@typescript-eslint/parser": "^5.59.1",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
diff --git a/src/configs/index.ts b/src/configs/index.ts
index fc7f259..5b0a3c0 100644
--- a/src/configs/index.ts
+++ b/src/configs/index.ts
@@ -3,12 +3,12 @@ import dotenv from 'dotenv';
dotenv.config();
const database =
- process.env.ENV === 'dev'
+ process.env.NODE_ENV === 'dev'
? process.env.POSTGRES_DB
: process.env.POSTGRES_DB_TEST;
const configs = {
- env: process.env.ENV,
+ env: process.env.NODE_ENV,
host: process.env.HOST,
port: Number(process.env.PORT),
db_host: process.env.POSTGRES_URI,
@@ -16,12 +16,15 @@ const configs = {
db_name: database,
db_user: process.env.POSTGRES_USER,
db_password: process.env.POSTGRES_PASSWORD,
+ redis_host: process.env.REDIS_HOST,
+ redis_port: Number(process.env.REDIS_PORT),
+ redis_password: process.env.REDIS_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,
+ access_expires: Number(process.env.JWT_ACCESS_TOKEN_EXPIRATION),
+ refresh_expires: Number(process.env.JWT_REFRESH_TOKEN_EXPIRATION),
backend_host: process.env.BACKEND_HOST,
frontend_host: process.env.FRONTEND_HOST
};
diff --git a/src/controllers/auth/index.ts b/src/controllers/auth/index.ts
index 9ed2f53..5c78f64 100644
--- a/src/controllers/auth/index.ts
+++ b/src/controllers/auth/index.ts
@@ -1,6 +1,6 @@
import { createUser } from './register';
import { authUser } from './login';
import { updatePassword } from './updatePassword';
-import { refreshToken } from './refreshToken';
+import { refreshAccessToken } from './refreshAccessToken';
-export { createUser, authUser, updatePassword, refreshToken };
+export { createUser, authUser, updatePassword, refreshAccessToken };
diff --git a/src/controllers/auth/login.ts b/src/controllers/auth/login.ts
index a27bd35..4a0c489 100644
--- a/src/controllers/auth/login.ts
+++ b/src/controllers/auth/login.ts
@@ -1,17 +1,36 @@
import { Request, Response } from 'express';
+import { setAccessToken, setRefreshToken } from '../../utils/token';
import Auth from '../../models/auth';
-import { signAccessToken, signRefreshToken } from '../../utils/token';
+import configs from '../../configs';
const auth = new Auth();
export const authUser = async (req: Request, res: Response) => {
try {
+ // Authenticate the user and generate an access token and refresh token
const response = await auth.authUser(req.body);
- const accessToken = await signAccessToken(response);
- const refreshToken = await signRefreshToken(response);
+ const accessToken = await setAccessToken(response);
+ const refreshToken = await setRefreshToken(response);
+
+ // Set the access token as an HTTP-only cookie
+ res.cookie('accessToken', accessToken, {
+ httpOnly: true,
+ sameSite: 'strict',
+ secure: false,
+ maxAge: configs.access_expires
+ });
+
+ // Set the refresh token as an HTTP-only cookie
+ res.cookie('refreshToken', refreshToken, {
+ httpOnly: true,
+ sameSite: 'strict',
+ secure: false,
+ maxAge: configs.refresh_expires
+ });
+
res.status(200).json({
status: true,
- data: { user: { ...response }, tokens: { accessToken, refreshToken } },
+ data: { user: { ...response }, accessToken },
message: 'User authenticated successfully.'
});
} catch (error) {
diff --git a/src/controllers/auth/refreshAccessToken.ts b/src/controllers/auth/refreshAccessToken.ts
new file mode 100644
index 0000000..02305c2
--- /dev/null
+++ b/src/controllers/auth/refreshAccessToken.ts
@@ -0,0 +1,57 @@
+import { Request as ExpressRequest, Response } from 'express';
+import {
+ verifyRefreshToken,
+ setAccessToken,
+ setRefreshToken
+} from '../../utils/token';
+import configs from '../../configs';
+import { DecodedToken } from '../../utils/token';
+
+interface Request extends ExpressRequest {
+ user?: DecodedToken;
+}
+
+export const refreshAccessToken = async (req: Request, res: Response) => {
+ const { refreshToken: token } = req.body;
+ try {
+ // Verify the refresh token
+ const decoded = await verifyRefreshToken(token);
+ // Generate a new access and refresh tokens
+ const { id, first_name, last_name, username, email } = decoded;
+ const accessToken = await setAccessToken({
+ id,
+ first_name,
+ last_name,
+ username,
+ email
+ });
+ const refreshToken = await setRefreshToken({
+ id,
+ first_name,
+ last_name,
+ username,
+ email
+ });
+
+ res.cookie('accessToken', accessToken, {
+ httpOnly: true,
+ sameSite: 'strict',
+ secure: false,
+ maxAge: configs.access_expires
+ });
+ res.cookie('refreshToken', refreshToken, {
+ httpOnly: true,
+ sameSite: 'strict',
+ secure: false,
+ maxAge: configs.refresh_expires
+ });
+
+ res.status(200).json({
+ data: { accessToken }
+ });
+ } catch (error) {
+ res.status(401).json({
+ message: (error as Error).message
+ });
+ }
+};
diff --git a/src/controllers/auth/refreshToken.ts b/src/controllers/auth/refreshToken.ts
deleted file mode 100644
index bc20c18..0000000
--- a/src/controllers/auth/refreshToken.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-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
- });
- }
-};
diff --git a/src/controllers/users/createUser.ts b/src/controllers/users/createUser.ts
index 6cf8b92..ae2802a 100644
--- a/src/controllers/users/createUser.ts
+++ b/src/controllers/users/createUser.ts
@@ -1,14 +1,14 @@
import { Request, Response } from 'express';
import User from '../../models/user';
-import { signAccessToken, signRefreshToken } from '../../utils/token';
+import { setAccessToken, setRefreshToken } 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);
+ const accessToken = await setAccessToken(response);
+ const refreshToken = await setRefreshToken(response);
res.status(201).json({
status: true,
data: { user: { ...response }, tokens: { accessToken, refreshToken } },
diff --git a/src/database/index.ts b/src/database/index.ts
index 523373b..16a1c0f 100644
--- a/src/database/index.ts
+++ b/src/database/index.ts
@@ -1,17 +1,4 @@
-import { Pool, PoolClient } from 'pg';
-import configs from '../configs';
+import pgClient from './pg';
+import redisClient from './redis';
-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 => {
- const client = await pool.connect();
- return client;
- }
-};
+export { pgClient, redisClient };
diff --git a/src/database/pg.ts b/src/database/pg.ts
new file mode 100644
index 0000000..ce8ba15
--- /dev/null
+++ b/src/database/pg.ts
@@ -0,0 +1,25 @@
+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
+});
+
+pool.on('connect', () => {
+ console.log('Connected to Postgres.');
+});
+
+pool.on('error', (error) => {
+ console.error('Error connecting to Postgres:', error);
+});
+
+export default {
+ connect: async (): Promise => {
+ const client = await pool.connect();
+ return client;
+ }
+};
diff --git a/src/database/redis.ts b/src/database/redis.ts
new file mode 100644
index 0000000..7015f8f
--- /dev/null
+++ b/src/database/redis.ts
@@ -0,0 +1,13 @@
+import { createClient } from 'redis';
+
+const client = createClient({ url: 'redis://default@localhost:6379' });
+
+client.on('connect', () => {
+ console.log('Connected to Redis.');
+});
+
+client.on('error', (error) => {
+ console.error('Error connecting to Redis:', error);
+});
+
+export default client;
diff --git a/src/models/auth.ts b/src/models/auth.ts
index 62a30a0..f20c1dc 100644
--- a/src/models/auth.ts
+++ b/src/models/auth.ts
@@ -1,5 +1,5 @@
-import database from '../database';
import { PoolClient } from 'pg';
+import { pgClient } from '../database';
import { compare, hash as hashPass } from '../utils/password';
import { UserType } from './user';
@@ -16,7 +16,7 @@ class Auth {
async withConnection(
callback: (connection: PoolClient) => Promise
): Promise {
- const connection = await database.connect();
+ const connection = await pgClient.connect();
try {
return await callback(connection);
} catch (error) {
diff --git a/src/models/tests/userSpec.ts b/src/models/tests/userSpec.ts
index 4ee8943..129279c 100644
--- a/src/models/tests/userSpec.ts
+++ b/src/models/tests/userSpec.ts
@@ -1,6 +1,6 @@
-import database from '../../database';
+import { pgClient } from '../../database';
import User, { UserType } from '../user';
-import Auth, { AuthType, PasswordType } from '../auth';
+import Auth, { PasswordType } from '../auth';
const auth = new Auth();
const user = new User();
@@ -31,7 +31,7 @@ describe('User Model', () => {
} as UserType;
beforeAll(async () => {
- const connection = await database.connect();
+ const connection = await pgClient.connect();
try {
const query = {
text: 'DELETE FROM users;\nALTER SEQUENCE users_id_seq RESTART WITH 1'
@@ -101,7 +101,7 @@ describe('Auth Model', () => {
} as PasswordType;
afterAll(async () => {
- const connection = await database.connect();
+ const connection = await pgClient.connect();
try {
const query = {
text: 'DELETE FROM users;\nALTER SEQUENCE users_id_seq RESTART WITH 1'
diff --git a/src/models/user.ts b/src/models/user.ts
index 37c533b..866f150 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -1,5 +1,5 @@
import { PoolClient } from 'pg';
-import database from '../database';
+import { pgClient } from '../database';
import { hash } from '../utils/password';
export type UserType = {
@@ -17,7 +17,7 @@ class User {
async withConnection(
callback: (connection: PoolClient) => Promise
): Promise {
- const connection = await database.connect();
+ const connection = await pgClient.connect();
try {
return await callback(connection);
} catch (error) {
diff --git a/src/routes/api/auth.ts b/src/routes/api/auth.ts
index ef916cf..5a692f3 100644
--- a/src/routes/api/auth.ts
+++ b/src/routes/api/auth.ts
@@ -7,10 +7,9 @@ import {
import {
createUser,
authUser,
- refreshToken,
+ refreshAccessToken,
updatePassword
} from '../../controllers/auth';
-import { checkAccessToken } from '../../utils/token';
import { verifyToken } from '../../middlewares/verifyToken';
const router = Router();
@@ -19,7 +18,6 @@ router
.post('/register', validateRegister, createUser)
.post('/login', validateLogin, authUser)
.patch('/reset-password', validateUpdatePassword, verifyToken, updatePassword)
- .post('/refresh-token', refreshToken)
- .get('/check-token', checkAccessToken);
+ .post('/refresh-token', refreshAccessToken);
export default router;
diff --git a/src/server.ts b/src/server.ts
index f5ae1b7..9f118b7 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,6 +1,7 @@
import express, { Application } from 'express';
import helmet from 'helmet';
import morgan from 'morgan';
+import cookieParser from 'cookie-parser';
import cors from 'cors';
import os from 'os';
import configs from './configs';
@@ -34,6 +35,7 @@ const corsOptions = {
// Middlewares
app.use(helmet());
app.use(cors(corsOptions));
+app.use(cookieParser());
app.use(morgan('common'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
diff --git a/src/utils/token.ts b/src/utils/token.ts
deleted file mode 100644
index 353c15a..0000000
--- a/src/utils/token.ts
+++ /dev/null
@@ -1,179 +0,0 @@
-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
- });
- }
-};
diff --git a/src/utils/token/index.ts b/src/utils/token/index.ts
new file mode 100644
index 0000000..55e9214
--- /dev/null
+++ b/src/utils/token/index.ts
@@ -0,0 +1,24 @@
+import { setAccessToken } from './setAccessToken';
+import { setRefreshToken } from './setRefreshToken';
+import { verifyAccessToken } from './verifyAccessToken';
+import { verifyRefreshToken } from './verifyRefreshToken';
+
+interface Payload {
+ id: number;
+ first_name: string;
+ last_name: string;
+ username: string;
+ email: string;
+}
+interface DecodedToken {
+ id: string;
+}
+
+export {
+ setAccessToken,
+ setRefreshToken,
+ verifyAccessToken,
+ verifyRefreshToken,
+ Payload,
+ DecodedToken
+};
diff --git a/src/utils/token/setAccessToken.ts b/src/utils/token/setAccessToken.ts
new file mode 100644
index 0000000..4ffef8b
--- /dev/null
+++ b/src/utils/token/setAccessToken.ts
@@ -0,0 +1,40 @@
+import jwt, { SignOptions } from 'jsonwebtoken';
+import fs from 'fs';
+import path from 'path';
+import configs from '../../configs';
+import { redisClient } from '../../database';
+import { Payload } from '.';
+
+const privateAccessKey = path.join(
+ __dirname,
+ '..',
+ '..',
+ '..',
+ 'keys',
+ 'accessToken',
+ 'private.key'
+);
+
+export const setAccessToken = async (payload: Payload): Promise => {
+ await redisClient.connect();
+ try {
+ const privateKey = await fs.promises.readFile(privateAccessKey, 'utf8');
+ const options: SignOptions = {
+ algorithm: 'RS256',
+ expiresIn: configs.access_expires,
+ issuer: 'Nodejs-Refresh-Token',
+ audience: `user_id-${payload.id}`,
+ subject: 'access_token'
+ };
+ const token = jwt.sign(payload, privateKey, options);
+ await redisClient.set(`access_token:${payload.id}`, token, {
+ EX: configs.access_expires
+ });
+ return token;
+ } catch (err) {
+ console.log((err as Error).message);
+ throw new Error('Failed to sign JWT');
+ } finally {
+ await redisClient.disconnect();
+ }
+};
diff --git a/src/utils/token/setRefreshToken.ts b/src/utils/token/setRefreshToken.ts
new file mode 100644
index 0000000..1485f77
--- /dev/null
+++ b/src/utils/token/setRefreshToken.ts
@@ -0,0 +1,39 @@
+import jwt, { SignOptions } from 'jsonwebtoken';
+import fs from 'fs';
+import path from 'path';
+import configs from '../../configs';
+import { redisClient } from '../../database';
+import { Payload } from '.';
+
+const privateRefreshKey = path.join(
+ __dirname,
+ '..',
+ '..',
+ '..',
+ 'keys',
+ 'refreshToken',
+ 'private.key'
+);
+
+export const setRefreshToken = async (payload: Payload): Promise => {
+ await redisClient.connect();
+ try {
+ const privateKey = await fs.promises.readFile(privateRefreshKey, 'utf8');
+ const options: SignOptions = {
+ algorithm: 'RS256',
+ expiresIn: configs.refresh_expires,
+ issuer: 'Nodejs-Refresh-Token',
+ audience: `user_id-${payload.id}`,
+ subject: 'refresh_token'
+ };
+ const token = jwt.sign(payload, privateKey, options);
+ await redisClient.set(`refresh_token:${payload.id}`, token, {
+ EX: configs.refresh_expires
+ });
+ return token;
+ } catch (err) {
+ throw new Error('Failed to sign JWT');
+ } finally {
+ await redisClient.disconnect();
+ }
+};
diff --git a/src/utils/token/verifyAccessToken.ts b/src/utils/token/verifyAccessToken.ts
new file mode 100644
index 0000000..ec442b1
--- /dev/null
+++ b/src/utils/token/verifyAccessToken.ts
@@ -0,0 +1,89 @@
+import { Request as ExpressRequest, Response, NextFunction } from 'express';
+import jwt from 'jsonwebtoken';
+import fs from 'fs';
+import path from 'path';
+import { redisClient } from '../../database';
+import { DecodedToken } from '.';
+import { verifyRefreshToken, setAccessToken } from '.';
+import configs from '../../configs';
+
+const publicAccessKey = path.join(
+ __dirname,
+ '..',
+ '..',
+ '..',
+ 'keys',
+ 'accessToken',
+ 'public.key'
+);
+interface Request extends ExpressRequest {
+ user?: DecodedToken;
+}
+
+export const verifyAccessToken = async (
+ req: Request,
+ res: Response,
+ next: NextFunction
+) => {
+ await redisClient.connect();
+ try {
+ const authorization = req.headers.authorization as string;
+ if (!authorization) {
+ return res.status(401).json({
+ message: 'Not Authorized'
+ });
+ }
+ const [bearer, token] = authorization.split(' ');
+ if (bearer !== 'Bearer' || !token) {
+ throw new Error(
+ 'Invalid Authorization header format. Format is "Bearer ".'
+ );
+ }
+ try {
+ const publicKey = await fs.promises.readFile(publicAccessKey, 'utf8');
+ const decoded = jwt.verify(token, publicKey, {
+ algorithms: ['RS256'],
+ issuer: 'Nodejs-Refresh-Token'
+ }) as DecodedToken;
+ const cachedToken = await redisClient.get(`access_token:${decoded.id}`);
+ if (!cachedToken || cachedToken !== token) {
+ throw new Error('Access token not found or expired');
+ }
+ req.user = { id: decoded.id };
+ await redisClient.disconnect();
+ return next();
+ } catch (err) {
+ await redisClient.disconnect();
+ if ((err as Error).name !== 'TokenExpiredError') {
+ throw new Error('Invalid access token');
+ }
+
+ const refreshToken = req.cookies.refreshToken;
+ if (!refreshToken) {
+ throw new Error('Refresh token missing');
+ }
+
+ const decoded = await verifyRefreshToken(refreshToken);
+ const { id, first_name, last_name, username, email } = decoded;
+ const newAccessToken = await setAccessToken({
+ id,
+ first_name,
+ last_name,
+ username,
+ email
+ });
+
+ // Attach user object to request and proceed with new access token
+ req.user = { id: String(decoded.id) };
+ res.cookie('accessToken', newAccessToken, {
+ httpOnly: true,
+ sameSite: 'strict',
+ secure: false,
+ maxAge: configs.access_expires
+ });
+ return next();
+ }
+ } catch (err) {
+ res.status(401).json({ message: (err as Error).message });
+ }
+};
diff --git a/src/utils/token/verifyRefreshToken.ts b/src/utils/token/verifyRefreshToken.ts
new file mode 100644
index 0000000..6cd68d4
--- /dev/null
+++ b/src/utils/token/verifyRefreshToken.ts
@@ -0,0 +1,35 @@
+import jwt from 'jsonwebtoken';
+import fs from 'fs';
+import path from 'path';
+import { redisClient } from '../../database';
+import { Payload } from '.';
+
+const publicRefreshKey = path.join(
+ __dirname,
+ '..',
+ '..',
+ '..',
+ 'keys',
+ 'refreshToken',
+ 'public.key'
+);
+
+export const verifyRefreshToken = async (token: string): Promise => {
+ await redisClient.connect();
+ try {
+ const publicKey = await fs.promises.readFile(publicRefreshKey, 'utf8');
+ const decoded = jwt.verify(token, publicKey, {
+ algorithms: ['RS256'],
+ issuer: 'Nodejs-Refresh-Token'
+ }) as Payload;
+ const cachedToken = await redisClient.get(`refresh_token:${decoded.id}`);
+ if (!cachedToken || cachedToken !== token) {
+ throw new Error('Refresh token not found or expired');
+ }
+ return decoded;
+ } catch (err) {
+ throw new Error(`Failed to verify JWT: ${(err as Error).message}`);
+ } finally {
+ await redisClient.disconnect();
+ }
+};
diff --git a/yarn.lock b/yarn.lock
index 36bb2f6..77bfa18 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -101,12 +101,12 @@
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
-"@nodelib/fs.stat@2.0.5":
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
-"@nodelib/fs.walk@^1.2.8":
+"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
@@ -114,6 +114,40 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
+"@redis/bloom@1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71"
+ integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==
+
+"@redis/client@1.5.6":
+ version "1.5.6"
+ resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.6.tgz#869cc65718d7d5493ef655a71dc40f3bc64a1b28"
+ integrity sha512-dFD1S6je+A47Lj22jN/upVU2fj4huR7S9APd7/ziUXsIXDL+11GPYti4Suv5y8FuXaN+0ZG4JF+y1houEJ7ToA==
+ dependencies:
+ cluster-key-slot "1.1.2"
+ generic-pool "3.9.0"
+ yallist "4.0.0"
+
+"@redis/graph@1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.0.tgz#cc2b82e5141a29ada2cce7d267a6b74baa6dd519"
+ integrity sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==
+
+"@redis/json@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.4.tgz#f372b5f93324e6ffb7f16aadcbcb4e5c3d39bda1"
+ integrity sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==
+
+"@redis/search@1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.2.tgz#6a8f66ba90812d39c2457420f859ce8fbd8f3838"
+ integrity sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==
+
+"@redis/time-series@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.4.tgz#af85eb080f6934580e4d3b58046026b6c2b18717"
+ integrity sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==
+
"@tsconfig/node10@^1.0.7":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
@@ -156,6 +190,13 @@
dependencies:
"@types/node" "*"
+"@types/cookie-parser@^1.4.3":
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.3.tgz#3a01df117c5705cf89a84c876b50c5a1fd427a21"
+ integrity sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==
+ dependencies:
+ "@types/express" "*"
+
"@types/cors@^2.8.13":
version "2.8.13"
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.13.tgz#b8ade22ba455a1b8cb3b5d3f35910fd204f84f94"
@@ -211,7 +252,7 @@
dependencies:
express-validator "*"
-"@types/express@^4.17.17":
+"@types/express@*", "@types/express@^4.17.17":
version "4.17.17"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4"
integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==
@@ -233,7 +274,7 @@
resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-4.3.1.tgz#2d8ab5601c2fe7d9673dcb157e03f128ab5c5fff"
integrity sha512-Vu8l+UGcshYmV1VWwULgnV/2RDbBaO6i2Ptx7nd//oJPIZGhoI1YLST4VKagD2Pq/Bc2/7zvtvhM7F3p4SN7kQ==
-"@types/json-schema@*":
+"@types/json-schema@*", "@types/json-schema@^7.0.9":
version "7.0.11"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
@@ -293,6 +334,18 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
+"@types/redis@^4.0.11":
+ version "4.0.11"
+ resolved "https://registry.yarnpkg.com/@types/redis/-/redis-4.0.11.tgz#0bb4c11ac9900a21ad40d2a6768ec6aaf651c0e1"
+ integrity sha512-bI+gth8La8Wg/QCR1+V1fhrL9+LZUSWfcqpOj2Kc80ZQ4ffbdL173vQd5wovmoV9i071FU9oP2g6etLuEwb6Rg==
+ dependencies:
+ redis "*"
+
+"@types/semver@^7.3.12":
+ version "7.3.13"
+ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
+ integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==
+
"@types/serve-static@*":
version "1.15.1"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.1.tgz#86b1753f0be4f9a1bee68d459fcda5be4ea52b5d"
@@ -308,6 +361,90 @@
dependencies:
typescript "*"
+"@typescript-eslint/eslint-plugin@^5.59.1":
+ version "5.59.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz#9b09ee1541bff1d2cebdcb87e7ce4a4003acde08"
+ integrity sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==
+ dependencies:
+ "@eslint-community/regexpp" "^4.4.0"
+ "@typescript-eslint/scope-manager" "5.59.1"
+ "@typescript-eslint/type-utils" "5.59.1"
+ "@typescript-eslint/utils" "5.59.1"
+ debug "^4.3.4"
+ grapheme-splitter "^1.0.4"
+ ignore "^5.2.0"
+ natural-compare-lite "^1.4.0"
+ semver "^7.3.7"
+ tsutils "^3.21.0"
+
+"@typescript-eslint/parser@^5.59.1":
+ version "5.59.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.1.tgz#73c2c12127c5c1182d2e5b71a8fa2a85d215cbb4"
+ integrity sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==
+ dependencies:
+ "@typescript-eslint/scope-manager" "5.59.1"
+ "@typescript-eslint/types" "5.59.1"
+ "@typescript-eslint/typescript-estree" "5.59.1"
+ debug "^4.3.4"
+
+"@typescript-eslint/scope-manager@5.59.1":
+ version "5.59.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz#8a20222719cebc5198618a5d44113705b51fd7fe"
+ integrity sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==
+ dependencies:
+ "@typescript-eslint/types" "5.59.1"
+ "@typescript-eslint/visitor-keys" "5.59.1"
+
+"@typescript-eslint/type-utils@5.59.1":
+ version "5.59.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz#63981d61684fd24eda2f9f08c0a47ecb000a2111"
+ integrity sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==
+ dependencies:
+ "@typescript-eslint/typescript-estree" "5.59.1"
+ "@typescript-eslint/utils" "5.59.1"
+ debug "^4.3.4"
+ tsutils "^3.21.0"
+
+"@typescript-eslint/types@5.59.1":
+ version "5.59.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.1.tgz#03f3fedd1c044cb336ebc34cc7855f121991f41d"
+ integrity sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==
+
+"@typescript-eslint/typescript-estree@5.59.1":
+ version "5.59.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz#4aa546d27fd0d477c618f0ca00b483f0ec84c43c"
+ integrity sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==
+ dependencies:
+ "@typescript-eslint/types" "5.59.1"
+ "@typescript-eslint/visitor-keys" "5.59.1"
+ debug "^4.3.4"
+ globby "^11.1.0"
+ is-glob "^4.0.3"
+ semver "^7.3.7"
+ tsutils "^3.21.0"
+
+"@typescript-eslint/utils@5.59.1":
+ version "5.59.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.1.tgz#d89fc758ad23d2157cfae53f0b429bdf15db9473"
+ integrity sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==
+ dependencies:
+ "@eslint-community/eslint-utils" "^4.2.0"
+ "@types/json-schema" "^7.0.9"
+ "@types/semver" "^7.3.12"
+ "@typescript-eslint/scope-manager" "5.59.1"
+ "@typescript-eslint/types" "5.59.1"
+ "@typescript-eslint/typescript-estree" "5.59.1"
+ eslint-scope "^5.1.1"
+ semver "^7.3.7"
+
+"@typescript-eslint/visitor-keys@5.59.1":
+ version "5.59.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz#0d96c36efb6560d7fb8eb85de10442c10d8f6058"
+ integrity sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==
+ dependencies:
+ "@typescript-eslint/types" "5.59.1"
+ eslint-visitor-keys "^3.3.0"
+
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
@@ -401,6 +538,11 @@ array-flatten@1.1.1:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
+array-union@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
+ integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@@ -452,7 +594,7 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
-braces@~3.0.2:
+braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
@@ -515,6 +657,11 @@ chownr@^2.0.0:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
+cluster-key-slot@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
+ integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
+
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
@@ -559,11 +706,24 @@ content-type@~1.0.4:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
+cookie-parser@^1.4.6:
+ version "1.4.6"
+ resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594"
+ integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==
+ dependencies:
+ cookie "0.4.1"
+ cookie-signature "1.0.6"
+
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
+cookie@0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
+ integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
+
cookie@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
@@ -598,7 +758,7 @@ debug@2.6.9:
dependencies:
ms "2.0.0"
-debug@4, debug@^4.1.1, debug@^4.3.2:
+debug@4, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -642,6 +802,13 @@ diff@^4.0.1:
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
+dir-glob@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
+ integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
+ dependencies:
+ path-type "^4.0.0"
+
doctrine@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
@@ -698,6 +865,14 @@ eslint-plugin-prettier@^4.2.1:
dependencies:
prettier-linter-helpers "^1.0.0"
+eslint-scope@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+ integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
+ dependencies:
+ esrecurse "^4.3.0"
+ estraverse "^4.1.1"
+
eslint-scope@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b"
@@ -780,6 +955,11 @@ esrecurse@^4.3.0:
dependencies:
estraverse "^5.2.0"
+estraverse@^4.1.1:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+ integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
estraverse@^5.1.0, estraverse@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
@@ -858,6 +1038,17 @@ fast-diff@^1.1.2:
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
+fast-glob@^3.2.9:
+ version "3.2.12"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
+ integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
+ dependencies:
+ "@nodelib/fs.stat" "^2.0.2"
+ "@nodelib/fs.walk" "^1.2.3"
+ glob-parent "^5.1.2"
+ merge2 "^1.3.0"
+ micromatch "^4.0.4"
+
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
@@ -970,6 +1161,11 @@ gauge@^3.0.0:
strip-ansi "^6.0.1"
wide-align "^1.1.2"
+generic-pool@3.9.0:
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4"
+ integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==
+
get-intrinsic@^1.0.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f"
@@ -979,6 +1175,13 @@ get-intrinsic@^1.0.2:
has "^1.0.3"
has-symbols "^1.0.3"
+glob-parent@^5.1.2, glob-parent@~5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
glob-parent@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
@@ -986,13 +1189,6 @@ glob-parent@^6.0.2:
dependencies:
is-glob "^4.0.3"
-glob-parent@~5.1.2:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
- integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
- dependencies:
- is-glob "^4.0.1"
-
glob@^7.1.3, glob@^7.1.6:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
@@ -1012,6 +1208,18 @@ globals@^13.19.0:
dependencies:
type-fest "^0.20.2"
+globby@^11.1.0:
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
+ integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
+ dependencies:
+ array-union "^2.1.0"
+ dir-glob "^3.0.1"
+ fast-glob "^3.2.9"
+ ignore "^5.2.0"
+ merge2 "^1.4.1"
+ slash "^3.0.0"
+
grapheme-splitter@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
@@ -1278,11 +1486,24 @@ merge-descriptors@1.0.1:
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
+merge2@^1.3.0, merge2@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+ integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
+micromatch@^4.0.4:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+ integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+ dependencies:
+ braces "^3.0.2"
+ picomatch "^2.3.1"
+
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
@@ -1358,6 +1579,11 @@ ms@2.1.3, ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+natural-compare-lite@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4"
+ integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==
+
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -1524,6 +1750,11 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
+path-type@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+ integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
pg-connection-string@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34"
@@ -1575,7 +1806,7 @@ pgpass@1.x:
dependencies:
split2 "^4.1.0"
-picomatch@^2.0.4, picomatch@^2.2.1:
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
@@ -1680,6 +1911,18 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
+redis@*, redis@^4.6.5:
+ version "4.6.5"
+ resolved "https://registry.yarnpkg.com/redis/-/redis-4.6.5.tgz#f32fbde44429e96f562bb0c9b1db0143ab8cfa4f"
+ integrity sha512-O0OWA36gDQbswOdUuAhRL6mTZpHFN525HlgZgDaVNgCJIAZR3ya06NTESb0R+TUZ+BFaDpz6NnnVvoMx9meUFg==
+ dependencies:
+ "@redis/bloom" "1.2.0"
+ "@redis/client" "1.5.6"
+ "@redis/graph" "1.1.0"
+ "@redis/json" "1.0.4"
+ "@redis/search" "1.1.2"
+ "@redis/time-series" "1.0.4"
+
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@@ -1729,7 +1972,7 @@ semver@^6.0.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
-semver@^7.3.5, semver@^7.3.8:
+semver@^7.3.5, semver@^7.3.7, semver@^7.3.8:
version "7.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0"
integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==
@@ -1813,6 +2056,11 @@ simple-update-notifier@^1.0.7:
dependencies:
semver "~7.0.0"
+slash@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+ integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+
split2@^4.1.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
@@ -1925,6 +2173,18 @@ ts-node@^10.9.1:
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
+tslib@^1.8.1:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+ integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
+tsutils@^3.21.0:
+ version "3.21.0"
+ resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
+ integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
+ dependencies:
+ tslib "^1.8.1"
+
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
@@ -2034,7 +2294,7 @@ xtend@^4.0.0:
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
-yallist@^4.0.0:
+yallist@4.0.0, yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==