diff --git a/apps/api/package.json b/apps/api/package.json index edcfd48..5af9da5 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -10,7 +10,7 @@ "test": "ts-node --test test/**/*.test.ts", "start": "node dist/index.js", "dev": "nodemon --delay 2000ms src/index.ts", - "prisma:generate": "dotenv -e ../../.env.local -- prisma generate", + "prisma:generate": "prisma generate", "prisma:migrate": "dotenv -e ../../.env.local -- prisma migrate dev", "prisma:init": "dotenv -e ../../.env.local -- prisma migrate dev --name init", "build": "tsc" @@ -19,19 +19,25 @@ "author": "", "license": "ISC", "dependencies": { + "@fastify/cookie": "^11.0.2", "@fastify/cors": "^11.0.0", + "@keyv/memcache": "^2.0.1", "@prisma/client": "^6.4.1", "dotenv": "^16.4.7", "fastify": "^5.2.1", "fastify-plugin": "^5.0.0", "graphql": "^16.10.0", + "jsonwebtoken": "^9.0.2", "mercurius": "^16.1.0", "mercurius-codegen": "^6.0.1", + "minio": "^8.0.4", + "uuid": "^11.1.0", "zod": "^3.24.2" }, "devDependencies": { "@repo/eslint-config": "*", "@repo/typescript-config": "*", + "@types/jsonwebtoken": "^9.0.9", "prisma": "^6.4.1", "typescript": "5.8.2" } diff --git a/apps/api/prisma/migrations/20250307053052_update_content_type/migration.sql b/apps/api/prisma/migrations/20250307053052_update_content_type/migration.sql deleted file mode 100644 index 3751c22..0000000 --- a/apps/api/prisma/migrations/20250307053052_update_content_type/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE `Message` MODIFY `content` TEXT NOT NULL; diff --git a/apps/api/prisma/migrations/20250306073430_init/migration.sql b/apps/api/prisma/migrations/20250309163226_init/migration.sql similarity index 74% rename from apps/api/prisma/migrations/20250306073430_init/migration.sql rename to apps/api/prisma/migrations/20250309163226_init/migration.sql index c45d68c..1087532 100644 --- a/apps/api/prisma/migrations/20250306073430_init/migration.sql +++ b/apps/api/prisma/migrations/20250309163226_init/migration.sql @@ -3,7 +3,8 @@ CREATE TABLE `User` ( `id` VARCHAR(191) NOT NULL, `email` VARCHAR(191) NOT NULL, `username` VARCHAR(191) NOT NULL, - `password` VARCHAR(191) NOT NULL, + `password` TEXT NOT NULL, + `s3ProfilePicObjectKey` VARCHAR(191) NULL, `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `updatedAt` DATETIME(3) NOT NULL, @@ -28,7 +29,7 @@ CREATE TABLE `Room` ( -- CreateTable CREATE TABLE `Message` ( `id` VARCHAR(191) NOT NULL, - `content` VARCHAR(191) NOT NULL, + `content` TEXT NOT NULL, `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `updatedAt` DATETIME(3) NOT NULL, `userId` VARCHAR(191) NOT NULL, @@ -37,6 +38,21 @@ CREATE TABLE `Message` ( PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +-- CreateTable +CREATE TABLE `RefreshToken` ( + `id` VARCHAR(191) NOT NULL, + `jti` VARCHAR(191) NOT NULL, + `userId` VARCHAR(191) NOT NULL, + `deviceId` VARCHAR(191) NOT NULL, + `hash` VARCHAR(191) NOT NULL, + `expiresAt` DATETIME(3) NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + UNIQUE INDEX `RefreshToken_jti_key`(`jti`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + -- CreateTable CREATE TABLE `_RoomMembers` ( `A` VARCHAR(191) NOT NULL, @@ -55,6 +71,9 @@ ALTER TABLE `Message` ADD CONSTRAINT `Message_userId_fkey` FOREIGN KEY (`userId` -- AddForeignKey ALTER TABLE `Message` ADD CONSTRAINT `Message_roomId_fkey` FOREIGN KEY (`roomId`) REFERENCES `Room`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; +-- AddForeignKey +ALTER TABLE `RefreshToken` ADD CONSTRAINT `RefreshToken_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE `_RoomMembers` ADD CONSTRAINT `_RoomMembers_A_fkey` FOREIGN KEY (`A`) REFERENCES `Room`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index d7a4c36..112080e 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -14,15 +14,17 @@ datasource db { } model User { - id String @id @default(uuid()) - email String @unique - username String @unique - password String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - messages Message[] - rooms Room[] @relation("RoomMembers") - ownedRooms Room[] @relation("RoomOwner") + id String @id @default(uuid()) + email String @unique + username String @unique + password String @db.Text + s3ProfilePicObjectKey String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + messages Message[] + rooms Room[] @relation("RoomMembers") + ownedRooms Room[] @relation("RoomOwner") + refreshTokens RefreshToken[] } model Room { @@ -48,3 +50,15 @@ model Message { roomId String room Room @relation(fields: [roomId], references: [id]) } + +model RefreshToken { + id String @id @default(uuid()) + jti String @unique + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + deviceId String + hash String + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts new file mode 100644 index 0000000..0382246 --- /dev/null +++ b/apps/api/src/config.ts @@ -0,0 +1,68 @@ +import { z } from 'zod'; +import dotenv from 'dotenv'; +import path from 'path'; + +const rootDir = path.resolve(process.cwd(), '../../'); +dotenv.config({ path: `${rootDir}/.env.local` }); + +const schema = z + .object({ + ALLOWED_ORIGINS: z.string(), + API_HOST: z.string(), + API_PORT: z.coerce.number(), + COOKIE_SECRET: z.string(), + MINIO_ENDPOINT: z.string(), + MINIO_PORT: z.coerce.number(), + MINIO_ACCESS_KEY: z.string(), + MINIO_SECRET_KEY: z.string(), + MINIO_BUCKET_NAME: z.string(), + MINIO_REGION: z.string().default('us-east-1'), + MINIO_USE_SSL: z.string().transform((val) => val === 'true'), + MEMC_HOST: z.string(), + MEMC_PORT: z.coerce.number(), + MEMC_TTL: z.coerce.number().default(30 * 60), // 30 minutes in seconds + MEMC_NAMESPACE: z.string().default('unreal-chat'), + NODE_ENV: z.enum(['development', 'staging', 'production']), + TOKEN_ACCESS_EXPIRES_IN: z.coerce.number().default(30 * 60), // 30 minutes in seconds + TOKEN_REFRESH_EXPIRES_IN: z.coerce.number().default(7 * 24 * 60 * 60), // 7 days in seconds + TOKEN_SECRET: z.string(), + }) + .transform((env) => { + return { + isProduction: env.NODE_ENV === 'production', + server: { + allowedOrigins: env.ALLOWED_ORIGINS.split(','), + port: env.API_PORT, + host: env.API_HOST, + }, + minio: { + endPoint: env.MINIO_ENDPOINT, + port: env.MINIO_PORT, + useSSL: env.MINIO_USE_SSL, + accessKey: env.MINIO_ACCESS_KEY, + secretKey: env.MINIO_SECRET_KEY, + bucketName: env.MINIO_BUCKET_NAME, + region: env.MINIO_REGION, + }, + token: { + accessTokenExpiresIn: env.TOKEN_ACCESS_EXPIRES_IN, + refreshTokenExpiresIn: env.TOKEN_REFRESH_EXPIRES_IN, + secret: env.TOKEN_SECRET, + }, + memc: { + host: env.MEMC_HOST, + port: env.MEMC_PORT, + ttl: env.MEMC_TTL, + namespace: env.MEMC_NAMESPACE, + }, + cookie: { + secret: env.COOKIE_SECRET, + }, + }; + }); + +export type Config = z.infer; +export type MinioConfig = Config['minio']; +export type TokenConfig = Config['token']; +export type MemcConfig = Config['memc']; +export default schema.parse(process.env); diff --git a/apps/api/src/generated/graphql.ts b/apps/api/src/generated/graphql.ts index 08a2d13..94bb6ab 100644 --- a/apps/api/src/generated/graphql.ts +++ b/apps/api/src/generated/graphql.ts @@ -42,6 +42,7 @@ export type User = { id: Scalars["ID"]; email: Scalars["String"]; username: Scalars["String"]; + s3ProfilePicObjectKey?: Maybe; createdAt: Scalars["DateTime"]; updatedAt?: Maybe; messages?: Maybe>; @@ -74,10 +75,23 @@ export type Message = { room?: Maybe; }; +export type RefreshTokenPayload = { + __typename?: "RefreshTokenPayload"; + accessToken: Scalars["String"]; + refreshToken: Scalars["String"]; +}; + export type AuthPayload = { __typename?: "AuthPayload"; - token: Scalars["String"]; user: User; + accessToken: Scalars["String"]; + refreshToken: Scalars["String"]; +}; + +export type PresignedUrl = { + __typename?: "PresignedUrl"; + url: Scalars["String"]; + expiresIn: Scalars["Int"]; }; export type Query = { @@ -88,6 +102,8 @@ export type Query = { rooms: Array; room?: Maybe; messages: Array; + getProfilePicUploadUrl: PresignedUrl; + getProfilePicUrl?: Maybe; }; export type QueryuserArgs = { @@ -102,25 +118,43 @@ export type QuerymessagesArgs = { roomId: Scalars["ID"]; }; +export type QuerygetProfilePicUploadUrlArgs = { + fileExtension: Scalars["String"]; +}; + export type Mutation = { __typename?: "Mutation"; register: AuthPayload; login: AuthPayload; + logout: Scalars["Boolean"]; + refreshToken: AuthPayload; + logoutAllDevices: Scalars["Boolean"]; createRoom: Room; joinRoom: Room; leaveRoom: Scalars["Boolean"]; sendMessage: Message; + deleteProfilePic: Scalars["Boolean"]; }; export type MutationregisterArgs = { email: Scalars["String"]; username: Scalars["String"]; password: Scalars["String"]; + deviceId: Scalars["String"]; }; export type MutationloginArgs = { email: Scalars["String"]; password: Scalars["String"]; + deviceId: Scalars["String"]; +}; + +export type MutationlogoutArgs = { + deviceId: Scalars["String"]; +}; + +export type MutationrefreshTokenArgs = { + deviceId: Scalars["String"]; }; export type MutationcreateRoomArgs = { @@ -260,7 +294,10 @@ export type ResolversTypes = { Room: ResolverTypeWrapper; Boolean: ResolverTypeWrapper; Message: ResolverTypeWrapper; + RefreshTokenPayload: ResolverTypeWrapper; AuthPayload: ResolverTypeWrapper; + PresignedUrl: ResolverTypeWrapper; + Int: ResolverTypeWrapper; Query: ResolverTypeWrapper<{}>; Mutation: ResolverTypeWrapper<{}>; Subscription: ResolverTypeWrapper<{}>; @@ -275,7 +312,10 @@ export type ResolversParentTypes = { Room: Room; Boolean: Scalars["Boolean"]; Message: Message; + RefreshTokenPayload: RefreshTokenPayload; AuthPayload: AuthPayload; + PresignedUrl: PresignedUrl; + Int: Scalars["Int"]; Query: {}; Mutation: {}; Subscription: {}; @@ -294,6 +334,11 @@ export type UserResolvers< id?: Resolver; email?: Resolver; username?: Resolver; + s3ProfilePicObjectKey?: Resolver< + Maybe, + ParentType, + ContextType + >; createdAt?: Resolver; updatedAt?: Resolver< Maybe, @@ -371,13 +416,34 @@ export type MessageResolvers< isTypeOf?: IsTypeOfResolverFn; }; +export type RefreshTokenPayloadResolvers< + ContextType = MercuriusContext, + ParentType extends + ResolversParentTypes["RefreshTokenPayload"] = ResolversParentTypes["RefreshTokenPayload"], +> = { + accessToken?: Resolver; + refreshToken?: Resolver; + isTypeOf?: IsTypeOfResolverFn; +}; + export type AuthPayloadResolvers< ContextType = MercuriusContext, ParentType extends ResolversParentTypes["AuthPayload"] = ResolversParentTypes["AuthPayload"], > = { - token?: Resolver; user?: Resolver; + accessToken?: Resolver; + refreshToken?: Resolver; + isTypeOf?: IsTypeOfResolverFn; +}; + +export type PresignedUrlResolvers< + ContextType = MercuriusContext, + ParentType extends + ResolversParentTypes["PresignedUrl"] = ResolversParentTypes["PresignedUrl"], +> = { + url?: Resolver; + expiresIn?: Resolver; isTypeOf?: IsTypeOfResolverFn; }; @@ -407,6 +473,17 @@ export type QueryResolvers< ContextType, RequireFields >; + getProfilePicUploadUrl?: Resolver< + ResolversTypes["PresignedUrl"], + ParentType, + ContextType, + RequireFields + >; + getProfilePicUrl?: Resolver< + Maybe, + ParentType, + ContextType + >; }; export type MutationResolvers< @@ -418,13 +495,33 @@ export type MutationResolvers< ResolversTypes["AuthPayload"], ParentType, ContextType, - RequireFields + RequireFields< + MutationregisterArgs, + "email" | "username" | "password" | "deviceId" + > >; login?: Resolver< ResolversTypes["AuthPayload"], ParentType, ContextType, - RequireFields + RequireFields + >; + logout?: Resolver< + ResolversTypes["Boolean"], + ParentType, + ContextType, + RequireFields + >; + refreshToken?: Resolver< + ResolversTypes["AuthPayload"], + ParentType, + ContextType, + RequireFields + >; + logoutAllDevices?: Resolver< + ResolversTypes["Boolean"], + ParentType, + ContextType >; createRoom?: Resolver< ResolversTypes["Room"], @@ -450,6 +547,11 @@ export type MutationResolvers< ContextType, RequireFields >; + deleteProfilePic?: Resolver< + ResolversTypes["Boolean"], + ParentType, + ContextType + >; }; export type SubscriptionResolvers< @@ -483,7 +585,9 @@ export type Resolvers = { User?: UserResolvers; Room?: RoomResolvers; Message?: MessageResolvers; + RefreshTokenPayload?: RefreshTokenPayloadResolvers; AuthPayload?: AuthPayloadResolvers; + PresignedUrl?: PresignedUrlResolvers; Query?: QueryResolvers; Mutation?: MutationResolvers; Subscription?: SubscriptionResolvers; @@ -515,6 +619,12 @@ export interface Loaders< id?: LoaderResolver; email?: LoaderResolver; username?: LoaderResolver; + s3ProfilePicObjectKey?: LoaderResolver< + Maybe, + User, + {}, + TContext + >; createdAt?: LoaderResolver; updatedAt?: LoaderResolver, User, {}, TContext>; messages?: LoaderResolver>, User, {}, TContext>; @@ -550,9 +660,30 @@ export interface Loaders< room?: LoaderResolver, Message, {}, TContext>; }; + RefreshTokenPayload?: { + accessToken?: LoaderResolver< + Scalars["String"], + RefreshTokenPayload, + {}, + TContext + >; + refreshToken?: LoaderResolver< + Scalars["String"], + RefreshTokenPayload, + {}, + TContext + >; + }; + AuthPayload?: { - token?: LoaderResolver; user?: LoaderResolver; + accessToken?: LoaderResolver; + refreshToken?: LoaderResolver; + }; + + PresignedUrl?: { + url?: LoaderResolver; + expiresIn?: LoaderResolver; }; } declare module "mercurius" { diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index eb0e146..18e2a6f 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,58 +1,82 @@ -import dotenv from 'dotenv'; import './types'; -import fastify, { FastifyRequest } from 'fastify'; +import fastify, { FastifyReply, FastifyRequest } from 'fastify'; +import fastifyCookie from '@fastify/cookie'; +import fastifyCors from '@fastify/cors'; import mercurius from 'mercurius'; import mercuriusCodegen from 'mercurius-codegen'; -import schema from './schema'; -import { resolvers } from './resolvers'; import { PrismaClient } from '@prisma/client'; -import fastifyCors from '@fastify/cors'; -import { z } from 'zod'; +import config from './config'; +import { resolvers } from './resolvers'; +import schema from './schema'; +import { MemcService, MinioService, TokenService } from './services'; -dotenv.config({ path: '../../.env.local' }); - -const { allowedOrigins, port, host } = z - .object({ - ALLOWED_ORIGINS: z.string(), - API_HOST: z.string(), - API_PORT: z.coerce.number(), - }) - .transform((env) => { - return { - allowedOrigins: env.ALLOWED_ORIGINS.split(','), - port: env.API_PORT, - host: env.API_HOST, - }; - }) - .parse(process.env); +const { + server: { allowedOrigins, ...serverConfig }, + minio: minioConfig, + token: tokenConfig, + memc: memcConfig, + cookie: cookieConfig, +} = config; const app = fastify({ logger: true, - exposeHeadRoutes: true, }); const prisma = new PrismaClient(); +const minio = new MinioService(minioConfig); +const memc = new MemcService(memcConfig); +const token = new TokenService({ ...tokenConfig, memc, prisma }); -const context = async (req: FastifyRequest) => { - const userId = (req.headers['user-id'] as string) || null; +setInterval( + () => { + token.cleanupExpiredTokens().catch((err) => { + app.log.error('Failed to clean up expired tokens:', err); + }); + }, + 60 * 60 * 1000 +); // 1 hour + +const context = async (req: FastifyRequest, reply: FastifyReply) => { + const { authorization } = req.headers; + const accessToken = authorization?.replace('Bearer ', ''); + const jwt = await token.verifyAccessToken(accessToken); + await minio.initialize(); return { prisma, - userId, + jwt, + minio, + token, + memc, + req, + reply, }; }; +app.register(fastifyCookie, { + secret: cookieConfig.secret, + parseOptions: { + secure: config.isProduction, + httpOnly: config.isProduction, + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 30, // 30 days + signed: true, + }, +}); + app.register(fastifyCors, { origin: (origin, callback) => { if (!origin || allowedOrigins.includes(origin)) return callback(null, true); return callback(new Error('Not allowed'), false); }, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization', 'user-id'], + allowedHeaders: ['Content-Type', 'Authorization'], }); app.register(mercurius, { schema, subscription: true, + jit: 1, + cache: true, context, }); @@ -75,10 +99,10 @@ mercuriusCodegen(app, { }, }); -app.listen({ host, port }, (err, address) => { +app.listen({ ...serverConfig }, (err, address) => { if (err) { console.error(err); process.exit(1); } - console.log(`Server listening at ${address}`); + console.log(`Server listening at ${address} with config:`, config); }); diff --git a/apps/api/src/resolvers/message.ts b/apps/api/src/resolvers/message.ts index 988eb3d..393609f 100644 --- a/apps/api/src/resolvers/message.ts +++ b/apps/api/src/resolvers/message.ts @@ -1,112 +1,72 @@ import { GraphQLError } from 'graphql'; import { IResolvers, withFilter } from 'mercurius'; +import { MutationResolvers, QueryResolvers } from '../generated/graphql'; +import { withAuth } from '../utils'; export const MESSAGE_ADDED = 'MESSAGE_ADDED'; export const messageResolvers: IResolvers = { Query: { - messages: async (_, { roomId }, { prisma, userId }) => { - if (!userId) { - throw new GraphQLError('You must be logged in to view messages', { - extensions: { - code: 'UNAUTHENTICATED', - }, + messages: withAuth( + async (_, { roomId }, { prisma, jwt }) => { + const room = await prisma.room.findUnique({ + where: { id: roomId, members: { some: { id: jwt.sub } } }, + }); + + if (!room) { + throw new GraphQLError('Room not found', { + extensions: { + code: 'NOT_FOUND', + }, + }); + } + + return prisma.message.findMany({ + where: { roomId }, + orderBy: { createdAt: 'asc' }, }); } - - // Check if user is a member of the room - const room = await prisma.room.findUnique({ - where: { id: roomId }, - include: { members: true }, - }); - - if (!room) { - throw new GraphQLError('Room not found', { - extensions: { - code: 'NOT_FOUND', - }, - }); - } - - const isMember = room.members.some( - (member: { id: string }) => member.id === userId - ); - if (!isMember) { - throw new GraphQLError('You are not a member of this room', { - extensions: { - code: 'FORBIDDEN', - }, - }); - } - - return prisma.message.findMany({ - where: { roomId }, - orderBy: { createdAt: 'asc' }, - }); - }, + ), }, Mutation: { - sendMessage: async ( - _: any, - { content, roomId }: { content: string; roomId: string }, - { prisma, userId, pubsub } - ) => { - if (!userId) { - throw new GraphQLError('You must be logged in to send a message', { - extensions: { - code: 'UNAUTHENTICATED', + sendMessage: withAuth( + async (_, { content, roomId }, { prisma, jwt, pubsub }) => { + const room = await prisma.room.findUnique({ + where: { id: roomId, members: { some: { id: jwt.sub } } }, + }); + + if (!room) { + throw new GraphQLError('Room not found', { + extensions: { + code: 'NOT_FOUND', + }, + }); + } + + const message = await prisma.message.create({ + data: { + content, + user: { + connect: { id: jwt.sub }, + }, + room: { + connect: { id: roomId }, + }, + }, + include: { + user: true, + room: true, }, }); - } - // Check if user is a member of the room - const room = await prisma.room.findUnique({ - where: { id: roomId }, - include: { members: true }, - }); - - if (!room) { - throw new GraphQLError('Room not found', { - extensions: { - code: 'NOT_FOUND', - }, + pubsub.publish({ + topic: MESSAGE_ADDED, + payload: { messageAdded: message, roomId }, }); + + return message; } - - const isMember = room.members.some( - (member: { id: string }) => member.id === userId - ); - if (!isMember) { - throw new GraphQLError('You are not a member of this room', { - extensions: { - code: 'FORBIDDEN', - }, - }); - } - - const message = await prisma.message.create({ - data: { - content, - user: { - connect: { id: userId }, - }, - room: { - connect: { id: roomId }, - }, - }, - include: { - user: true, - room: true, - }, - }); - - pubsub.publish({ - topic: MESSAGE_ADDED, - payload: { messageAdded: message, roomId }, - }); - - return message; - }, + ), }, Subscription: { messageAdded: { @@ -119,21 +79,17 @@ export const messageResolvers: IResolvers = { }, }, Message: { - user: async (parent, _, { prisma }) => { - if (parent.user) { - return parent.user; - } - return prisma.user.findUnique({ - where: { id: parent.userId }, - }); - }, - room: async (parent, _, { prisma }) => { - if (parent.room) { - return parent.room; - } - return prisma.room.findUnique({ - where: { id: parent.roomId }, - }); - }, + user: async (parent, _, { prisma }) => + parent.user + ? parent.user + : prisma.user.findUnique({ + where: { id: parent.userId }, + }), + room: async (parent, _, { prisma }) => + parent.room + ? parent.room + : prisma.room.findUnique({ + where: { id: parent.roomId }, + }), }, }; diff --git a/apps/api/src/resolvers/room.ts b/apps/api/src/resolvers/room.ts index 42e891e..30deced 100644 --- a/apps/api/src/resolvers/room.ts +++ b/apps/api/src/resolvers/room.ts @@ -1,5 +1,7 @@ import { GraphQLError } from 'graphql'; import { IResolvers } from 'mercurius'; +import { withAuth } from '../utils'; +import { MutationResolvers } from '../generated/graphql'; export const ROOM_ADDED = 'ROOM_ADDED'; export const ROOM_UPDATED = 'ROOM_UPDATED'; @@ -11,176 +13,133 @@ export const roomResolvers: IResolvers = { where: { isPrivate: false }, }); }, - room: async (_: any, { id }: { id: string }, { prisma }) => { + room: async (_, { id }, { prisma }) => { return prisma.room.findUnique({ where: { id }, }); }, }, Mutation: { - createRoom: async ( - _, - { name, description, isPrivate }, - { prisma, userId, pubsub } - ) => { - if (!userId) { - throw new GraphQLError('You must be logged in to create a room', { - extensions: { - code: 'UNAUTHENTICATED', + createRoom: withAuth( + async (_, { name, description, isPrivate }, { prisma, jwt, pubsub }) => { + const room = await prisma.room.create({ + data: { + name, + description, + isPrivate: isPrivate ?? false, + owner: { + connect: { id: jwt.sub }, + }, + members: { + connect: { id: jwt.sub }, + }, }, }); - } - const room = await prisma.room.create({ - data: { - name, - description, - isPrivate: isPrivate ?? false, - owner: { - connect: { id: userId }, - }, - members: { - connect: { id: userId }, - }, - }, - }); - - pubsub.publish({ - topic: ROOM_ADDED, - payload: { roomAdded: room }, - }); - - return room; - }, - joinRoom: async ( - _, - { roomId }: { roomId: string }, - { prisma, userId, pubsub } - ) => { - if (!userId) { - throw new GraphQLError('You must be logged in to join a room', { - extensions: { - code: 'UNAUTHENTICATED', - }, + pubsub.publish({ + topic: ROOM_ADDED, + payload: { roomAdded: room }, }); + + return room; } - - const room = await prisma.room.findUnique({ - where: { id: roomId }, - include: { members: true }, - }); - - if (!room) { - throw new GraphQLError('Room not found', { - extensions: { - code: 'NOT_FOUND', - }, + ), + joinRoom: withAuth( + async (_, { roomId }, { prisma, jwt, pubsub }) => { + const room = await prisma.room.findUnique({ + where: { id: roomId }, + include: { members: true }, }); - } - if (room.isPrivate) { - // In a real application, you would check if the user has been invited - throw new GraphQLError( - 'You cannot join a private room without an invitation', - { + if (!room) { + throw new GraphQLError('Room not found', { + extensions: { + code: 'NOT_FOUND', + }, + }); + } + + if (room.isPrivate) { + // In a real application, you would check if the user has been invited + throw new GraphQLError( + 'You cannot join a private room without an invitation', + { + extensions: { + code: 'FORBIDDEN', + }, + } + ); + } + + // Check if user is already a member + const isMember = room.members.some( + (member: { id: string }) => member.id === jwt.sub + ); + if (isMember) { + return room; + } + + const updatedRoom = await prisma.room.update({ + where: { id: roomId }, + data: { + members: { + connect: { id: jwt.sub }, + }, + }, + include: { members: true }, + }); + + // Publish room updated event + pubsub.publish({ + topic: ROOM_UPDATED, + payload: { roomUpdated: updatedRoom }, + }); + + return updatedRoom; + } + ), + leaveRoom: withAuth( + async (_, { roomId }, { prisma, jwt, pubsub }) => { + const room = await prisma.room.findUnique({ + where: { id: roomId, members: { some: { id: jwt.sub } } }, + include: { members: true }, + }); + + if (!room) { + throw new GraphQLError('Room not found', { + extensions: { + code: 'NOT_FOUND', + }, + }); + } + + if (room.ownerId === jwt.sub) { + throw new GraphQLError('You cannot leave a room you own', { extensions: { code: 'FORBIDDEN', }, - } - ); - } + }); + } - // Check if user is already a member - const isMember = room.members.some( - (member: { id: string }) => member.id === userId - ); - if (isMember) { - return room; - } - - const updatedRoom = await prisma.room.update({ - where: { id: roomId }, - data: { - members: { - connect: { id: userId }, - }, - }, - include: { members: true }, - }); - - // Publish room updated event - pubsub.publish({ - topic: ROOM_UPDATED, - payload: { roomUpdated: updatedRoom }, - }); - - return updatedRoom; - }, - leaveRoom: async ( - _, - { roomId }: { roomId: string }, - { prisma, userId, pubsub } - ) => { - if (!userId) { - throw new GraphQLError('You must be logged in to leave a room', { - extensions: { - code: 'UNAUTHENTICATED', + const updatedRoom = await prisma.room.update({ + where: { id: roomId }, + data: { + members: { + disconnect: { id: jwt.sub }, + }, }, + include: { members: true }, }); - } - const room = await prisma.room.findUnique({ - where: { id: roomId }, - include: { members: true }, - }); - - if (!room) { - throw new GraphQLError('Room not found', { - extensions: { - code: 'NOT_FOUND', - }, + // Publish room updated event + pubsub.publish({ + topic: ROOM_UPDATED, + payload: { roomUpdated: updatedRoom }, }); + + return true; } - - // Check if user is a member - const isMember = room.members.some( - (member: { id: string }) => member.id === userId - ); - if (!isMember) { - throw new GraphQLError('You are not a member of this room', { - extensions: { - code: 'FORBIDDEN', - }, - }); - } - - // If user is the owner, they cannot leave - if (room.ownerId === userId) { - throw new GraphQLError('You cannot leave a room you own', { - extensions: { - code: 'FORBIDDEN', - }, - }); - } - - const updatedRoom = await prisma.room.update({ - where: { id: roomId }, - data: { - members: { - disconnect: { id: userId }, - }, - }, - include: { members: true }, - }); - - // Publish room updated event - pubsub.publish({ - topic: ROOM_UPDATED, - payload: { roomUpdated: updatedRoom }, - }); - - return true; - }, + ), }, Subscription: { roomAdded: { diff --git a/apps/api/src/resolvers/user.ts b/apps/api/src/resolvers/user.ts index 07863b7..8303ae4 100644 --- a/apps/api/src/resolvers/user.ts +++ b/apps/api/src/resolvers/user.ts @@ -1,48 +1,79 @@ import { GraphQLError } from 'graphql'; import { IResolvers } from 'mercurius'; -// In a real application, you would use bcrypt for password hashing -// import bcrypt from 'bcryptjs'; -// import jwt from 'jsonwebtoken'; +import { MutationResolvers, QueryResolvers } from '../generated/graphql'; +import { withAuth, hashPassword, verifyPassword } from '../utils'; export const userResolvers: IResolvers = { Query: { - me: async (_, __, { prisma, userId }) => { - // In a real application, you would get the user from the context - // which would be set by an authentication middleware - if (!userId) { - return null; + me: withAuth(async (_, __, { prisma, jwt }) => + prisma.user.findUnique({ + where: { id: jwt.sub }, + }) + ), + users: withAuth(async (_, __, { prisma }) => + prisma.user.findMany() + ), + user: withAuth(async (_, { id }, { prisma }) => + prisma.user.findUnique({ where: { id } }) + ), + getProfilePicUploadUrl: withAuth( + async (_, { fileExtension }, { jwt, minio, prisma }) => { + const validExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + const normalizedExtension = fileExtension + .toLowerCase() + .replace('.', ''); + + if (!validExtensions.includes(normalizedExtension)) { + throw new GraphQLError('Invalid file extension', { + extensions: { + code: 'BAD_USER_INPUT', + }, + }); + } + + const objectName = `${jwt.sub}.${normalizedExtension}`; + const [url] = await Promise.all([ + minio.generateUploadUrl(objectName), + prisma.user.update({ + where: { id: jwt.sub }, + data: { s3ProfilePicObjectKey: objectName }, + }), + ]); + return { + url, + expiresIn: 60 * 60, // 1 hour in seconds + }; } - return prisma.user.findUnique({ - where: { id: userId }, - }); - }, - users: async (_, __, { prisma }) => { - return prisma.user.findMany(); - }, - user: async (_, { id }: { id: string }, { prisma }) => { - return prisma.user.findUnique({ - where: { id }, - }); - }, + ), + getProfilePicUrl: withAuth( + async (_, __, { jwt, minio, prisma }) => { + const user = await prisma.user.findUnique({ + where: { id: jwt.sub }, + select: { s3ProfilePicObjectKey: true }, + }); + + const { s3ProfilePicObjectKey } = user ?? {}; + if (!s3ProfilePicObjectKey) return null; + + const exists = await minio.objectExists(s3ProfilePicObjectKey); + if (!exists) return null; + + return minio.generateDownloadUrl(s3ProfilePicObjectKey); + } + ), }, Mutation: { register: async ( - _: any, - { - email, - username, - password, - }: { email: string; username: string; password: string }, - { prisma } + _, + { email, username, password, deviceId }, + { prisma, token, reply } ) => { - // Check if user already exists const existingUser = await prisma.user.findFirst({ where: { OR: [{ email }, { username }], }, }); - if (existingUser) { throw new GraphQLError('User already exists', { extensions: { @@ -50,63 +81,107 @@ export const userResolvers: IResolvers = { }, }); } - - // In a real application, you would hash the password - // const hashedPassword = await bcrypt.hash(password, 10); - - const user = await prisma.user.create({ - data: { - email, - username, - password, // In a real app: hashedPassword - }, - }); - - // In a real application, you would generate a JWT token - // const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET); - + const hashedPassword = hashPassword(password); + const { user, accessToken, refreshToken } = await prisma.$transaction( + async (tx) => { + const user = await tx.user.create({ + data: { + email, + username, + password: hashedPassword, + }, + }); + const tokens = await token.generateTokens({ + userId: user.id, + role: 'user', + deviceId, + txn: tx, + }); + return { user, ...tokens }; + } + ); + reply.setCookie('refreshToken', refreshToken); return { - token: 'dummy-token', // In a real app: token + accessToken, + refreshToken, user, }; }, login: async ( - _: any, - { email, password }: { email: string; password: string }, - { prisma } + _, + { email, password, deviceId }, + { prisma, token, reply } ) => { const user = await prisma.user.findUnique({ where: { email }, }); - if (!user) { + if (!user || !verifyPassword(password, user.password)) { throw new GraphQLError('Invalid credentials', { extensions: { code: 'INVALID_CREDENTIALS', }, }); } - - // In a real application, you would verify the password - // const valid = await bcrypt.compare(password, user.password); - const valid = password === user.password; // This is just for demo purposes - - if (!valid) { - throw new GraphQLError('Invalid credentials', { - extensions: { - code: 'INVALID_CREDENTIALS', - }, - }); - } - - // In a real application, you would generate a JWT token - // const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET); - + const { accessToken, refreshToken } = await token.generateTokens({ + userId: user.id, + role: 'user', + deviceId, + }); + reply.setCookie('refreshToken', refreshToken); return { - token: 'dummy-token', // In a real app: token user, + accessToken, + refreshToken, }; }, + logout: async (_, { deviceId }, { token, jwt, reply }) => { + jwt && + (await token.revokeTokensByDevice({ + userId: jwt.sub, + deviceId, + })); + reply.clearCookie('refreshToken'); + return true; + }, + logoutAllDevices: withAuth( + async (_, __, { token, jwt, reply }) => { + await token.revokeAllTokens(jwt.sub); + reply.clearCookie('refreshToken'); + return true; + } + ), + refreshToken: withAuth( + async (_, { deviceId }, { token, jwt, reply }) => { + const { accessToken, refreshToken } = await token.rotateRefreshToken({ + userId: jwt.sub, + oldToken: jwt.jti, + deviceId, + }); + reply.setCookie('refreshToken', refreshToken); + return { + accessToken, + refreshToken, + }; + } + ), + deleteProfilePic: withAuth( + async (_, __, { jwt, prisma, minio }) => { + const user = await prisma.user.findUnique({ + where: { id: jwt.sub! }, + select: { s3ProfilePicObjectKey: true }, + }); + await Promise.all([ + user?.s3ProfilePicObjectKey && + minio.deleteObject(user.s3ProfilePicObjectKey), + prisma.user.update({ + where: { id: jwt.sub! }, + data: { s3ProfilePicObjectKey: null }, + }), + ]); + return true; + } + ), }, User: { messages: async (user, _, { prisma }) => diff --git a/apps/api/src/schema/index.ts b/apps/api/src/schema/index.ts index 02b3e59..e44c593 100644 --- a/apps/api/src/schema/index.ts +++ b/apps/api/src/schema/index.ts @@ -7,6 +7,7 @@ export default gql` id: ID! email: String! username: String! + s3ProfilePicObjectKey: String createdAt: DateTime! updatedAt: DateTime messages: [Message!] @@ -37,9 +38,20 @@ export default gql` room: Room } + type RefreshTokenPayload { + accessToken: String! + refreshToken: String! + } + type AuthPayload { - token: String! user: User! + accessToken: String! + refreshToken: String! + } + + type PresignedUrl { + url: String! + expiresIn: Int! } type Query { @@ -49,15 +61,26 @@ export default gql` rooms: [Room!]! room(id: ID!): Room messages(roomId: ID!): [Message!]! + getProfilePicUploadUrl(fileExtension: String!): PresignedUrl! + getProfilePicUrl: String } type Mutation { - register(email: String!, username: String!, password: String!): AuthPayload! - login(email: String!, password: String!): AuthPayload! + register( + email: String! + username: String! + password: String! + deviceId: String! + ): AuthPayload! + login(email: String!, password: String!, deviceId: String!): AuthPayload! + logout(deviceId: String!): Boolean! + refreshToken(deviceId: String!): AuthPayload! + logoutAllDevices: Boolean! createRoom(name: String!, description: String, isPrivate: Boolean): Room! joinRoom(roomId: ID!): Room! leaveRoom(roomId: ID!): Boolean! sendMessage(content: String!, roomId: ID!): Message! + deleteProfilePic: Boolean! } type Subscription { diff --git a/apps/api/src/services/index.ts b/apps/api/src/services/index.ts new file mode 100644 index 0000000..0d9dfd7 --- /dev/null +++ b/apps/api/src/services/index.ts @@ -0,0 +1,3 @@ +export * from './memc.service'; +export * from './minio.service'; +export * from './token.service'; diff --git a/apps/api/src/services/memc.service.ts b/apps/api/src/services/memc.service.ts new file mode 100644 index 0000000..9f0e129 --- /dev/null +++ b/apps/api/src/services/memc.service.ts @@ -0,0 +1,65 @@ +import Keyv from 'keyv'; +import MemcacheStore from '@keyv/memcache'; +import { MemcConfig } from '../config'; + +/** + * Service for interacting with Memcached using Keyv as an abstraction layer + */ +export class MemcService { + private keyv: Keyv; + + /** + * Creates a new MemcService instance + * @param config - Memcached configuration including host, port and TTL + */ + constructor(config: MemcConfig) { + const memcache = new MemcacheStore(`${config.host}:${config.port}`); + this.keyv = new Keyv({ + store: memcache as any, + ttl: config.ttl, + }); + } + + /** + * Retrieves a value from Memcached by key + * @param key - The key to retrieve + * @returns The value if found, undefined otherwise + */ + public async get(key: string): Promise { + try { + return await this.keyv.get(key); + } catch (error) { + console.error('Error getting value from Memcached:', error); + return undefined; + } + } + + /** + * Stores a value in Memcached + * @param key - The key to store the value under + * @param value - The value to store + * @param ttl - Optional time-to-live in seconds (overrides default TTL) + */ + public async set(key: string, value: string, ttl?: number): Promise { + try { + const ttlMilliseconds = ttl ? ttl * 1000 : undefined; + return await this.keyv.set(key, value, ttlMilliseconds); + } catch (error) { + console.error('Error setting value in Memcached:', error); + return false; + } + } + + /** + * Deletes a value from Memcached + * @param key - The key to delete + */ + public async delete(key: string): Promise { + try { + return await this.keyv.delete(key); + } catch (error) { + console.error('Error deleting value from Memcached:', error); + return false; + } + } +} diff --git a/apps/api/src/services/minio.service.ts b/apps/api/src/services/minio.service.ts new file mode 100644 index 0000000..9037852 --- /dev/null +++ b/apps/api/src/services/minio.service.ts @@ -0,0 +1,106 @@ +import { Client } from 'minio'; +import { MinioConfig } from '../config'; + +/** + * Service for handling MinIO operations + */ +export class MinioService { + private client: Client; + private bucketName: string; + private readonly defaultExpiry = 60 * 60; // 1 hour in seconds + private initialized = false; + /** + * Creates a new MinioService instance + * @param config - MinIO configuration + */ + constructor(config: MinioConfig) { + this.client = new Client(config); + this.bucketName = config.bucketName; + } + + /** + * Initializes the MinIO service by ensuring the bucket exists + */ + public async initialize(): Promise { + if (this.initialized) { + return; + } + const bucketExists = await this.client.bucketExists(this.bucketName); + if (!bucketExists) { + await this.client.makeBucket(this.bucketName, 'us-east-1'); + // Set the bucket policy to allow public read access + const policy = { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { AWS: ['*'] }, + Action: ['s3:GetObject'], + Resource: [`arn:aws:s3:::${this.bucketName}/*`], + }, + ], + }; + await this.client.setBucketPolicy( + this.bucketName, + JSON.stringify(policy) + ); + } + this.initialized = true; + } + + /** + * Generates a presigned URL for uploading a file + * @param objectName - Name of the object to upload + * @param expiryInSeconds - Expiry time in seconds (default: 1 hour) + * @returns Presigned URL for uploading + */ + public async generateUploadUrl( + objectName: string, + expiryInSeconds: number = this.defaultExpiry + ): Promise { + return this.client.presignedPutObject( + this.bucketName, + objectName, + expiryInSeconds + ); + } + + /** + * Generates a presigned URL for downloading a file + * @param objectName - Name of the object to download + * @param expiryInSeconds - Expiry time in seconds (default: 1 hour) + * @returns Presigned URL for downloading + */ + public async generateDownloadUrl( + objectName: string, + expiryInSeconds: number = this.defaultExpiry + ): Promise { + return this.client.presignedGetObject( + this.bucketName, + objectName, + expiryInSeconds + ); + } + + /** + * Checks if an object exists in the bucket + * @param objectName - Name of the object to check + * @returns True if the object exists, false otherwise + */ + public async objectExists(objectName: string): Promise { + try { + await this.client.statObject(this.bucketName, objectName); + return true; + } catch (error) { + return false; + } + } + + /** + * Deletes an object from the bucket + * @param objectName - Name of the object to delete + */ + public async deleteObject(objectName: string): Promise { + await this.client.removeObject(this.bucketName, objectName); + } +} diff --git a/apps/api/src/services/token.service.ts b/apps/api/src/services/token.service.ts new file mode 100644 index 0000000..411dfc2 --- /dev/null +++ b/apps/api/src/services/token.service.ts @@ -0,0 +1,232 @@ +import jwt from 'jsonwebtoken'; +import { v4 as uuidv4 } from 'uuid'; +import crypto from 'crypto'; +import { TokenConfig } from '../config'; +import { PrismaClient } from '@prisma/client'; +import { MemcService } from './memc.service'; + +type TransactionClient = Omit< + PrismaClient, + '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends' +>; + +export interface TokenPayload { + sub: string; + role: string; + deviceId: string; + jti: string; +} + +interface GenerateTokensParams { + userId: string; + role: string; + deviceId: string; + txn?: TransactionClient; +} + +interface RotateRefreshTokenParams { + userId: string; + oldToken: string; + deviceId: string; +} + +interface RevokeTokensByDeviceParams { + userId: string; + deviceId: string; +} + +/** + * Service for handling token generation, verification, and management + */ +export class TokenService { + private prisma: PrismaClient; + private memc: MemcService; + private config: TokenConfig; + + /** + * Creates a new TokenService instance + * @param config - Token configuration + * @param prisma - Prisma client instance + * @param memc - Memcached service instance + */ + constructor({ + prisma, + memc, + ...config + }: TokenConfig & { prisma: PrismaClient; memc: MemcService }) { + this.prisma = prisma; + this.memc = memc; + this.config = config; + } + + /** + * Hashes a token using SHA-256 + * @param token - The token to hash + * @returns Hashed token string + */ + private async hashToken(token: string): Promise { + return crypto.createHash('sha256').update(token).digest('hex'); + } + + /** + * Generates new access and refresh tokens for a user + * @param params - Object containing userId, role and deviceId + * @returns Object containing accessToken and refreshToken + */ + async generateTokens(params: GenerateTokensParams) { + const { userId, role, deviceId, txn } = params; + const jti = uuidv4(); + const refreshToken = uuidv4(); + const hash = await this.hashToken(refreshToken); + const accessToken = jwt.sign( + { + sub: userId, + role, + deviceId, + jti, + }, + this.config.secret, + { expiresIn: this.config.accessTokenExpiresIn } + ); + + const tx = txn + ? (callback: Function) => callback(txn) + : this.prisma.$transaction.bind(this.prisma); + + await tx(async (tx) => { + await tx.refreshToken.create({ + data: { + jti, + userId, + deviceId, + hash, + expiresAt: new Date(Date.now() + this.config.refreshTokenExpiresIn), + }, + }); + + await this.whitelistAccessToken(jti); + }); + + return { accessToken, refreshToken }; + } + + /** + * Verifies an access token and checks if it's whitelisted + * @param token - The token to verify + * @returns Decoded token payload or null if invalid + */ + async verifyAccessToken(token?: string) { + try { + if (!token) return null; + const decoded = jwt.verify(token, this.config.secret) as TokenPayload; + const cached = await this.memc.get(`token:whitelist:${decoded.jti}`); + if (!cached) throw new Error('Token inválido'); + + return decoded; + } catch (error) { + console.error('Error verifying access token', error); + return null; + } + } + + /** + * Verifies and rotates a refresh token + * @param params - Object containing userId, oldToken and deviceId + * @returns New access and refresh tokens + */ + async rotateRefreshToken(params: RotateRefreshTokenParams) { + const { userId, oldToken, deviceId } = params; + return await this.prisma.$transaction(async (txn) => { + const storedTokens = await txn.refreshToken.findMany({ + where: { userId, deviceId }, + }); + const hash = await this.hashToken(oldToken); + const validToken = storedTokens.find((token) => token.hash === hash); + if (!validToken || validToken.expiresAt < new Date()) + throw new Error('Refresh token inválido'); + + await Promise.all( + storedTokens.map((token) => { + this.memc.delete(`token:whitelist:${token.jti}`); + }) + ); + await txn.refreshToken.deleteMany({ + where: { id: { in: storedTokens.map((token) => token.id) } }, + }); + + return this.generateTokens({ userId, role: 'user', deviceId, txn }); + }); + } + + /** + * Revokes all tokens for a user, useful for logout from all devices + * @param userId - The user ID + * @returns True if successful + */ + async revokeAllTokens(userId: string) { + await this.prisma.$transaction(async (tx) => { + const tokens = await tx.refreshToken.findMany({ + where: { userId }, + }); + await Promise.all( + tokens.map((token) => { + this.memc.delete(`token:whitelist:${token.jti}`); + }) + ); + await tx.refreshToken.deleteMany({ + where: { id: { in: tokens.map((token) => token.id) } }, + }); + }); + return true; + } + + /** + * Revokes tokens for a specific device + * @param params - Object containing userId and deviceId + * @returns True if successful + */ + async revokeTokensByDevice(params: RevokeTokensByDeviceParams) { + const { userId, deviceId } = params; + await this.prisma.$transaction(async (tx) => { + const tokens = await tx.refreshToken.findMany({ + where: { userId, deviceId }, + }); + await Promise.all( + tokens.map((token) => { + this.memc.delete(`token:whitelist:${token.jti}`); + }) + ); + await tx.refreshToken.deleteMany({ + where: { id: { in: tokens.map((token) => token.id) } }, + }); + }); + return true; + } + + /** + * Adds an access token to the whitelist + * @param jti - The JWT ID + */ + async whitelistAccessToken(jti: string) { + const ttl = this.config.accessTokenExpiresIn; + await this.memc.set(`token:whitelist:${jti}`, '1', ttl); + } + + /** + * Cleans up expired tokens from the database and Memcached + */ + async cleanupExpiredTokens() { + const now = new Date(); + const expired = await this.prisma.refreshToken.findMany({ + where: { expiresAt: { lt: now } }, + }); + await Promise.all( + expired.map((token) => { + this.memc.delete(`token:whitelist:${token.jti}`); + }) + ); + await this.prisma.refreshToken.deleteMany({ + where: { id: { in: expired.map((token) => token.id) } }, + }); + } +} diff --git a/apps/api/src/types.ts b/apps/api/src/types.ts index 0dbbd81..b007799 100644 --- a/apps/api/src/types.ts +++ b/apps/api/src/types.ts @@ -1,8 +1,20 @@ -import type { PrismaClient } from '@prisma/client'; +import { PrismaClient } from '@prisma/client'; +import { + MinioService, + TokenService, + MemcService, + TokenPayload, +} from './services'; +import { FastifyReply, FastifyRequest } from 'fastify'; declare module 'mercurius' { interface MercuriusContext { prisma: PrismaClient; - userId: string | null; + jwt: TokenPayload; + minio: MinioService; + token: TokenService; + memc: MemcService; + req: FastifyRequest; + reply: FastifyReply; } } diff --git a/apps/api/src/utils/crypto.ts b/apps/api/src/utils/crypto.ts new file mode 100644 index 0000000..d15f269 --- /dev/null +++ b/apps/api/src/utils/crypto.ts @@ -0,0 +1,14 @@ +import { pbkdf2Sync, randomBytes, timingSafeEqual } from 'crypto'; +export const hashPassword = (password: string) => { + const salt = randomBytes(16).toString('hex'); + const hash = pbkdf2Sync(password, salt, 1000, 128, 'sha512').toString('hex'); + const hashedPassword = `${salt}:${hash}`; + return hashedPassword; +}; + +export const verifyPassword = (password: string, hashedPassword: string) => { + const [salt, hash] = hashedPassword.split(':'); + const hashBuffer = Buffer.from(hash, 'hex'); + const result = pbkdf2Sync(password, salt, 1000, 128, 'sha512'); + return timingSafeEqual(hashBuffer, result); +}; diff --git a/apps/api/src/utils/index.ts b/apps/api/src/utils/index.ts new file mode 100644 index 0000000..432db58 --- /dev/null +++ b/apps/api/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './crypto'; +export * from './middlewares'; diff --git a/apps/api/src/utils/middlewares.ts b/apps/api/src/utils/middlewares.ts new file mode 100644 index 0000000..1cfa3b3 --- /dev/null +++ b/apps/api/src/utils/middlewares.ts @@ -0,0 +1,18 @@ +import { MercuriusContext } from 'mercurius'; + +const isCallback = ( + maybeFunction: unknown | Function +): maybeFunction is Function => typeof maybeFunction === 'function'; + +export const withAuth = (callback: T) => + async function ( + _: unknown, + __: unknown, + context: MercuriusContext, + info: unknown + ) { + if (!context.jwt) { + throw new Error('Not authenticated!'); + } + return isCallback(callback) && callback(_, __, context, info); + }; diff --git a/docker-compose.yml b/docker-compose.yml index c376116..10f1004 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,12 +6,21 @@ services: container_name: unreal-chat-api restart: unless-stopped environment: - - NODE_ENV=production - - DATABASE_URL=${DATABASE_URL} - - JWT_SECRET=your-secret-key - - API_PORT=4000 - ALLOWED_ORIGINS=${ALLOWED_ORIGINS} - API_HOST=${API_HOST} + - API_PORT=${API_PORT} + - COOKIE_SECRET=${COOKIE_SECRET} + - DATABASE_URL=${DATABASE_URL} + - MEMC_HOST=${MEMC_HOST} + - MEMC_PORT=${MEMC_PORT} + - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} + - MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME} + - MINIO_ENDPOINT=${MINIO_ENDPOINT} + - MINIO_PORT=${MINIO_PORT} + - MINIO_SECRET_KEY=${MINIO_SECRET_KEY} + - MINIO_USE_SSL=${MINIO_USE_SSL} + - NODE_ENV=production + - TOKEN_SECRET=${TOKEN_SECRET} networks: - default-network @@ -31,9 +40,7 @@ services: networks: - default-network -volumes: - db_data: - networks: - default-network: - external: true \ No newline at end of file + default: + name: default-network + external: true \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 93cb991..eee8113 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,19 +26,25 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@fastify/cookie": "^11.0.2", "@fastify/cors": "^11.0.0", + "@keyv/memcache": "^2.0.1", "@prisma/client": "^6.4.1", "dotenv": "^16.4.7", "fastify": "^5.2.1", "fastify-plugin": "^5.0.0", "graphql": "^16.10.0", + "jsonwebtoken": "^9.0.2", "mercurius": "^16.1.0", "mercurius-codegen": "^6.0.1", + "minio": "^8.0.4", + "uuid": "^11.1.0", "zod": "^3.24.2" }, "devDependencies": { "@repo/eslint-config": "*", "@repo/typescript-config": "*", + "@types/jsonwebtoken": "^9.0.9", "prisma": "^6.4.1", "typescript": "5.8.2" } @@ -1576,6 +1582,26 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/@fastify/cookie": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.2.tgz", + "integrity": "sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "cookie": "^1.0.0", + "fastify-plugin": "^5.0.0" + } + }, "node_modules/@fastify/cors": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.0.0.tgz", @@ -2327,6 +2353,74 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@keyv/memcache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@keyv/memcache/-/memcache-2.0.1.tgz", + "integrity": "sha512-JavNaKi/9L/+amZ0k7EH5odYb6napHJu388jv4Q04gtT8CR4ObRBIuhVBgOyTYa+KxLCs5RqckZgP5Sj/GjSUA==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "*", + "buffer": "^6.0.3", + "memjs": "^1.3.2" + } + }, + "node_modules/@keyv/memcache/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.3.tgz", + "integrity": "sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3" + } + }, + "node_modules/@keyv/serialize/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/@lukeed/ms": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", @@ -2942,6 +3036,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/minimatch": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", @@ -2949,6 +3054,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.13.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", @@ -3224,6 +3336,13 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/@zxing/text-encoding": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", + "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", + "license": "(Unlicense OR Apache-2.0)", + "optional": true + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -3456,6 +3575,12 @@ "node": ">=4" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -3477,6 +3602,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/avvio": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", @@ -3635,6 +3775,15 @@ "readable-stream": "^3.4.0" } }, + "node_modules/block-stream2": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz", + "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", + "license": "MIT", + "dependencies": { + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3657,6 +3806,12 @@ "node": ">=8" } }, + "node_modules/browser-or-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", + "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", + "license": "MIT" + }, "node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -3723,6 +3878,68 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4132,6 +4349,15 @@ "node": ">=0.10.0" } }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -4162,6 +4388,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/degenerator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", @@ -4358,6 +4601,20 @@ "node": ">=4" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexify": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", @@ -4376,6 +4633,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.112", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.112.tgz", @@ -4416,6 +4682,36 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", @@ -4756,6 +5052,12 @@ "node": ">=6" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -4926,6 +5228,24 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-parser": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.1.1" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastify": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.2.1.tgz", @@ -5065,6 +5385,15 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/find-my-way": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.2.0.tgz", @@ -5117,6 +5446,21 @@ "dev": true, "license": "ISC" }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -5184,7 +5528,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5217,6 +5560,43 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -5290,6 +5670,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -5478,11 +5870,49 @@ "node": ">=4" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5835,6 +6265,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -5847,6 +6293,18 @@ "node": ">=8" } }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -5881,6 +6339,24 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -5960,6 +6436,24 @@ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", "license": "MIT" }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -5985,6 +6479,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unc-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", @@ -6178,6 +6687,49 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kebab-case": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz", @@ -6267,6 +6819,42 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -6285,6 +6873,12 @@ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", @@ -6408,6 +7002,24 @@ "node": ">=0.10.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memjs": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/memjs/-/memjs-1.3.2.tgz", + "integrity": "sha512-qUEg2g8vxPe+zPn09KidjIStHPtoBO8Cttm8bgJFWWabbsjQ9Av9Ky+6UcvKx6ue0LLb/LEhtcyQpRyKfzeXcg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mercurius": { "version": "16.1.0", "resolved": "https://registry.npmjs.org/mercurius/-/mercurius-16.1.0.tgz", @@ -6562,6 +7174,27 @@ "node": ">=10.0.0" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -6594,6 +7227,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minio": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minio/-/minio-8.0.4.tgz", + "integrity": "sha512-GVW7y2PNbzjjFJ9opVMGKvDNuRkyz3bMt1q7UrHs7bsKFWLXbSvMPffjE/HkVYWUjlD8kQwMaeqiHhhvZJJOfQ==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.4", + "block-stream2": "^2.1.0", + "browser-or-node": "^2.1.1", + "buffer-crc32": "^1.0.0", + "eventemitter3": "^5.0.1", + "fast-xml-parser": "^4.4.1", + "ipaddr.js": "^2.0.1", + "lodash": "^4.17.21", + "mime-types": "^2.1.35", + "query-string": "^7.1.3", + "stream-json": "^1.8.0", + "through2": "^4.0.2", + "web-encoding": "^1.1.5", + "xml2js": "^0.5.0 || ^0.6.2" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -7582,6 +8240,15 @@ "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", "license": "MIT" }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -7770,6 +8437,24 @@ "node": ">= 16" } }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8141,6 +8826,23 @@ ], "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-regex2": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-4.0.1.tgz", @@ -8176,6 +8878,12 @@ "dev": true, "license": "MIT" }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, "node_modules/secure-json-parse": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-3.0.2.tgz", @@ -8248,6 +8956,23 @@ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", "license": "MIT" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -8431,6 +9156,15 @@ "node": ">=0.10.0" } }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/sponge-case": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sponge-case/-/sponge-case-1.0.1.tgz", @@ -8456,12 +9190,36 @@ "node": ">= 0.8" } }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, "node_modules/stream-shift": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "license": "MIT" }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -8548,6 +9306,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/style-to-object": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", @@ -8609,6 +9379,15 @@ "dev": true, "license": "MIT" }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, "node_modules/tiny-lru": { "version": "11.2.11", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.2.11.tgz", @@ -9104,12 +9883,38 @@ "punycode": "^2.1.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -9273,6 +10078,18 @@ "resolved": "apps/web", "link": true }, + "node_modules/web-encoding": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", + "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", + "license": "MIT", + "dependencies": { + "util": "^0.12.3" + }, + "optionalDependencies": { + "@zxing/text-encoding": "0.9.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -9310,6 +10127,26 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, + "node_modules/which-typed-array": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wonka": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz", @@ -9392,6 +10229,28 @@ } } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/turbo.json b/turbo.json index c46ed61..1055248 100644 --- a/turbo.json +++ b/turbo.json @@ -6,8 +6,21 @@ "ALLOWED_ORIGINS", "API_HOST", "API_PORT", - "DATABASE_URL", + "COOKIE_SECRET", + "MINIO_ENDPOINT", + "MINIO_PORT", + "MINIO_ACCESS_KEY", + "MINIO_SECRET_KEY", + "MINIO_BUCKET_NAME", + "MINIO_USE_SSL", + "MEMC_HOST", + "MEMC_PORT", + "MEMC_TTL", + "MEMC_NAMESPACE", "NODE_ENV", + "TOKEN_ACCESS_EXPIRES_IN", + "TOKEN_REFRESH_EXPIRES_IN", + "TOKEN_SECRET", "VITE_API_URL", "VITE_WS_URL" ],