diff --git a/.gitignore b/.gitignore index af754b12..bde2eca5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules/ yarn-error.log .expo/ **.env +**/build/ package-lock.json \ No newline at end of file diff --git a/packages/server/.eslintrc b/packages/server/.eslintrc new file mode 100644 index 00000000..ae1473f6 --- /dev/null +++ b/packages/server/.eslintrc @@ -0,0 +1,22 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "sourceType": "script", + "ecmaVersion": 2023 + }, + "plugins": ["@typescript-eslint"], + "env": { + "node": true, + "amd": true + }, + "rules": { + "@typescript-eslint/no-explicit-any": "off" + }, + "ignorePatterns": ["/__tests__/**/*"], + "extends": [ + "prettier", + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ] +} diff --git a/packages/server/.eslintrc.json b/packages/server/.eslintrc.json deleted file mode 100644 index 5ab43ef8..00000000 --- a/packages/server/.eslintrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "parserOptions": { - "sourceType": "module", - "ecmaVersion": 2023 - }, - "env": { - "node": true - }, - "extends": ["prettier", "eslint:recommended"] -} diff --git a/packages/server/__tests__/tsconfig.json b/packages/server/__tests__/tsconfig.json new file mode 100644 index 00000000..e69de29b diff --git a/packages/server/controllers/auth.js b/packages/server/controllers/auth.js deleted file mode 100644 index 5d453fb7..00000000 --- a/packages/server/controllers/auth.js +++ /dev/null @@ -1,200 +0,0 @@ -const { OAuth2Client } = require("google-auth-library"); -const { v5: uuidv5, v4: uuidv4 } = require("uuid"); -const prisma = require("../prisma/prisma"); -const token = require("../utils/token"); -const bcrypt = require("bcrypt"); -const client = new OAuth2Client(process.env.WEB_CLIENT_ID); - -const NAMESPACE = "7af17462-8078-4703-adda-be2143a4d93a"; - -async function create(accessToken, refreshToken, profile, callback) { - try { - let { sub, given_name, family_name, picture, email } = profile._json; - picture = picture.replace("=s96-c", ""); - const googleUUID = uuidv5(sub, NAMESPACE); - const user = await prisma.users.findUniqueOrThrow({ - where: { - userId: googleUUID, - }, - }); - if (!user) { - const newUser = await prisma.users.create({ - data: { - userId: googleUUID, - email: email, - avatar: picture, - firstName: given_name, - lastName: family_name, - }, - }); - console.log("user doesn't exist. create one"); - callback(null, newUser); - } else { - console.log("user exists"); - callback(null, user); - } - } catch (error) { - callback(error); - } -} - -async function authenticateWithGoogle(token) { - // Verify the token is valid; to ensure it's a valid google auth. - const { payload } = await client.verifyIdToken({ - idToken: token, - audience: process.env.WEB_CLIENT_ID, - }); - - // Check if this user already exist on our database. - const { sub, email, given_name, family_name, picture } = payload; - - // generate Type 5 UUIDs with hashes of user's google account IDs instead of - // completely random Type 4 UUIDs to keep UUIDs unique regardless of google or - // local authentication - const googleUUID = uuidv5(sub, NAMESPACE); - const user = await prisma.users.findUnique({ - where: { - userId: googleUUID, - }, - }); - - // if the query returns a row, there's a user with the existing userId. - if (!user) { - const newUser = await prisma.users.create({ - data: { - userId: googleUUID, - email: email, - avatar: picture, - firstName: given_name, - lastName: family_name, - }, - }); - - return newUser; - } else { - return user; - } -} - -async function register(newUser) { - const saltRounds = 10; - const salt = await bcrypt.genSalt(saltRounds); - const passwordHash = await bcrypt.hash(newUser.password, salt); - const userId = uuidv4(); - - // placeholder names until new user puts in their names in onboarding screen - await prisma.users.create({ - data: { - userId, - firstName: "New", - lastName: "User", - email: newUser.email, - password: passwordHash, - }, - }); -} - -async function login(request, response, next) { - const provider = request.url.slice(1); - - switch (provider) { - case "google": - if (!request.body.token) { - return response.status(400).json({ - status: "fail", - data: { - token: "Token not provided", - }, - }); - } - - try { - const user = await authenticateWithGoogle(request.body.token); - request.user = user; - next(); - } catch (err) { - return response.status(500).json({ - status: "error", - message: err.message, - }); - } - break; - default: - return response.status(500).json({ - status: "error", - message: "Unsupported authentication provider", - }); - } -} - -async function serialize(payload, callback) { - try { - const { user_id } = payload; - console.log("serializeUser", user_id); - callback(null, user_id); - } catch (error) { - callback(error); - } -} - -async function deserialize(id, callback) { - try { - console.log("deserializeUser"); - console.log("id", id); - const user = await prisma.users.findUniqueOrThrow({ - where: { - userId: id, - }, - }); - if (user) { - console.log(user); - callback(null, user); - } - } catch (error) { - callback(error); - } -} - -async function authenticate(request, response, next) { - const authToken = request.get("Authorization"); - - if (!authToken) { - return response.status(400).json({ - status: "fail", - data: { - Authorization: "Token not provided", - }, - }); - } - - try { - request.user = token.verifyAccessToken(authToken); - next(); - } catch (error) { - // Handle any errors that occur during token verification - switch (error.name) { - case "TokenExpiredError": - return response.status(401).json({ - status: "fail", - data: { - accessToken: "Token expired", - }, - }); - default: - return response.status(500).json({ - status: "error", - message: error.message, - }); - } - } -} - -module.exports = { - create, - login, - register, - serialize, - deserialize, - authenticate, - authenticateWithGoogle, -}; diff --git a/packages/server/controllers/auth.ts b/packages/server/controllers/auth.ts new file mode 100644 index 00000000..faf0c57a --- /dev/null +++ b/packages/server/controllers/auth.ts @@ -0,0 +1,171 @@ +import { User } from "@prisma/client"; +import { NewUser, RequestWithUser } from "../types/auth"; + +import { OAuth2Client } from "google-auth-library"; +import { v4 as uuidv4, v5 as uuidv5 } from "uuid"; +import prisma from "../utils/prisma"; +import token from "../utils/token"; +import bcrypt from "bcrypt"; +import { NextFunction, Request, Response } from "express"; + +const client = new OAuth2Client(process.env.WEB_CLIENT_ID); + +const NAMESPACE = "7af17462-8078-4703-adda-be2143a4d93a"; + +async function authenticateWithGoogle(token: string): Promise { + // Verify the token is valid; to ensure it's a valid google auth. + const loginTicket = await client.verifyIdToken({ + idToken: token, + audience: process.env.WEB_CLIENT_ID + }); + const payload = loginTicket.getPayload(); + + if (!payload) { + throw new Error("Could not verify with Google."); + } + + // Check if this user already exist on our database. + const { sub, email, given_name, family_name, picture } = payload; + + // generate Type 5 UUIDs with hashes of user's google account IDs instead of + // completely random Type 4 UUIDs to map our generated UUIDs to google + // accounts + const googleUUID = uuidv5(sub, NAMESPACE); + const user = await prisma.user.findUnique({ + where: { + userId: googleUUID + } + }); + + // if the query returns a row, there's a user with the existing userId. + if (!user) { + const newUser = await prisma.user.create({ + data: { + // ok type assertions, email and profile scope used by default + userId: googleUUID, + email: email as string, + avatar: picture, + firstName: given_name as string, + lastName: family_name as string + } + }); + + return newUser; + } else { + return user; + } +} + +async function register(newUser: NewUser) { + const saltRounds = 10; + const salt = await bcrypt.genSalt(saltRounds); + const passwordHash = await bcrypt.hash(newUser.password, salt); + const userId = uuidv4(); + + // placeholder names until new user puts in their names in onboarding screen + await prisma.user.create({ + data: { + userId, + firstName: "New", + lastName: "User", + email: newUser.email, + password: passwordHash + } + }); +} + +async function login(request: Request, response: Response, next: NextFunction) { + const provider = request.url.slice(1); + + switch (provider) { + case "google": + if (!request.body.token) { + response.status(400).json({ + status: "fail", + data: { + token: "Token not provided" + } + }); + return; + } + + try { + const user = await authenticateWithGoogle(request.body.token); + (request as RequestWithUser).user = user; + next(); + } catch (err) { + if (err instanceof Error) { + response.status(500).json({ + status: "error", + message: err.message + }); + return; + } + + response.status(500).json({ + status: "error", + message: "Internal Server Error" + }); + } + break; + default: + response.status(500).json({ + status: "error", + message: "Unsupported authentication provider" + }); + return; + } +} + +async function authenticate( + request: Request, + response: Response, + next: NextFunction +) { + const authToken = request.get("Authorization"); + + if (!authToken) { + return response.status(400).json({ + status: "fail", + data: { + Authorization: "Token not provided" + } + }); + } + + try { + (request as RequestWithUser).user = await token.verifyAccessToken( + authToken + ); + next(); + } catch (error) { + if (error instanceof Error) { + switch (error.name) { + case "TokenExpiredError": + return response.status(401).json({ + status: "fail", + data: { + accessToken: "Token expired" + } + }); + default: + return response.status(500).json({ + status: "error", + message: error.message + }); + } + } + + response.status(500).json({ + status: "error", + message: "Internal Server Error" + }); + } +} + +export default { + login, + register, + authenticate, + authenticateWithGoogle +}; diff --git a/packages/server/controllers/events.js b/packages/server/controllers/events.ts similarity index 61% rename from packages/server/controllers/events.js rename to packages/server/controllers/events.ts index d05a308a..06e747aa 100644 --- a/packages/server/controllers/events.js +++ b/packages/server/controllers/events.ts @@ -1,16 +1,17 @@ // const postgres = require("../utils/postgres"); -const prisma = require("../prisma/prisma"); +import prisma from "../utils/prisma"; +import { Event } from "@prisma/client"; -async function getAllEvents() { +async function getAllEvents(): Promise { return prisma.event.findMany(); } -async function getEvents(limit, action, eventId) { - let events; +async function getEvents(limit: number, action: string, eventId: string): Promise { + let events: Event[]; switch (action) { case "prev": - events = await prisma.events.findMany({ + events = await prisma.event.findMany({ take: -1 * limit, skip: 1, // skip cursor (TODO: check if skip 1 is needed for previous page queries) cursor: { @@ -22,7 +23,7 @@ async function getEvents(limit, action, eventId) { }); break; case "next": - events = await prisma.events.findMany({ + events = await prisma.event.findMany({ take: limit, skip: 1, // skip cursor cursor: { @@ -35,7 +36,7 @@ async function getEvents(limit, action, eventId) { break; // first request made for first page, no action in cursor present default: - events = await prisma.events.findMany({ + events = await prisma.event.findMany({ take: limit, orderBy: { eventId: "asc", @@ -47,14 +48,14 @@ async function getEvents(limit, action, eventId) { return events; } -async function getPages(limit) { - const totalEvents = await prisma.events.count(); +async function getPages(limit: number): Promise { + const totalEvents = await prisma.event.count(); const totalPages = Math.ceil(totalEvents / limit); return totalPages; } -async function getEvent(eventId) { - const event = await prisma.events.findUnique({ +async function getEvent(eventId: string): Promise { + const event = await prisma.event.findUnique({ where: { eventId: eventId, }, @@ -62,7 +63,7 @@ async function getEvent(eventId) { return event; } -module.exports = { +export default { getEvent, getEvents, getPages, diff --git a/packages/server/controllers/guilds.js b/packages/server/controllers/guilds.js deleted file mode 100644 index 589981f0..00000000 --- a/packages/server/controllers/guilds.js +++ /dev/null @@ -1,20 +0,0 @@ -const prisma = require("../prisma/prisma"); - -async function getAllGuilds() { - const query = await prisma.guilds.findMany(); - return query; -} - -async function getGuild(guildId) { - const query = await prisma.guilds.findFirst({ - where: { - guildId: guildId, - }, - }); - return query; -} - -module.exports = { - getGuild, - getAllGuilds, -}; diff --git a/packages/server/controllers/guilds.ts b/packages/server/controllers/guilds.ts new file mode 100644 index 00000000..4368e370 --- /dev/null +++ b/packages/server/controllers/guilds.ts @@ -0,0 +1,21 @@ +import prisma from "../utils/prisma"; +import { Guild } from "@prisma/client"; + +async function getAllGuilds(): Promise { + const query = await prisma.guild.findMany(); + return query; +} + +async function getGuild(guildId: string): Promise { + const query = await prisma.guild.findUnique({ + where: { + guildId: guildId + } + }); + return query; +} + +export default { + getGuild, + getAllGuilds +}; diff --git a/packages/server/controllers/users.js b/packages/server/controllers/users.js deleted file mode 100644 index 1363bdac..00000000 --- a/packages/server/controllers/users.js +++ /dev/null @@ -1,30 +0,0 @@ -const prisma = require("../prisma/prisma"); - -async function getAllUsers() { - const query = await prisma.users.findMany(); - return query; -} - -async function getUser(userId) { - const query = await prisma.users.findUnique({ - where: { - userId: userId, - }, - }); - return query; -} - -async function getUserByEmail(email) { - const query = await prisma.users.findUnique({ - where: { - email: email, - }, - }); - return query; -} - -module.exports = { - getUser, - getAllUsers, - getUserByEmail, -}; diff --git a/packages/server/controllers/users.ts b/packages/server/controllers/users.ts new file mode 100644 index 00000000..3a76d7ae --- /dev/null +++ b/packages/server/controllers/users.ts @@ -0,0 +1,31 @@ +import prisma from "../utils/prisma"; +import { User } from "@prisma/client"; + +async function getAllUsers(): Promise { + const query = await prisma.user.findMany(); + return query; +} + +async function getUser(userId: string): Promise { + const query = await prisma.user.findUnique({ + where: { + userId: userId + } + }); + return query; +} + +async function getUserByEmail(email: string): Promise { + const query = await prisma.user.findUnique({ + where: { + email: email + } + }); + return query; +} + +export default { + getUser, + getAllUsers, + getUserByEmail +}; diff --git a/packages/server/index.ts b/packages/server/index.ts new file mode 100644 index 00000000..5af3af36 --- /dev/null +++ b/packages/server/index.ts @@ -0,0 +1,41 @@ +import dotenv from "dotenv"; +import express from "express"; +import cors from "cors"; +import cookieParser from "cookie-parser"; + +dotenv.config(); + +const app = express(); +const PORT = process.env.PORT || 5050; + +import users from "./routes/api/users"; +import guilds from "./routes/api/guilds"; +import events from "./routes/api/events"; +import auth from "./routes/api/auth"; +import images from "./routes/api/images"; + +app.use( + cors({ + origin: ["icebreak://", "http://localhost:8081"], + credentials: true + }) +); + +app.use(cookieParser()); +app.use(express.json({ limit: "20mb" })); + +app.get("/", async (request, response) => { + response.send("Hello SEA!"); +}); + +app.use("/api/auth", auth); +app.use("/api/users", users); +app.use("/api/guilds", guilds); +app.use("/api/events", events); +app.use("/api/media/images", images); + +const server = app.listen(PORT, () => { + console.log(`Server listening on port ${PORT}`); +}); + +module.exports = server; diff --git a/packages/server/package.json b/packages/server/package.json index ea010727..bd03c395 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -6,7 +6,8 @@ "license": "MIT", "private": true, "scripts": { - "start": "nodemon index.js", + "start": "ts-node index.ts", + "build": "yarn tsc .", "forward": "ttab node scripts/ngrok.js", "dev": "ttab yarn start", "test": "jest" @@ -26,13 +27,26 @@ "jsonwebtoken": "^8.5.1", "pg": "^8.8.0", "redis": "^4.6.7", + "ts-node": "^10.9.1", "uniqid": "^5.4.0", "uuid": "^9.0.0" }, "devDependencies": { + "@types/bcrypt": "^5.0.0", + "@types/cookie-parser": "^1.4.6", + "@types/cors": "^2.8.16", + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.4", + "@types/node": "^20.8.10", + "@types/uuid": "^9.0.5", + "@typescript-eslint/eslint-plugin": "^6.2.1", + "@typescript-eslint/parser": "^6.2.1", "axios": "^1.3.4", + "eslint": "^8.46.0", + "eslint-config-prettier": "^8.10.0", "jest": "^29.5.0", "nodemon": "^2.0.19", - "prisma": "^5.1.1" + "prisma": "^5.1.1", + "typescript": "^5.1.6" } } diff --git a/packages/server/prisma/prisma.js b/packages/server/prisma/prisma.js deleted file mode 100644 index 5bd4993b..00000000 --- a/packages/server/prisma/prisma.js +++ /dev/null @@ -1,3 +0,0 @@ -const { PrismaClient } = require("@prisma/client"); -const prisma = new PrismaClient(); -module.exports = prisma; diff --git a/packages/server/prisma/schema.prisma b/packages/server/prisma/schema.prisma index 10eeb079..03545cd7 100644 --- a/packages/server/prisma/schema.prisma +++ b/packages/server/prisma/schema.prisma @@ -7,65 +7,71 @@ datasource db { url = env("DB_URL") } -model users { - userId String @id @map("user_id") @db.Uuid - joinedDate DateTime? @map("joined_date") @db.Timestamp(6) - firstName String @map("first_name") @db.VarChar(50) - lastName String @map("last_name") @db.VarChar(50) - isNew Boolean @map("is_new") @default(true) - email String @unique @db.VarChar(255) - avatar String? @db.VarChar(255) - password String? @db.VarChar(255) - eventAttendees eventAttendees[] - guildMembers guildMembers[] +model User { + userId String @id @map("user_id") @db.Uuid + joinedDate DateTime? @map("joined_date") @db.Timestamp(6) + firstName String @map("first_name") @db.VarChar(50) + lastName String @map("last_name") @db.VarChar(50) + isNew Boolean @default(true) @map("is_new") + email String @unique @db.VarChar(255) + avatar String? @db.VarChar(255) + password String? @db.VarChar(255) + eventAttendees EventAttendee[] + guildMembers GuildMember[] + + @@map("users") } -model eventAttendees { - userId String @db.Uuid @map("user_id") - eventId String @db.Uuid @map("event_id") - events events @relation(fields: [eventId], references: [eventId], onDelete: Cascade, onUpdate: NoAction, map: "fk_event") - attendees users @relation(fields: [userId], references: [userId], onDelete: Cascade, onUpdate: NoAction, map: "fk_user") +model EventAttendee { + userId String @map("user_id") @db.Uuid + eventId String @map("event_id") @db.Uuid + event Event? @relation(fields: [eventId], references: [eventId], onDelete: Cascade, onUpdate: NoAction, map: "fk_event") + attendee User? @relation(fields: [userId], references: [userId], onDelete: Cascade, onUpdate: NoAction, map: "fk_user") @@id([userId, eventId]) @@map("event_attendees") } -model events { - eventId String @id(map: "event_pkey") @db.Uuid @map("event_id") - guildId String @db.Uuid @map("guild_id") - title String @db.VarChar(255) - description String? @db.VarChar(255) - startDate DateTime? @db.Timestamp(6) @map("start_date") - endDate DateTime? @db.Timestamp(6) @map("end_date") - location String? @db.VarChar(255) - thumbnail String? @db.VarChar(255) - eventAttendees eventAttendees[] - guilds guilds @relation(fields: [guildId], references: [guildId], onDelete: Cascade, onUpdate: Cascade, map: "event_guild_id_fkey") +model Event { + eventId String @id(map: "event_pkey") @map("event_id") @db.Uuid + guildId String @map("guild_id") @db.Uuid + title String @db.VarChar(255) + description String? @db.VarChar(255) + startDate DateTime? @map("start_date") @db.Timestamp(6) + endDate DateTime? @map("end_date") @db.Timestamp(6) + location String? @db.VarChar(255) + thumbnail String? @db.VarChar(255) + eventAttendees EventAttendee[] + guilds Guild @relation(fields: [guildId], references: [guildId], onDelete: Cascade, onUpdate: Cascade, map: "event_guild_id_fkey") + + @@map("events") } -model guildMembers { - userId String @db.Uuid @map("user_id") - guildId String @db.Uuid @map("guild_id") - guilds guilds @relation(fields: [guildId], references: [guildId], onDelete: Cascade, onUpdate: NoAction, map: "fk_guild") - members users @relation(fields: [userId], references: [userId], onDelete: Cascade, onUpdate: NoAction, map: "fk_user") +model GuildMember { + userId String @map("user_id") @db.Uuid + guildId String @map("guild_id") @db.Uuid + guilds Guild @relation(fields: [guildId], references: [guildId], onDelete: Cascade, onUpdate: NoAction, map: "fk_guild") + members User @relation(fields: [userId], references: [userId], onDelete: Cascade, onUpdate: NoAction, map: "fk_user") @@id([guildId, userId], map: "users_guilds_pkey") @@map("guild_members") } -model guilds { - guildId String @id(map: "guild_pkey") @db.Uuid @map("guild_id") - name String @db.VarChar(100) - handler String @db.VarChar(50) - description String - category String @db.VarChar(255) - location String? @db.VarChar(255) - website String? @db.VarChar(255) - tags String[] - banner String? @db.VarChar(255) - icon String? @db.VarChar(255) - media String[] - isInviteOnly Boolean @map("invite_only") - events events[] - members guildMembers[] +model Guild { + guildId String @id(map: "guild_pkey") @default(dbgenerated("gen_random_uuid()")) @map("guild_id") @db.Uuid + name String @db.VarChar(100) + handler String @db.VarChar(50) + description String + category String @db.VarChar(255) + location String? @db.VarChar(255) + website String? @db.VarChar(255) + tags String[] + banner String? @db.VarChar(255) + icon String? @db.VarChar(255) + media String[] + isInviteOnly Boolean @map("invite_only") + events Event[] + members GuildMember[] + + @@map("guilds") } diff --git a/packages/server/routes/api/auth.ts b/packages/server/routes/api/auth.ts new file mode 100644 index 00000000..1e909533 --- /dev/null +++ b/packages/server/routes/api/auth.ts @@ -0,0 +1,415 @@ +import express from "express"; +import bcrypt from "bcrypt"; +import token from "../../utils/token"; + +const router = express.Router(); + +import AuthController from "../../controllers/auth"; +import UserController from "../../controllers/users"; +import { APIRequest, APIResponse } from "../../types"; +import { checkInvalidToken, addToTokenBlacklist } from "../../utils/redis"; +import { RequestWithUser, UserPayload } from "../../types/auth"; + +type AuthResponse = { + user: UserPayload; + accessToken: string; + refreshToken: string; +}; + +router.get( + "/user", + AuthController.authenticate, + (request: RequestWithUser, response: APIResponse) => { + if (!request.user) { + response.status(500).json({ + status: "error", + message: "Could not authenticate user" + }); + return; + } + + const accessToken = token.generateAccessToken(request.user); + const refreshToken = token.generateRefreshToken(request.user); + + // destructuring so we don't send JWT iat and expiration properties in + // response + const { userId, firstName, lastName, avatar, email, isNew } = request.user; + + response.status(200).json({ + status: "success", + data: { + user: { + userId, + firstName, + lastName, + avatar, + email, + isNew + }, + accessToken, + refreshToken + } + }); + } +); + +type GoogleAuthReqBody = { + token: string; +}; + +router.post( + "/google", + AuthController.login, + async ( + request: RequestWithUser, + response: APIResponse + ) => { + if (!request.user) { + response.status(500).json({ + status: "error", + message: "Could not authenticate user" + }); + return; + } + + try { + const { userId, firstName, lastName, email, avatar, isNew } = + request.user; + + const accessToken = token.generateAccessToken(request.user); + const refreshToken = token.generateRefreshToken(request.user); + + response.status(200).json({ + status: "success", + data: { + user: { + userId, + firstName, + lastName, + avatar, + email, + isNew + }, + accessToken, + refreshToken + } + }); + } catch (error) { + if (error instanceof Error) { + response.status(500).json({ + status: "error", + message: error.message + }); + } else { + response.status(500).json({ + status: "error", + message: "Internal Server Error" + }); + } + } + } +); + +type LocalAuthReqBody = { + email: string; + password: string; +}; + +router.post( + "/local/register", + async ( + request: APIRequest, + response: APIResponse + ) => { + try { + const { email, password } = request.body; + + if (email == undefined) { + return response.status(400).json({ + status: "fail", + data: { + email: "Email not provided" + } + }); + } + + if (password === undefined) { + return response.status(400).json({ + status: "fail", + data: { + password: "Password not provided" + } + }); + } + + const emailRegex = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/g; + + if (!emailRegex.test(email)) { + // check if email is valid, doesn't include or no spaces + return response.status(400).json({ + status: "fail", + data: { + email: "Invalid email was provided" + } + }); + } + + const requestedUser = await UserController.getUserByEmail(email); + + if (requestedUser?.email === email) { + // check if email is already in the database + return response.status(400).json({ + status: "fail", + data: { + email: "A user with this email already exists." + } + }); + } + + await AuthController.register(request.body); + + // users will have to log in manually after successfully registering + response.status(200).json({ + status: "success", + data: null + }); + } catch (error) { + if (error instanceof Error) { + response.status(500).json({ + status: "error", + message: error.message + }); + } else { + response.status(500).json({ + status: "error", + message: "Internal Server Error" + }); + } + } + } +); + +router.post( + "/local", + async ( + request: APIRequest, + response: APIResponse + ) => { + try { + // get user input + const { email, password } = request.body; + + if (email == undefined) { + return response.status(400).json({ + status: "fail", + data: { + email: "Email not provided" + } + }); + } + + if (password == undefined) { + return response.status(400).json({ + status: "fail", + data: { + password: "Password not provided" + } + }); + } + + const emailRegex = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/g; + + if (!emailRegex.test(email)) { + // check if email is valid, doesn't include or no spaces + return response.status(400).json({ + status: "fail", + data: { + email: "Invalid email provided" + } + }); + } + + const requestedUser = await UserController.getUserByEmail(email); + + if (!requestedUser || requestedUser.email !== email) { + // check if email is in the database + return response.status(400).json({ + status: "fail", + data: { + email: "A user with that email does not exist." + } + }); + } + + const isValidPassword = await bcrypt.compare( + password, + requestedUser.password || "" + ); + + if (!isValidPassword) { + return response.status(400).json({ + status: "fail", + data: { + password: "Incorrect password" + } + }); + } + + const refreshToken = token.generateRefreshToken(requestedUser); + const accessToken = token.generateAccessToken(requestedUser); + + response.status(200).json({ + status: "success", + data: { + user: { + userId: requestedUser.userId, + firstName: requestedUser.firstName, + lastName: requestedUser.lastName, + email: requestedUser.email, + avatar: requestedUser.avatar, + isNew: requestedUser.isNew + }, + accessToken, + refreshToken + } + }); + } catch (error) { + if (error instanceof Error) { + response.status(500).json({ + status: "error", + message: error.message + }); + } else { + response.status(500).json({ + status: "error", + message: "Internal Server Error" + }); + } + } + } +); + +type TokenReqBody = { + refreshToken: string; +}; + +type TokenResponse = { + accessToken: string; + refreshToken: string; +}; + +router.post( + "/token", + async ( + request: APIRequest, + response: APIResponse + ) => { + try { + const { refreshToken } = request.body; + + if (!refreshToken) { + return response.status(400).json({ + status: "fail", + data: { + refreshToken: "Refresh token not provided" + } + }); + } + + const isInvalidToken = await checkInvalidToken(refreshToken); + + if (isInvalidToken) { + return response.status(401).json({ + status: "fail", + data: { + refreshToken: "Provided refresh token is revoked" + } + }); + } + + const { userId } = await token.verifyRefreshToken(refreshToken); + const user = await UserController.getUser(userId); + + if (!user) { + response.status(400).json({ + status: "fail", + data: { + refreshToken: "Invalid refresh token provided" + } + }); + return; + } + + const accessToken = token.generateAccessToken(user); + const newRefreshToken = token.generateRefreshToken(user); + + response.status(200).json({ + status: "success", + data: { + accessToken, + refreshToken: newRefreshToken + } + }); + } catch (error) { + if (error instanceof Error) { + response.status(500).json({ + status: "error", + message: error.message + }); + } else { + response.status(500).json({ + status: "error", + message: "Internal Server Error" + }); + } + } + } +); + +type RevokeTokenReqBody = { + refreshToken: string; +}; + +router.post( + "/token/revoke", + async ( + request: APIRequest, + response: APIResponse + ) => { + const { refreshToken } = request.body; + + if (!refreshToken) { + return response.status(400).json({ + status: "fail", + data: { + refreshToken: "Refresh token not provided" + } + }); + } + + try { + token.verifyRefreshToken(refreshToken); + + await addToTokenBlacklist(refreshToken); + + response.status(200).json({ + status: "success", + data: null + }); + } catch (error) { + if (error instanceof Error) { + response.status(500).json({ + status: "error", + message: error.message + }); + } else { + response.status(500).json({ + status: "error", + message: "Internal Server Error" + }); + } + } + } +); + +export default router; diff --git a/packages/server/routes/api/auth.js b/packages/server/routes/api/authjs.js similarity index 100% rename from packages/server/routes/api/auth.js rename to packages/server/routes/api/authjs.js diff --git a/packages/server/routes/api/events.js b/packages/server/routes/api/events.ts similarity index 52% rename from packages/server/routes/api/events.js rename to packages/server/routes/api/events.ts index 983df511..d14bf67c 100644 --- a/packages/server/routes/api/events.js +++ b/packages/server/routes/api/events.ts @@ -1,9 +1,24 @@ -const express = require("express"); +import express from "express"; const router = express.Router(); +const DEFAULT_EVENT_LIMIT: number = 10; -const EventController = require("../../controllers/events"); -const AuthController = require("../../controllers/auth"); -const DEFAULT_EVENT_LIMIT = 10; +import EventController from "../../controllers/events"; +import AuthController from "../../controllers/auth"; +import { APIRequest, APIResponse } from "../../types/index" +import { Event } from "@prisma/client"; + +type APIRequestCursor = { + cursor: string, +} + +type APIResponseCursor = { + events: Event[], + totalPages?: number, + cursor: { + previousPage: string | null, + nextPage: string | null, + } +} /** * cursor is base-64 encoded and formatted as @@ -14,9 +29,11 @@ const DEFAULT_EVENT_LIMIT = 10; router.get( "/pages/:cursor?", AuthController.authenticate, - async (request, response) => { + async (request: APIRequest, response: APIResponse) => { try { - if (request.query.limit && isNaN(request.query.limit)) { + const queryLimit: string = request.query.limit as string + + if (queryLimit && isNaN(parseInt(queryLimit))) { return response.status(400).json({ status: "fail", data: { @@ -25,17 +42,22 @@ router.get( }); } - const eventLimit = parseInt(request.query.limit) || DEFAULT_EVENT_LIMIT; + const eventLimit: number = parseInt(queryLimit) || DEFAULT_EVENT_LIMIT; // base64 decode cursor parameter if not null - const requestCursor = request.params.cursor + const requestCursor: string = request.params.cursor ? Buffer.from(request.params.cursor, "base64").toString() : ""; - let [currentPage, action, eventId] = requestCursor.split("___"); - currentPage = parseInt(currentPage) || 1; - const events = await EventController.getEvents( + + // let [currentPage, action, eventId] = requestCursor.split("___"); + const cursor: string[] = requestCursor.split("___"); + const currentPage: number = parseInt(cursor[0]) || 1; + const action: string = cursor[1] + const eventId: string = cursor[2] + + const events: Event[] = await EventController.getEvents( eventLimit, action, - eventId + eventId, ); if (events.length === 0) { @@ -45,12 +67,12 @@ router.get( // generate cursors for previous and next pages // const firstEventId = events[0].event_id; // const lastEventId = events[events.length - 1].event_id; - const firstEventId = events[0].eventId; - const lastEventId = events[events.length - 1].eventId; - const prevCursor = Buffer.from( + const firstEventId: string = events[0].eventId; + const lastEventId: string = events[events.length - 1].eventId; + const prevCursor: string = Buffer.from( `${currentPage - 1}___prev___${firstEventId}` ).toString("base64"); - const nextCursor = Buffer.from( + const nextCursor: string = Buffer.from( `${currentPage + 1}___next___${lastEventId}` ).toString("base64"); @@ -69,7 +91,7 @@ router.get( } else { // first request made to this route, determine total number of pages // null previous cursor on first page - const totalPages = await EventController.getPages(eventLimit); + const totalPages: number = await EventController.getPages(eventLimit); response.status(200).json({ status: "success", @@ -84,21 +106,37 @@ router.get( }); } } catch (error) { - response.status(500).json({ + if(error instanceof Error) { + response.status(500).json({ status: "error", message: error.message, - }); + }) + } + else { + response.status(500).json({ + status: "error", + message: "An unknown error has occured", + }); + } } } ); +type APIRequestGetEvent = { + eventId: string, +} + +type APIResponseGetEvent = { + event: Event | null, +} + router.get( "/:eventId", AuthController.authenticate, - async (request, response) => { + async (request: APIRequest, response: APIResponse) => { try { const { eventId } = request.params; - const event = await EventController.getEvent(eventId); + const event: Event | null = await EventController.getEvent(eventId); response.status(200).json({ status: "success", data: { @@ -106,12 +144,20 @@ router.get( }, }); } catch (error) { - response.status(500).json({ + if(error instanceof Error) { + response.status(500).json({ status: "error", message: error.message, - }); + }) + } + else { + response.status(500).json({ + status: "error", + message: "An unknown error has occured", + }); + } } } ); -module.exports = router; +export default router; diff --git a/packages/server/routes/api/guilds.js b/packages/server/routes/api/guilds.js deleted file mode 100644 index 08cc2f32..00000000 --- a/packages/server/routes/api/guilds.js +++ /dev/null @@ -1,56 +0,0 @@ -const express = require("express"); -const router = express.Router(); - -const GuildController = require("../../controllers/guilds"); -const AuthController = require("../../controllers/auth"); - -router.get("/", AuthController.authenticate, async (request, response) => { - try { - const guilds = await GuildController.getAllGuilds(); - response.status(200).json({ - status: "success", - data: { - guilds, - }, - }); - } catch (error) { - response.status(500).json({ - status: "error", - message: error.message, - }); - } -}); - -router.get( - "/:guildId", - AuthController.authenticate, - async (request, response) => { - try { - const { guildId } = request.params; - - if (guildId === undefined) { - return response.status(400).json({ - status: "fail", - data: { - guildId: "Guild ID not provided", - }, - }); - } - - const guild = await GuildController.getGuild(guildId); - response.status(200).json({ - status: "success", - data: { - guild, - }, - }); - } catch (error) { - response.status(500).json({ - status: "error", - message: error.message, - }); - } - } -); - -module.exports = router; diff --git a/packages/server/routes/api/guilds.ts b/packages/server/routes/api/guilds.ts new file mode 100644 index 00000000..a360fd97 --- /dev/null +++ b/packages/server/routes/api/guilds.ts @@ -0,0 +1,86 @@ +import express from "express"; +const router = express.Router(); + +import GuildController from "../../controllers/guilds"; +import AuthController from "../../controllers/auth"; +import { APIRequest, APIResponse } from "../../types/index" +import { Guild } from "@prisma/client"; + +type APIResponseAllGuilds = { + guilds: Guild[], +} + +router.get("/", AuthController.authenticate, +async (request: APIRequest, response: APIResponse) => { + try { + const guilds: Guild[] = await GuildController.getAllGuilds(); + response.status(200).json({ + status: "success", + data: { + guilds: guilds, + }, + }); + } catch (error) { + if(error instanceof Error) { + response.status(500).json({ + status: "error", + message: error.message, + }) + } + else { + response.status(500).json({ + status: "error", + message: "An unknown error has occured", + }); + } + } +}); + +type APIResponseGetGuild = { + guild: Guild | null, +} + +type APIRequestGetGuild = { + guildId: string, +} + +router.get( + "/:guildId", + AuthController.authenticate, + async (request: APIRequest, response: APIResponse) => { + try { + const { guildId } = request.params; + + if (guildId === undefined) { + return response.status(400).json({ + status: "fail", + data: { + guild: "Guild ID not provided", + }, + }); + } + + const guild = await GuildController.getGuild(guildId); + response.status(200).json({ + status: "success", + data: { + guild: guild, + }, + }); + } catch (error) { + if(error instanceof Error) { + response.status(500).json({ + status: "error", + message: error.message, + }) + } + else { + response.status(500).json({ + status: "error", + message: "An unknown error has occured", + }); + } + } + }); + +export default router; diff --git a/packages/server/routes/api/users.js b/packages/server/routes/api/users.ts similarity index 51% rename from packages/server/routes/api/users.js rename to packages/server/routes/api/users.ts index 9d0359df..99df4ec1 100644 --- a/packages/server/routes/api/users.js +++ b/packages/server/routes/api/users.ts @@ -1,8 +1,8 @@ -const express = require("express"); +import express from "express"; const router = express.Router(); -const UserController = require("../../controllers/users"); -const AuthController = require("../../controllers/auth"); +import UserController from "../../controllers/users"; +import AuthController from "../../controllers/auth"; router.get("/", async (request, response) => { try { @@ -10,14 +10,24 @@ router.get("/", async (request, response) => { response.status(200).json({ status: "success", data: { - users, - }, + users + } }); + return; } catch (error) { + if (error instanceof Error) { + response.status(500).json({ + status: "error", + message: error.message + }); + return; + } + response.status(500).json({ status: "error", - message: error.message, + message: "Internal Server Error" }); + return; } }); @@ -29,28 +39,38 @@ router.get( const { userId } = request.params; if (userId === undefined) { - return response.status(400).json({ + response.status(400).json({ status: "fail", data: { - userId: "User ID not provided", - }, + userId: "User ID not provided" + } }); + return; } const user = await UserController.getUser(userId); response.status(200).json({ status: "success", data: { - user: user, - }, + user: user + } }); } catch (error) { + if (error instanceof Error) { + response.status(500).json({ + status: "error", + message: error.message + }); + return; + } + response.status(500).json({ status: "error", - message: error.message, + message: "Internal Server Error" }); + return; } } ); -module.exports = router; +export default router; diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json new file mode 100644 index 00000000..62550cd6 --- /dev/null +++ b/packages/server/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "lib": ["ES2023"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./build", + "allowJs": true, + "module": "Node16", + "moduleResolution": "Node16", + "target": "ES2022" + }, + "exclude": ["./__tests__/**/*"], + "include": [ + "./controllers/**/*", + "./routes/**/*", + "./utils/**/*", + "./index.js" + ] +} diff --git a/packages/server/types/auth.ts b/packages/server/types/auth.ts new file mode 100644 index 00000000..9802e883 --- /dev/null +++ b/packages/server/types/auth.ts @@ -0,0 +1,37 @@ +import { APIRequest } from "."; + +// extending json web token payloads with necessary user properties +// for our authorization +declare module "jsonwebtoken" { + export interface JwtPayload { + userId: string; + firstName: string; + lastName: string; + avatar: string | null; + email: string; + } +} + +export type NewUser = { + email: string; + password: string; +}; + +export type RefreshTokenPayload = { + userId: string; +}; + +// user info stored in JWT payloads +export type UserPayload = { + userId: string; + firstName: string; + lastName: string; + avatar: string | null; + email: string; + isNew: boolean; +}; + +export interface RequestWithUser, V = void> + extends APIRequest { + user?: UserPayload; +} diff --git a/packages/server/types/index.ts b/packages/server/types/index.ts new file mode 100644 index 00000000..3b94c01e --- /dev/null +++ b/packages/server/types/index.ts @@ -0,0 +1,40 @@ +import { Send, Request, Response } from "express-serve-static-core"; + +/** + * @template T, V + * @param T - (optional) structure of request route parameters + * @param V - (optional) structure of request body + * + * void = optional generic parameters + */ +export type APIRequest, V = void> = Request< + T, + any, + V +>; + +/** + * @template T + * @param T - structure of response body + */ +export interface APIResponse extends Response { + json: Send< + SuccessResponseBody | FailResponseBody | ErrorResponseBody, + this + >; +} + +type SuccessResponseBody = { + status: "success"; + data: T; +}; + +type FailResponseBody = { + status: "fail"; + data: Record; +}; + +type ErrorResponseBody = { + status: "error"; + message: string; +}; diff --git a/packages/server/utils/postgres.js b/packages/server/utils/postgres.js deleted file mode 100644 index 4f2bc37f..00000000 --- a/packages/server/utils/postgres.js +++ /dev/null @@ -1,17 +0,0 @@ -const { Client: PostgresClient } = require("pg"); - -const postgres = new PostgresClient({ - host: process.env.DB_HOST, - user: process.env.DB_USER, - database: process.env.DB_NAME, - password: process.env.DB_PASSWORD, - port: process.env.DB_PORT, -}); - -postgres.connect(); - -function closePostgres() { - postgres.end(); -} - -module.exports = { postgres, closePostgres }; diff --git a/packages/server/utils/prisma.ts b/packages/server/utils/prisma.ts new file mode 100644 index 00000000..c957cebc --- /dev/null +++ b/packages/server/utils/prisma.ts @@ -0,0 +1,3 @@ +import { PrismaClient } from "@prisma/client"; +const prisma = new PrismaClient(); +export default prisma; diff --git a/packages/server/utils/prismaTS.ts b/packages/server/utils/prismaTS.ts deleted file mode 100644 index 15c3b933..00000000 --- a/packages/server/utils/prismaTS.ts +++ /dev/null @@ -1,26 +0,0 @@ -// prisma client initialization once we migrate backend to TypeScript -// delete "prisma.js" and rename this file to "prisma.ts" once migrated to -// TypeScript - -import { PrismaClient } from "@prisma/client"; - -declare global { - namespace NodeJS { - interface Global { - prisma: PrismaClient; - } - } -} - -let prisma: PrismaClient; - -if (!global.prisma) { - global.prisma = new PrismaClient({ - log: ["info"], - }); -} -prisma = global.prisma; - -export default prisma; -// Prevents hitting the limit on number of Prisma Clients instantiated while testing the code locally -// Achieves goal by setting a single global instance of Prisma Client to be used when local testing diff --git a/packages/server/utils/token.ts b/packages/server/utils/token.ts new file mode 100644 index 00000000..667a2134 --- /dev/null +++ b/packages/server/utils/token.ts @@ -0,0 +1,69 @@ +import { User } from "@prisma/client"; +import jwt from "jsonwebtoken"; +import { UserPayload } from "../types/auth"; + +function generateRefreshToken(user: User | UserPayload) { + const { userId } = user; + return jwt.sign( + { + userId + }, + process.env.TOKEN_SECRET!, + { + expiresIn: "1d" + } + ); +} + +function generateAccessToken(user: User | UserPayload) { + const { userId, firstName, lastName, avatar, email } = user; + return jwt.sign( + { + userId, + firstName, + lastName, + avatar, + email + }, + process.env.WEB_CLIENT_SECRET!, + { + expiresIn: "1h" + } + ); +} + +function verifyRefreshToken(refreshToken: string): Promise { + return new Promise((resolve, reject) => { + jwt.verify(refreshToken, process.env.TOKEN_SECRET!, (err, decoded) => { + if (err) { + reject(err); + } + + resolve(decoded as UserPayload); + }); + }); +} + +async function verifyAccessToken(accessToken: string): Promise { + return new Promise((resolve, reject) => { + jwt.verify( + accessToken, + process.env.WEB_CLIENT_SECRET!, + function (err, decoded) { + if (err) { + reject(err); + } + // Token is valid + // resolve promise with the payload of the access token + resolve(decoded as UserPayload); + } + ); + }); +} + +export default { + generateRefreshToken, + generateAccessToken, + verifyRefreshToken, + verifyAccessToken +}; diff --git a/packages/server/utils/token.js b/packages/server/utils/tokenjs.js similarity index 100% rename from packages/server/utils/token.js rename to packages/server/utils/tokenjs.js diff --git a/yarn.lock b/yarn.lock index c43d79ee..411389bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1919,7 +1919,7 @@ dependencies: "@types/hammerjs" "^2.0.36" -"@eslint-community/eslint-utils@^4.2.0": +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== @@ -1931,6 +1931,11 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.8.1.tgz#8c4bb756cc2aa7eaf13cfa5e69c83afb3260c20c" integrity sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ== +"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": + version "4.6.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" + integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== + "@eslint/eslintrc@^2.0.2": version "2.1.2" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396" @@ -1946,11 +1951,31 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/eslintrc@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.1.tgz#18d635e24ad35f7276e8a49d135c7d3ca6a46f93" + integrity sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + "@eslint/js@8.39.0": version "8.39.0" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b" integrity sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng== +"@eslint/js@^8.46.0": + version "8.46.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.46.0.tgz#3f7802972e8b6fe3f88ed1aabc74ec596c456db6" + integrity sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA== + "@expo/bunyan@4.0.0", "@expo/bunyan@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@expo/bunyan/-/bunyan-4.0.0.tgz#be0c1de943c7987a9fbd309ea0b1acd605890c7b" @@ -2287,6 +2312,15 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@humanwhocodes/config-array@^0.11.10": + version "0.11.10" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" + integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.5" + "@humanwhocodes/config-array@^0.11.8": version "0.11.11" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844" @@ -3490,6 +3524,72 @@ dependencies: "@babel/types" "^7.20.7" +"@types/bcrypt@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-5.0.0.tgz#a835afa2882d165aff5690893db314eaa98b9f20" + integrity sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw== + dependencies: + "@types/node" "*" + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/cookie-parser@^1.4.6": + version "1.4.6" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.6.tgz#002643c514cccf883a65cbe044dbdc38c0b92ade" + integrity sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w== + dependencies: + "@types/express" "*" + +"@types/cors@^2.8.16": + version "2.8.16" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.16.tgz#a24bf65acd216c078890ca6ceb91e672adb158e7" + integrity sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.17.35" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz#c95dd4424f0d32e525d23812aa8ab8e4d3906c4f" + integrity sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@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== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/graceful-fs@^4.1.3": version "4.1.6" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" @@ -3502,6 +3602,11 @@ resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.41.tgz#f6ecf57d1b12d2befcce00e928a6a097c22980aa" integrity sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA== +"@types/http-errors@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.1.tgz#20172f9578b225f6c7da63446f56d4ce108d5a65" + integrity sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ== + "@types/invariant@^2.2.35": version "2.2.35" resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.35.tgz#cd3ebf581a6557452735688d8daba6cf0bd5a3be" @@ -3526,6 +3631,28 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/json-schema@^7.0.12": + version "7.0.12" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" + integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== + +"@types/jsonwebtoken@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.4.tgz#8b74bbe87bde81a3469d4b32a80609bec62c23ec" + integrity sha512-8UYapdmR0QlxgvJmyE8lP7guxD0UGVMfknsdtCFZh4ovShdBl3iOI4zdvqBHrB/IS+xUj3PSx73Qkey1fhWz+g== + dependencies: + "@types/node" "*" + +"@types/mime@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" + integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== + +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + "@types/minimist@^1.2.0": version "1.2.5" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" @@ -3541,21 +3668,65 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.1.tgz#178d58ee7e4834152b0e8b4d30cbfab578b9bb30" integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== +"@types/node@^20.8.10": + version "20.8.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.10.tgz#a5448b895c753ae929c26ce85cab557c6d4a365e" + integrity sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w== + dependencies: + undici-types "~5.26.4" + "@types/normalize-package-data@^2.4.0": version "2.4.4" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== +"@types/qs@*": + version "6.9.7" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + "@types/qs@^6.5.3": version "6.9.8" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.8.tgz#f2a7de3c107b89b441e071d5472e6b726b4adf45" integrity sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg== +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/semver@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" + integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== + +"@types/send@*": + version "0.17.1" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301" + integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.2" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.2.tgz#3e5419ecd1e40e7405d34093f10befb43f63381a" + integrity sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw== + dependencies: + "@types/http-errors" "*" + "@types/mime" "*" + "@types/node" "*" + "@types/stack-utils@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/uuid@^9.0.5": + version "9.0.5" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.5.tgz#25a71eb73eba95ac0e559ff3dd018fc08294acf6" + integrity sha512-xfHdwa1FMJ082prjSJpoEI57GZITiQz10r3vEJCHa2khEFQjKy91aWKz6+zybzssCvXUwE1LQWgWVwZ4nYUvHQ== + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -3582,6 +3753,92 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/eslint-plugin@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.2.1.tgz#41b79923fee46a745a3a50cba1c33c622aa3c79a" + integrity sha512-iZVM/ALid9kO0+I81pnp1xmYiFyqibAHzrqX4q5YvvVEyJqY+e6rfTXSCsc2jUxGNqJqTfFSSij/NFkZBiBzLw== + dependencies: + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.2.1" + "@typescript-eslint/type-utils" "6.2.1" + "@typescript-eslint/utils" "6.2.1" + "@typescript-eslint/visitor-keys" "6.2.1" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + natural-compare-lite "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/parser@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.2.1.tgz#e18a31eea1cca8841a565f1701960c8123ed07f9" + integrity sha512-Ld+uL1kYFU8e6btqBFpsHkwQ35rw30IWpdQxgOqOh4NfxSDH6uCkah1ks8R/RgQqI5hHPXMaLy9fbFseIe+dIg== + dependencies: + "@typescript-eslint/scope-manager" "6.2.1" + "@typescript-eslint/types" "6.2.1" + "@typescript-eslint/typescript-estree" "6.2.1" + "@typescript-eslint/visitor-keys" "6.2.1" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.2.1.tgz#b6f43a867b84e5671fe531f2b762e0b68f7cf0c4" + integrity sha512-UCqBF9WFqv64xNsIEPfBtenbfodPXsJ3nPAr55mGPkQIkiQvgoWNo+astj9ZUfJfVKiYgAZDMnM6dIpsxUMp3Q== + dependencies: + "@typescript-eslint/types" "6.2.1" + "@typescript-eslint/visitor-keys" "6.2.1" + +"@typescript-eslint/type-utils@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.2.1.tgz#8eb8a2cccdf39cd7cf93e02bd2c3782dc90b0525" + integrity sha512-fTfCgomBMIgu2Dh2Or3gMYgoNAnQm3RLtRp+jP7A8fY+LJ2+9PNpi5p6QB5C4RSP+U3cjI0vDlI3mspAkpPVbQ== + dependencies: + "@typescript-eslint/typescript-estree" "6.2.1" + "@typescript-eslint/utils" "6.2.1" + debug "^4.3.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/types@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.2.1.tgz#7fcdeceb503aab601274bf5e210207050d88c8ab" + integrity sha512-528bGcoelrpw+sETlyM91k51Arl2ajbNT9L4JwoXE2dvRe1yd8Q64E4OL7vHYw31mlnVsf+BeeLyAZUEQtqahQ== + +"@typescript-eslint/typescript-estree@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.2.1.tgz#2af6e90c1e91cb725a5fe1682841a3f74549389e" + integrity sha512-G+UJeQx9AKBHRQBpmvr8T/3K5bJa485eu+4tQBxFq0KoT22+jJyzo1B50JDT9QdC1DEmWQfdKsa8ybiNWYsi0Q== + dependencies: + "@typescript-eslint/types" "6.2.1" + "@typescript-eslint/visitor-keys" "6.2.1" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/utils@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.2.1.tgz#2aa4279ec13053d05615bcbde2398e1e8f08c334" + integrity sha512-eBIXQeupYmxVB6S7x+B9SdBeB6qIdXKjgQBge2J+Ouv8h9Cxm5dHf/gfAZA6dkMaag+03HdbVInuXMmqFB/lKQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.2.1" + "@typescript-eslint/types" "6.2.1" + "@typescript-eslint/typescript-estree" "6.2.1" + semver "^7.5.4" + +"@typescript-eslint/visitor-keys@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.2.1.tgz#442e7c09fe94b715a54ebe30e967987c3c41fbf4" + integrity sha512-iTN6w3k2JEZ7cyVdZJTVJx2Lv7t6zFA8DCrJEHD2mwfc16AEvvBWVhbFh34XyG2NORCd0viIgQY1+u7kPI0WpA== + dependencies: + "@typescript-eslint/types" "6.2.1" + eslint-visitor-keys "^3.4.1" + "@urql/core@2.3.6": version "2.3.6" resolved "https://registry.yarnpkg.com/@urql/core/-/core-2.3.6.tgz#ee0a6f8fde02251e9560c5f17dce5cd90f948552" @@ -5498,6 +5755,11 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +eslint-config-prettier@^8.10.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" + integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== + eslint-config-prettier@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz#eb25485946dd0c66cd216a46232dc05451518d1f" @@ -5537,7 +5799,7 @@ eslint-plugin-react@7.32.2: semver "^6.3.0" string.prototype.matchall "^4.0.8" -eslint-scope@^7.2.0: +eslint-scope@^7.2.0, eslint-scope@^7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== @@ -5545,11 +5807,16 @@ eslint-scope@^7.2.0: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.0, eslint-visitor-keys@^3.4.1: +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.0: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== +eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz#8c2095440eca8c933bedcadf16fefa44dbe9ba5f" + integrity sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw== + eslint@8.39.0: version "8.39.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.39.0.tgz#7fd20a295ef92d43809e914b70c39fd5a23cf3f1" @@ -5596,7 +5863,50 @@ eslint@8.39.0: strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.5.1, espree@^9.6.0: +eslint@^8.46.0: + version "8.46.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.46.0.tgz#a06a0ff6974e53e643acc42d1dcf2e7f797b3552" + integrity sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.1" + "@eslint/js" "^8.46.0" + "@humanwhocodes/config-array" "^0.11.10" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.2" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^9.5.1, espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -6555,7 +6865,7 @@ globalthis@^1.0.3: dependencies: define-properties "^1.1.3" -globby@^11.0.1: +globby@^11.0.1, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -6606,6 +6916,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + graphql-tag@^2.10.1: version "2.12.6" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" @@ -6822,7 +7137,7 @@ ignore-by-default@^1.0.1: resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== -ignore@^5.2.0: +ignore@^5.2.0, ignore@^5.2.4: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== @@ -9048,6 +9363,11 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +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" @@ -9395,7 +9715,7 @@ open@^8.0.4, open@^8.3.0: is-docker "^2.1.1" is-wsl "^2.2.0" -optionator@^0.9.1: +optionator@^0.9.1, optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== @@ -11399,12 +11719,17 @@ trim-newlines@^3.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== +ts-api-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.1.tgz#8144e811d44c749cd65b2da305a032510774452d" + integrity sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A== + ts-interface-checker@^0.1.9: version "0.1.13" resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -ts-node@^10.8.1: +ts-node@^10.8.1, ts-node@^10.9.1: version "10.9.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== @@ -11547,6 +11872,11 @@ typed-array-length@^1.0.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== +typescript@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== + ua-parser-js@^1.0.35: version "1.0.36" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.36.tgz#a9ab6b9bd3a8efb90bb0816674b412717b7c428c" @@ -11582,6 +11912,11 @@ undefsafe@^2.0.5: resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"