From bf0839b06182aa11e348e5cd4382286729db9b62 Mon Sep 17 00:00:00 2001 From: Boon Hian Date: Sat, 2 Mar 2024 12:24:57 +0800 Subject: [PATCH] feat: add validation function and oauth signin --- apps/challenges/.env.example | 8 +- apps/challenges/package.json | 3 + apps/challenges/src/controllers/auth.ts | 55 ++++++++++++ apps/challenges/src/controllers/submission.ts | 2 +- apps/challenges/src/index.ts | 6 ++ .../src/middleware/jwtMiddleware.ts | 21 +++++ apps/challenges/src/model/response.ts | 5 -- apps/challenges/src/repo/userRepo.ts | 10 ++- apps/challenges/src/routes/auth.ts | 10 +++ apps/challenges/src/routes/user.ts | 4 +- apps/challenges/src/service/authService.ts | 90 +++++++++++++++++++ .../challenges/src/service/questionService.ts | 2 +- .../src/service/submissionService.ts | 2 +- apps/challenges/src/service/tokenService.ts | 32 +++++++ apps/challenges/src/service/userService.ts | 13 ++- apps/challenges/src/types/types.ts | 20 +++++ apps/challenges/src/utils/supabase.ts | 41 +++++++++ apps/challenges/src/utils/validator.ts | 6 ++ yarn.lock | 86 ++++++++++++++++++ 19 files changed, 402 insertions(+), 14 deletions(-) create mode 100644 apps/challenges/src/controllers/auth.ts create mode 100644 apps/challenges/src/middleware/jwtMiddleware.ts delete mode 100644 apps/challenges/src/model/response.ts create mode 100644 apps/challenges/src/routes/auth.ts create mode 100644 apps/challenges/src/service/authService.ts create mode 100644 apps/challenges/src/service/tokenService.ts create mode 100644 apps/challenges/src/types/types.ts create mode 100644 apps/challenges/src/utils/supabase.ts diff --git a/apps/challenges/.env.example b/apps/challenges/.env.example index 907bb9fb..e03e7931 100644 --- a/apps/challenges/.env.example +++ b/apps/challenges/.env.example @@ -2,4 +2,10 @@ MONGO_URI=mongodb://localhost:27017/ MONGO_PORT=27017 MONGO_DATABSE_NAME=challenges -DO_RANKING_CALCULATION=true \ No newline at end of file +BASE_URL=http://localhost:3001 +PORT=3001 +DO_RANKING_CALCULATION=true + +// fill in the following for supabase (used in register and login) +SUPABASE_URL= +SUPABASE_ANON_KEY= \ No newline at end of file diff --git a/apps/challenges/package.json b/apps/challenges/package.json index 8bcfac8b..79d95fc8 100644 --- a/apps/challenges/package.json +++ b/apps/challenges/package.json @@ -6,6 +6,7 @@ "dependencies": { "@babel/preset-env": "^7.23.8", "@babel/preset-typescript": "^7.23.3", + "@supabase/supabase-js": "^2.39.7", "@types/express": "^4.17.21", "babel-jest": "^29.7.0", "cron": "^3.1.6", @@ -15,6 +16,7 @@ "express-async-handler": "^1.1.4", "jest": "^29.7.0", "jsonwebtoken": "^9.0.2", + "jwt-decode": "^4.0.0", "prettier": "^2.8.1", "supertest": "^6.3.4", "ts-jest": "^29.1.1", @@ -25,6 +27,7 @@ "devDependencies": { "@types/express": "^4.17.21", "@types/jest": "^29.5.11", + "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.10.6", "@types/supertest": "^6.0.2", "jest": "^29.7.0", diff --git a/apps/challenges/src/controllers/auth.ts b/apps/challenges/src/controllers/auth.ts new file mode 100644 index 00000000..ca7a1a62 --- /dev/null +++ b/apps/challenges/src/controllers/auth.ts @@ -0,0 +1,55 @@ +import asyncHandler from "express-async-handler"; +import { Request, Response } from "express"; +import { z } from "zod"; +import AuthService from "../service/authService"; + + +const oauthSignIn = asyncHandler(async (req: Request, res: Response) => { + const { redirectURL } = req.body; + + try { + const resp = await AuthService.oauthSignIn(redirectURL); + res.status(resp.status).json({ resp }); + } catch (error) { + res.status(500).json({ message: 'Internal Server Error' }); + } +}); + +const authCallback = asyncHandler(async (req: Request, res: Response) => { + let codeString: string; + let nextString: string | undefined; + let append = '#status=auth_failure'; + + try { + console.log(req.query); + const { code, next } = req.query; + + if (!code) { + throw new Error('Invalid code'); + } + + codeString = z.string().parse(code); + nextString = z.string().optional().parse(next); + + const { accessToken, refreshToken } = await AuthService.oauthCallback(codeString, nextString); + append = `#access_token=${accessToken}&refresh_token=${refreshToken}&status=auth_success`; + + } catch (err) { + console.error(err); + } finally { + if (nextString) { + res.redirect(303, nextString + append); + } else { + res.status(append === '#status=auth_failure' ? 400 : 200).json({ message: append }); + } + } + +}); + + +const AuthController = { + oauthSignIn, + authCallback, +} + +export { AuthController as default }; diff --git a/apps/challenges/src/controllers/submission.ts b/apps/challenges/src/controllers/submission.ts index 1b296579..469af500 100644 --- a/apps/challenges/src/controllers/submission.ts +++ b/apps/challenges/src/controllers/submission.ts @@ -15,7 +15,7 @@ import mongoose from 'mongoose'; // @access Public const getSubmissions = asyncHandler(async (req: Request, res: Response) => { const submissions = await Submission.find({}) - res.status(200).json(submissions) + res.status(200).json(submissions) }) // @desc Get submission diff --git a/apps/challenges/src/index.ts b/apps/challenges/src/index.ts index a12288ad..cbbcccef 100644 --- a/apps/challenges/src/index.ts +++ b/apps/challenges/src/index.ts @@ -4,13 +4,16 @@ import SeasonRouter from "./routes/seasons"; import QuestionaireRouter from "./routes/questionaire"; import SubmissionRouter from "./routes/submission"; import UserRouter from "./routes/user"; +import AuthRouter from "./routes/auth"; import { connectDB } from "./config/db"; import { CronJob } from "cron"; import { rankingCalculation } from "./tasks/rankingCalculation"; +import { SupabaseService } from "./utils/supabase"; dotenv.config({ path: "../.env"}); // Database connectDB(); +SupabaseService.initClient(); const app: Express = express(); const port = process.env.PORT || 3000; @@ -31,7 +34,10 @@ app.use("/api/seasons", SeasonRouter); app.use('/api/question', QuestionaireRouter); app.use('/api/submission', SubmissionRouter); app.use('/api/user', UserRouter); +app.use('/api/auth', AuthRouter); +// the check is needed for testing +// refer to https://stackoverflow.com/a/63299022 if (process.env.NODE_ENV !== 'test') { app.listen(port, () => { console.log(`[server]: Server is running at http://localhost:${port}`); diff --git a/apps/challenges/src/middleware/jwtMiddleware.ts b/apps/challenges/src/middleware/jwtMiddleware.ts new file mode 100644 index 00000000..d248667b --- /dev/null +++ b/apps/challenges/src/middleware/jwtMiddleware.ts @@ -0,0 +1,21 @@ +// jwt middleware for express +import jwt from "jsonwebtoken"; +import { Request, Response, NextFunction } from "express"; + +const jwtMiddleware = (req: Request, res: Response, next: NextFunction) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (token == null) { + return res.sendStatus(401); + } + + jwt.verify(token, process.env.JWT_SECRET || "", (err, user) => { + if (err) { + return res.sendStatus(403); + } + next(); + }); +} + +export default jwtMiddleware; diff --git a/apps/challenges/src/model/response.ts b/apps/challenges/src/model/response.ts deleted file mode 100644 index 04ad531b..00000000 --- a/apps/challenges/src/model/response.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface GeneralResp { - status: number; - message: string; - data?: any; -} \ No newline at end of file diff --git a/apps/challenges/src/repo/userRepo.ts b/apps/challenges/src/repo/userRepo.ts index 66fe00e6..2e687fb1 100644 --- a/apps/challenges/src/repo/userRepo.ts +++ b/apps/challenges/src/repo/userRepo.ts @@ -9,6 +9,13 @@ const getUserByID = async (id: mongoose.Types.ObjectId) => { return user; } +const getUserByEmail = async (email: string) => { + const user = await User.findOne({ + email: email + }); + return user; +} + const createUser = async ( name: string, email: string, @@ -23,7 +30,8 @@ const createUser = async ( const UserRepo = { getUserByID, - createUser + getUserByEmail, + createUser, } export { UserRepo as default }; \ No newline at end of file diff --git a/apps/challenges/src/routes/auth.ts b/apps/challenges/src/routes/auth.ts new file mode 100644 index 00000000..a23e2999 --- /dev/null +++ b/apps/challenges/src/routes/auth.ts @@ -0,0 +1,10 @@ +import Express from "express"; +import AuthController from "../controllers/auth"; + +const router = Express.Router(); + +router.post("/oauth/signin", AuthController.oauthSignIn); +router.get("/oauth/callback", AuthController.authCallback); +// router.get("/:userID/rankings", SeasonController.getUserAllSeasonRankings); + +export { router as default }; \ No newline at end of file diff --git a/apps/challenges/src/routes/user.ts b/apps/challenges/src/routes/user.ts index bca6ada1..b78e2675 100644 --- a/apps/challenges/src/routes/user.ts +++ b/apps/challenges/src/routes/user.ts @@ -1,10 +1,8 @@ import Express from "express"; import UserController from "../controllers/user"; -import SeasonController from "../controllers/season"; const router = Express.Router(); -router.post("/", UserController.createUser); -// router.get("/:userID/rankings", SeasonController.getUserAllSeasonRankings); +router.get("/:userID", UserController.createUser); export { router as default }; \ No newline at end of file diff --git a/apps/challenges/src/service/authService.ts b/apps/challenges/src/service/authService.ts new file mode 100644 index 00000000..b2daf382 --- /dev/null +++ b/apps/challenges/src/service/authService.ts @@ -0,0 +1,90 @@ +import { SupabaseService } from '../utils/supabase'; + +import { jwtDecode } from "jwt-decode"; +import UserService from "../service/userService"; +import { isValidEmail, getEmailPrefix } from "../utils/validator"; +import supabase from "../utils/supabase"; +import { z } from 'zod'; +import TokenService from './tokenService'; +import { GeneralResp, OauthcallbackResp } from '../types/types'; + +const oauthSignIn = async ( + url: string +) => { + let redirectURL: string; + try { + redirectURL = z.string().url().parse(url); + } catch (err) { + return { + status: 400, + message: 'Invalid redirectURL', + data: null, + } as GeneralResp; + } + + try { + const resp = await SupabaseService.signInWithAzure(redirectURL); + return { + status: 302, + message: 'waiting for response from azure', + data: resp, + } as GeneralResp; + } catch (err) { + console.log("error in createUser with azure: ", err) + return { + status: 500, + message: 'Internal Server Error', + data: null, + } as GeneralResp; + } +} + +const oauthCallback = async ( + code: string, + next: string | undefined, +): Promise => { + // verify the query parameters + const resp = await supabase.auth.exchangeCodeForSession(code); + + const supabaseToken = resp.data.session?.access_token; + + if (resp.error || !supabaseToken) { + throw new Error("Failed to exchange code for session."); + } + + const decodedJWTToken = jwtDecode(supabaseToken); + const decodedJWTObj = decodedJWTToken as { + aud: string; + exp: number; + iat: number; + iss: string; + sub: string; + email: string; + role: string; + session_id: string; + } + console.log(decodedJWTObj); + + let user = await UserService.getUserByEmail(decodedJWTObj.email); + + if (!user) { + const email = isValidEmail.parse(decodedJWTObj.email); + const userName = getEmailPrefix(email); + user = await UserService.createUser(userName, decodedJWTObj.email); + } + + const accessToken = await TokenService.generateAccessToken(user._id.toString(), user.email); + const refreshToken = await TokenService.generateRefreshToken(user._id.toString(), user.email); + + return { + accessToken: accessToken, + refreshToken: refreshToken, + } +} + +const AuthService = { + oauthSignIn, + oauthCallback, +} + +export { AuthService as default }; \ No newline at end of file diff --git a/apps/challenges/src/service/questionService.ts b/apps/challenges/src/service/questionService.ts index 59fe4eeb..f0f1c34e 100644 --- a/apps/challenges/src/service/questionService.ts +++ b/apps/challenges/src/service/questionService.ts @@ -2,7 +2,7 @@ import mongoose from 'mongoose'; import QuestionRepo from '../repo/questionRepo'; import { CreateQuestionReq } from '../model/question'; import ValidationService from './validationService'; -import { GeneralResp } from '../model/response'; +import { GeneralResp } from '../types/types'; const getQuestionByID = async( questionID: string, diff --git a/apps/challenges/src/service/submissionService.ts b/apps/challenges/src/service/submissionService.ts index 9b97a035..cec4478d 100644 --- a/apps/challenges/src/service/submissionService.ts +++ b/apps/challenges/src/service/submissionService.ts @@ -1,7 +1,7 @@ import { CreateSubmissionReq, SubmissionModel } from "../model/submission"; import SubmissionRepo from "../repo/submissionRepo"; +import { GeneralResp } from "../types/types"; import QuestionService from "./questionService"; -import { GeneralResp } from "../model/response"; import ValidationService from "./validationService"; const createSubmission = async ( diff --git a/apps/challenges/src/service/tokenService.ts b/apps/challenges/src/service/tokenService.ts new file mode 100644 index 00000000..af1eb144 --- /dev/null +++ b/apps/challenges/src/service/tokenService.ts @@ -0,0 +1,32 @@ +import jwt from "jsonwebtoken"; + +const minuteInSeconds = 60; + +const generateAccessToken = async ( + id: string, + email: string +) => { + const secret = process.env.JWT_SECRET || ""; + const token = jwt.sign({ id, email }, secret, { + expiresIn: 10 * minuteInSeconds + }); + return token; +} + +const generateRefreshToken = async ( + id: string, + email: string +) => { + const secret = process.env.JWT_SECRET || ""; + const token = jwt.sign({ id, email }, secret, { + expiresIn: "7d" + }); + return token; +} + +const TokenService = { + generateAccessToken, + generateRefreshToken, +} + +export { TokenService as default }; \ No newline at end of file diff --git a/apps/challenges/src/service/userService.ts b/apps/challenges/src/service/userService.ts index b17ce14b..c3f2c027 100644 --- a/apps/challenges/src/service/userService.ts +++ b/apps/challenges/src/service/userService.ts @@ -1,5 +1,7 @@ import mongoose from 'mongoose'; import UserRepo from '../repo/userRepo'; +import { z } from 'zod'; +import { isValidEmail } from '../utils/validator'; const getUserByID = async (id: string) => { if (!mongoose.isValidObjectId(id)) { @@ -10,6 +12,14 @@ const getUserByID = async (id: string) => { return user; } +const getUserByEmail = async (email: string) => { + const _email = isValidEmail.parse(email); + + const user = await UserRepo.getUserByEmail(_email); + + return user; +} + const createUser = async ( name: string, email: string, @@ -20,7 +30,8 @@ const createUser = async ( const UserService = { getUserByID, - createUser + getUserByEmail, + createUser, } export { UserService as default }; \ No newline at end of file diff --git a/apps/challenges/src/types/types.ts b/apps/challenges/src/types/types.ts new file mode 100644 index 00000000..a0f8c586 --- /dev/null +++ b/apps/challenges/src/types/types.ts @@ -0,0 +1,20 @@ +export interface GeneralResp { + status: number; + message: string; + data?: any; +} + +class StatusCodeError extends Error { + status: number; + constructor(status: number, message: string) { + super(message); + this.status = status; + } +} + +export interface OauthcallbackResp { + accessToken: string; + refreshToken: string; +} + +export default { StatusCodeError } \ No newline at end of file diff --git a/apps/challenges/src/utils/supabase.ts b/apps/challenges/src/utils/supabase.ts new file mode 100644 index 00000000..a6d5d5e2 --- /dev/null +++ b/apps/challenges/src/utils/supabase.ts @@ -0,0 +1,41 @@ +import { SupabaseClient, createClient } from '@supabase/supabase-js' +import dotenv from "dotenv"; +dotenv.config({ path: "../.env" }); + + +let supabase: SupabaseClient; + +const initClient = () => { + supabase = createClient( + process.env.SUPABASE_URL || "", + process.env.SUPABASE_ANON_KEY || "", + { + auth: { + autoRefreshToken: false, + persistSession: false, + detectSessionInUrl: false, + flowType: "pkce" + }, + } + ) + console.log("Supabase client initialized"); +} + +const signInWithAzure = async (redirectURL: string) => { + const url = process.env.BASE_URL || "http://localhost:3001"; + const resposne = await supabase.auth.signInWithOAuth({ + provider: "azure", + options: { + scopes: 'email', + redirectTo: url + "/api/auth/oauth/callback?next=" + redirectURL, + }, + }) + return resposne; +} + +const SupabaseService = { + initClient, + signInWithAzure, +} + +export { SupabaseService, supabase as default } \ No newline at end of file diff --git a/apps/challenges/src/utils/validator.ts b/apps/challenges/src/utils/validator.ts index 36eebb9f..ca6ea6f4 100644 --- a/apps/challenges/src/utils/validator.ts +++ b/apps/challenges/src/utils/validator.ts @@ -1,6 +1,10 @@ import mongoose from 'mongoose'; import { z } from 'zod'; +export const getEmailPrefix = (email: string) => { + return email.replace(/@.*$/, ""); +} + export const isValidDate = (d: Date) => { return d instanceof Date && !isNaN(d.valueOf()) } @@ -22,6 +26,8 @@ export const isValidCreateSubmissionRequest = z.object({ answer: z.string(), }); +export const isValidEmail = z.string().min(1).email(); + export const isValidCreateQuestionRequest = z.object({ question_no: z.string(), question_title: z.string(), diff --git a/yarn.lock b/yarn.lock index bb5b5d6b..0d2122f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7105,6 +7105,63 @@ resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.54.0.tgz#f92f6b646533776a41bc62b650d2a03f1e282af8" integrity sha512-nElTXkS+nMfDNMkWfLmyeqHQfMGJ1JjrjAVMibV61Oc/rYdUv0cKRYCi1l4ivZ5SySB3vQLcLolxbKBkbNznZA== +"@supabase/functions-js@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.1.5.tgz#ed1b85f499dfda21d40fe39b86ab923117cb572b" + integrity sha512-BNzC5XhCzzCaggJ8s53DP+WeHHGT/NfTsx2wUSSGKR2/ikLFQTBCDzMvGz/PxYMqRko/LwncQtKXGOYp1PkPaw== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/gotrue-js@2.62.2": + version "2.62.2" + resolved "https://registry.yarnpkg.com/@supabase/gotrue-js/-/gotrue-js-2.62.2.tgz#9f15a451559d71475c953aa0027e1248b0210196" + integrity sha512-AP6e6W9rQXFTEJ7sTTNYQrNf0LCcnt1hUW+RIgUK+Uh3jbWvcIST7wAlYyNZiMlS9+PYyymWQ+Ykz/rOYSO0+A== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/node-fetch@2.6.15", "@supabase/node-fetch@^2.6.14": + version "2.6.15" + resolved "https://registry.yarnpkg.com/@supabase/node-fetch/-/node-fetch-2.6.15.tgz#731271430e276983191930816303c44159e7226c" + integrity sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ== + dependencies: + whatwg-url "^5.0.0" + +"@supabase/postgrest-js@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@supabase/postgrest-js/-/postgrest-js-1.9.2.tgz#39c839022ce73f4eb5da6fb7724fb6a6392150c7" + integrity sha512-I6yHo8CC9cxhOo6DouDMy9uOfW7hjdsnCxZiaJuIVZm1dBGTFiQPgfMa9zXCamEWzNyWRjZvupAUuX+tqcl5Sw== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/realtime-js@2.9.3": + version "2.9.3" + resolved "https://registry.yarnpkg.com/@supabase/realtime-js/-/realtime-js-2.9.3.tgz#f822401aed70883dca5d538179b11089d6d1b6ed" + integrity sha512-lAp50s2n3FhGJFq+wTSXLNIDPw5Y0Wxrgt44eM5nLSA3jZNUUP3Oq2Ccd1CbZdVntPCWLZvJaU//pAd2NE+QnQ== + dependencies: + "@supabase/node-fetch" "^2.6.14" + "@types/phoenix" "^1.5.4" + "@types/ws" "^8.5.10" + ws "^8.14.2" + +"@supabase/storage-js@2.5.5": + version "2.5.5" + resolved "https://registry.yarnpkg.com/@supabase/storage-js/-/storage-js-2.5.5.tgz#2958e2a2cec8440e605bb53bd36649288c4dfa01" + integrity sha512-OpLoDRjFwClwc2cjTJZG8XviTiQH4Ik8sCiMK5v7et0MDu2QlXjCAW3ljxJB5+z/KazdMOTnySi+hysxWUPu3w== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/supabase-js@^2.39.7": + version "2.39.7" + resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.39.7.tgz#61c3277a94bd9fd0574b39ecdf4fecffd73a139c" + integrity sha512-1vxsX10Uhc2b+Dv9pRjBjHfqmw2N2h1PyTg9LEfICR3x2xwE24By1MGCjDZuzDKH5OeHCsf4it6K8KRluAAEXA== + dependencies: + "@supabase/functions-js" "2.1.5" + "@supabase/gotrue-js" "2.62.2" + "@supabase/node-fetch" "2.6.15" + "@supabase/postgrest-js" "1.9.2" + "@supabase/realtime-js" "2.9.3" + "@supabase/storage-js" "2.5.5" + "@swc/core-darwin-arm64@1.3.37": version "1.3.37" resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.37.tgz#a92e075ae35f18a64aaf3823ea175f03564f8da1" @@ -7682,6 +7739,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/jsonwebtoken@^9.0.6": + version "9.0.6" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz#d1af3544d99ad992fb6681bbe60676e06b032bd3" + integrity sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw== + dependencies: + "@types/node" "*" + "@types/lodash.mergewith@4.6.6": version "4.6.6" resolved "https://registry.yarnpkg.com/@types/lodash.mergewith/-/lodash.mergewith-4.6.6.tgz#c4698f5b214a433ff35cb2c75ee6ec7f99d79f10" @@ -7786,6 +7850,11 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/phoenix@^1.5.4": + version "1.6.4" + resolved "https://registry.yarnpkg.com/@types/phoenix/-/phoenix-1.6.4.tgz#cceac93a827555473ad38057d1df7d06eef1ed71" + integrity sha512-B34A7uot1Cv0XtaHRYDATltAdKx0BvVKNgYNqE4WjtPUa4VQJM7kxeXcVKaH+KS+kCmZ+6w+QaUdcljiheiBJA== + "@types/prettier@^2.1.5", "@types/prettier@^2.6.1": version "2.7.2" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0" @@ -7997,6 +8066,13 @@ dependencies: winston "*" +"@types/ws@^8.5.10": + version "8.5.10" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" + integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -14409,6 +14485,11 @@ jwt-decode@^3.1.2: resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== +jwt-decode@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" + integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== + kareem@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.4.1.tgz#7d81ec518204a48c1cb16554af126806c3cd82b0" @@ -20052,6 +20133,11 @@ ws@^8.11.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== +ws@^8.14.2: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + ws@^8.2.3: version "8.13.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"