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. + +

Architecture OverviewFigure 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==