From 135696eb2acecefdefeae125ce774ca55e4ee93f Mon Sep 17 00:00:00 2001 From: adhamhaddad Date: Wed, 26 Apr 2023 07:59:58 +0200 Subject: [PATCH] added new changes --- .env | 13 +- README.md | 13 +- documents/authenticate.png | Bin 0 -> 22090 bytes package.json | 8 +- src/configs/index.ts | 11 +- src/controllers/auth/index.ts | 4 +- src/controllers/auth/login.ts | 27 +- src/controllers/auth/refreshAccessToken.ts | 57 ++++ src/controllers/auth/refreshToken.ts | 31 --- src/controllers/users/createUser.ts | 6 +- src/database/index.ts | 19 +- src/database/pg.ts | 25 ++ src/database/redis.ts | 13 + src/models/auth.ts | 4 +- src/models/tests/userSpec.ts | 8 +- src/models/user.ts | 4 +- src/routes/api/auth.ts | 6 +- src/server.ts | 2 + src/utils/token.ts | 179 ------------- src/utils/token/index.ts | 24 ++ src/utils/token/setAccessToken.ts | 40 +++ src/utils/token/setRefreshToken.ts | 39 +++ src/utils/token/verifyAccessToken.ts | 89 +++++++ src/utils/token/verifyRefreshToken.ts | 35 +++ yarn.lock | 292 +++++++++++++++++++-- 25 files changed, 674 insertions(+), 275 deletions(-) create mode 100644 documents/authenticate.png create mode 100644 src/controllers/auth/refreshAccessToken.ts delete mode 100644 src/controllers/auth/refreshToken.ts create mode 100644 src/database/pg.ts create mode 100644 src/database/redis.ts delete mode 100644 src/utils/token.ts create mode 100644 src/utils/token/index.ts create mode 100644 src/utils/token/setAccessToken.ts create mode 100644 src/utils/token/setRefreshToken.ts create mode 100644 src/utils/token/verifyAccessToken.ts create mode 100644 src/utils/token/verifyRefreshToken.ts 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 0000000000000000000000000000000000000000..c1d8524d32268b91c4361b651782bde6a2303fd9 GIT binary patch literal 22090 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 { 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==