diff --git a/apps/backend/src/api/api.module.ts b/apps/backend/src/api/api.module.ts index e7e8d9a2..3da6804a 100644 --- a/apps/backend/src/api/api.module.ts +++ b/apps/backend/src/api/api.module.ts @@ -18,6 +18,8 @@ import { MediaController } from '@gitroom/backend/api/routes/media.controller'; import { UploadModule } from '@gitroom/nestjs-libraries/upload/upload.module'; import { ServeStaticModule } from '@nestjs/serve-static'; import { CommentsController } from '@gitroom/backend/api/routes/comments.controller'; +import { BillingController } from '@gitroom/backend/api/routes/billing.controller'; +import { NotificationsController } from '@gitroom/backend/api/routes/notifications.controller'; const authenticatedController = [ UsersController, @@ -27,6 +29,8 @@ const authenticatedController = [ PostsController, MediaController, CommentsController, + BillingController, + NotificationsController, ]; @Module({ imports: [ diff --git a/apps/backend/src/api/routes/auth.controller.ts b/apps/backend/src/api/routes/auth.controller.ts index 579e10d7..1938ff82 100644 --- a/apps/backend/src/api/routes/auth.controller.ts +++ b/apps/backend/src/api/routes/auth.controller.ts @@ -1,23 +1,30 @@ -import {Body, Controller, Post, Res} from '@nestjs/common'; -import {Response} from 'express'; +import { Body, Controller, Post, Req, Res } from '@nestjs/common'; +import { Response, Request } from 'express'; -import {CreateOrgUserDto} from "@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto"; -import {LoginUserDto} from "@gitroom/nestjs-libraries/dtos/auth/login.user.dto"; -import {AuthService} from "@gitroom/backend/services/auth/auth.service"; +import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto'; +import { LoginUserDto } from '@gitroom/nestjs-libraries/dtos/auth/login.user.dto'; +import { AuthService } from '@gitroom/backend/services/auth/auth.service'; @Controller('/auth') export class AuthController { - constructor( - private _authService: AuthService - ) { - } + constructor(private _authService: AuthService) {} @Post('/register') async register( - @Body() body: CreateOrgUserDto, - @Res({ passthrough: true }) response: Response + @Req() req: Request, + @Body() body: CreateOrgUserDto, + @Res({ passthrough: true }) response: Response ) { try { - const jwt = await this._authService.routeAuth(body.provider, body); + const getOrgFromCookie = this._authService.getOrgFromCookie( + req?.cookies?.org + ); + + const { jwt, addedOrg } = await this._authService.routeAuth( + body.provider, + body, + getOrgFromCookie + ); + response.cookie('auth', jwt, { domain: '.' + new URL(process.env.FRONTEND_URL!).hostname, secure: true, @@ -25,23 +32,42 @@ export class AuthController { sameSite: 'none', expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), }); + + if (typeof addedOrg !== 'boolean') { + response.cookie('showorg', addedOrg.organizationId, { + domain: '.' + new URL(process.env.FRONTEND_URL!).hostname, + secure: true, + httpOnly: true, + sameSite: 'none', + expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), + }); + } + response.header('reload', 'true'); response.status(200).json({ - register: true + register: true, }); - } - catch (e) { + } catch (e) { response.status(400).send(e.message); } } @Post('/login') async login( - @Body() body: LoginUserDto, - @Res({ passthrough: true }) response: Response + @Req() req: Request, + @Body() body: LoginUserDto, + @Res({ passthrough: true }) response: Response ) { try { - const jwt = await this._authService.routeAuth(body.provider, body); + const getOrgFromCookie = this._authService.getOrgFromCookie( + req?.cookies?.org + ); + const { jwt, addedOrg } = await this._authService.routeAuth( + body.provider, + body, + getOrgFromCookie + ); + response.cookie('auth', jwt, { domain: '.' + new URL(process.env.FRONTEND_URL!).hostname, secure: true, @@ -49,12 +75,22 @@ export class AuthController { sameSite: 'none', expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), }); + + if (typeof addedOrg !== 'boolean') { + response.cookie('showorg', addedOrg.organizationId, { + domain: '.' + new URL(process.env.FRONTEND_URL!).hostname, + secure: true, + httpOnly: true, + sameSite: 'none', + expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), + }); + } + response.header('reload', 'true'); response.status(200).json({ - login: true + login: true, }); - } - catch (e) { + } catch (e) { response.status(400).send(e.message); } } diff --git a/apps/backend/src/api/routes/billing.controller.ts b/apps/backend/src/api/routes/billing.controller.ts new file mode 100644 index 00000000..8c30c1b8 --- /dev/null +++ b/apps/backend/src/api/routes/billing.controller.ts @@ -0,0 +1,64 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; +import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service'; +import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; +import { Organization } from '@prisma/client'; +import { BillingSubscribeDto } from '@gitroom/nestjs-libraries/dtos/billing/billing.subscribe.dto'; + +@Controller('/billing') +export class BillingController { + constructor( + private _subscriptionService: SubscriptionService, + private _stripeService: StripeService + ) {} + + @Get('/check/:id') + async checkId( + @GetOrgFromRequest() org: Organization, + @Param('id') body: string + ) { + return { + exists: !!(await this._subscriptionService.checkSubscription( + org.id, + body + )), + }; + } + + @Post('/subscribe') + subscribe( + @GetOrgFromRequest() org: Organization, + @Body() body: BillingSubscribeDto + ) { + return this._stripeService.subscribe(org.id, body); + } + + @Post('/modify') + async modifyPayment(@GetOrgFromRequest() org: Organization) { + const customer = await this._stripeService.getCustomerByOrganizationId( + org.id + ); + const { url } = await this._stripeService.createBillingPortalLink(customer); + return { + portal: url, + }; + } + + @Get('/') + getCurrentBilling(@GetOrgFromRequest() org: Organization) { + return this._subscriptionService.getSubscriptionByOrganizationId(org.id); + } + + @Post('/cancel') + cancel(@GetOrgFromRequest() org: Organization) { + return this._stripeService.setToCancel(org.id); + } + + @Post('/prorate') + prorate( + @GetOrgFromRequest() org: Organization, + @Body() body: BillingSubscribeDto + ) { + return this._stripeService.prorate(org.id, body); + } +} diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index bfde9ee3..f0dcabcd 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -7,6 +7,11 @@ import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.reque import { Organization } from '@prisma/client'; import { ApiKeyDto } from '@gitroom/nestjs-libraries/dtos/integrations/api.key.dto'; import { IntegrationFunctionDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.function.dto'; +import { + AuthorizationActions, + Sections, +} from '@gitroom/backend/services/auth/permissions/permissions.service'; +import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability'; @Controller('/integrations') export class IntegrationsController { @@ -15,6 +20,7 @@ export class IntegrationsController { private _integrationService: IntegrationService ) {} @Get('/') + @CheckPolicies([AuthorizationActions.Create, Sections.CHANNEL]) getIntegration() { return this._integrationManager.getAllIntegrations(); } @@ -97,6 +103,7 @@ export class IntegrationsController { } @Post('/article/:integration/connect') + @CheckPolicies([AuthorizationActions.Create, Sections.CHANNEL]) async connectArticle( @GetOrgFromRequest() org: Organization, @Param('integration') integration: string, @@ -136,6 +143,7 @@ export class IntegrationsController { } @Post('/social/:integration/connect') + @CheckPolicies([AuthorizationActions.Create, Sections.CHANNEL]) async connectSocialMedia( @GetOrgFromRequest() org: Organization, @Param('integration') integration: string, diff --git a/apps/backend/src/api/routes/notifications.controller.ts b/apps/backend/src/api/routes/notifications.controller.ts new file mode 100644 index 00000000..acf578f3 --- /dev/null +++ b/apps/backend/src/api/routes/notifications.controller.ts @@ -0,0 +1,31 @@ +import { Controller, Get } from '@nestjs/common'; +import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request'; +import { Organization, User } from '@prisma/client'; +import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; +import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service'; + +@Controller('/notifications') +export class NotificationsController { + constructor(private _notificationsService: NotificationService) {} + @Get('/') + async mainPageList( + @GetUserFromRequest() user: User, + @GetOrgFromRequest() organization: Organization + ) { + return this._notificationsService.getMainPageCount( + organization.id, + user.id + ); + } + + @Get('/list') + async notifications( + @GetUserFromRequest() user: User, + @GetOrgFromRequest() organization: Organization + ) { + return this._notificationsService.getNotifications( + organization.id, + user.id + ); + } +} diff --git a/apps/backend/src/api/routes/posts.controller.ts b/apps/backend/src/api/routes/posts.controller.ts index c6e22a30..cdf93370 100644 --- a/apps/backend/src/api/routes/posts.controller.ts +++ b/apps/backend/src/api/routes/posts.controller.ts @@ -15,6 +15,8 @@ import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post. import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto'; import { CommentsService } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.service'; import {StarsService} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.service"; +import {CheckPolicies} from "@gitroom/backend/services/auth/permissions/permissions.ability"; +import {AuthorizationActions, Sections} from "@gitroom/backend/services/auth/permissions/permissions.service"; @Controller('/posts') export class PostsController { @@ -63,6 +65,7 @@ export class PostsController { } @Post('/') + @CheckPolicies([AuthorizationActions.Create, Sections.POSTS_PER_MONTH]) createPost( @GetOrgFromRequest() org: Organization, @Body() body: CreatePostDto diff --git a/apps/backend/src/api/routes/settings.controller.ts b/apps/backend/src/api/routes/settings.controller.ts index 6f283597..4c0692b4 100644 --- a/apps/backend/src/api/routes/settings.controller.ts +++ b/apps/backend/src/api/routes/settings.controller.ts @@ -1,76 +1,128 @@ -import {Body, Controller, Delete, Get, Param, Post} from "@nestjs/common"; -import {GetOrgFromRequest} from "@gitroom/nestjs-libraries/user/org.from.request"; -import {Organization} from "@prisma/client"; -import {StarsService} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.service"; +import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common'; +import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; +import { Organization } from '@prisma/client'; +import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service'; +import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability'; +import { + AuthorizationActions, + Sections, +} from '@gitroom/backend/services/auth/permissions/permissions.service'; +import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service'; +import {AddTeamMemberDto} from "@gitroom/nestjs-libraries/dtos/settings/add.team.member.dto"; @Controller('/settings') export class SettingsController { - constructor( - private starsService: StarsService, - ) { - } + constructor( + private _starsService: StarsService, + private _organizationService: OrganizationService + ) {} - @Get('/github') - async getConnectedGithubAccounts( - @GetOrgFromRequest() org: Organization - ) { - return { - github: (await this.starsService.getGitHubRepositoriesByOrgId(org.id)).map((repo) => ({ - id: repo.id, - login: repo.login, - })) - } - } + @Get('/github') + @CheckPolicies([AuthorizationActions.Create, Sections.ADMIN]) + async getConnectedGithubAccounts(@GetOrgFromRequest() org: Organization) { + return { + github: ( + await this._starsService.getGitHubRepositoriesByOrgId(org.id) + ).map((repo) => ({ + id: repo.id, + login: repo.login, + })), + }; + } - @Post('/github') - async addGitHub( - @GetOrgFromRequest() org: Organization, - @Body('code') code: string - ) { - if (!code) { - throw new Error('No code provided'); - } - await this.starsService.addGitHub(org.id, code); + @Post('/github') + @CheckPolicies([AuthorizationActions.Create, Sections.ADMIN]) + async addGitHub( + @GetOrgFromRequest() org: Organization, + @Body('code') code: string + ) { + if (!code) { + throw new Error('No code provided'); } + await this._starsService.addGitHub(org.id, code); + } - @Get('/github/url') - authUrl() { - return { - url: `https://github.com/login/oauth/authorize?client_id=${process.env.GITHUB_CLIENT_ID}&scope=${encodeURIComponent('read:org repo')}&redirect_uri=${encodeURIComponent(`${process.env.FRONTEND_URL}/settings`)}` - }; - } + @Get('/github/url') + @CheckPolicies([AuthorizationActions.Create, Sections.ADMIN]) + authUrl() { + return { + url: `https://github.com/login/oauth/authorize?client_id=${ + process.env.GITHUB_CLIENT_ID + }&scope=${encodeURIComponent( + 'read:org repo' + )}&redirect_uri=${encodeURIComponent( + `${process.env.FRONTEND_URL}/settings` + )}`, + }; + } - @Get('/organizations/:id') - async getOrganizations( - @GetOrgFromRequest() org: Organization, - @Param('id') id: string - ) { - return {organizations: await this.starsService.getOrganizations(org.id, id)}; - } + @Get('/organizations/:id') + @CheckPolicies([AuthorizationActions.Create, Sections.ADMIN]) + async getOrganizations( + @GetOrgFromRequest() org: Organization, + @Param('id') id: string + ) { + return { + organizations: await this._starsService.getOrganizations(org.id, id), + }; + } - @Get('/organizations/:id/:github') - async getRepositories( - @GetOrgFromRequest() org: Organization, - @Param('id') id: string, - @Param('github') github: string, - ) { - return {repositories: await this.starsService.getRepositoriesOfOrganization(org.id, id, github)}; - } + @Get('/organizations/:id/:github') + @CheckPolicies([AuthorizationActions.Create, Sections.ADMIN]) + async getRepositories( + @GetOrgFromRequest() org: Organization, + @Param('id') id: string, + @Param('github') github: string + ) { + return { + repositories: await this._starsService.getRepositoriesOfOrganization( + org.id, + id, + github + ), + }; + } - @Post('/organizations/:id') - async updateGitHubLogin( - @GetOrgFromRequest() org: Organization, - @Param('id') id: string, - @Body('login') login: string, - ) { - return this.starsService.updateGitHubLogin(org.id, id, login); - } + @Post('/organizations/:id') + @CheckPolicies([AuthorizationActions.Create, Sections.ADMIN]) + async updateGitHubLogin( + @GetOrgFromRequest() org: Organization, + @Param('id') id: string, + @Body('login') login: string + ) { + return this._starsService.updateGitHubLogin(org.id, id, login); + } - @Delete('/repository/:id') - async deleteRepository( - @GetOrgFromRequest() org: Organization, - @Param('id') id: string - ) { - return this.starsService.deleteRepository(org.id, id); - } -} \ No newline at end of file + @Delete('/repository/:id') + @CheckPolicies([AuthorizationActions.Create, Sections.ADMIN]) + async deleteRepository( + @GetOrgFromRequest() org: Organization, + @Param('id') id: string + ) { + return this._starsService.deleteRepository(org.id, id); + } + + @Get('/team') + @CheckPolicies([AuthorizationActions.Create, Sections.TEAM_MEMBERS], [AuthorizationActions.Create, Sections.ADMIN]) + async getTeam(@GetOrgFromRequest() org: Organization) { + return this._organizationService.getTeam(org.id); + } + + @Post('/team') + @CheckPolicies([AuthorizationActions.Create, Sections.TEAM_MEMBERS], [AuthorizationActions.Create, Sections.ADMIN]) + async inviteTeamMember( + @GetOrgFromRequest() org: Organization, + @Body() body: AddTeamMemberDto, + ) { + return this._organizationService.inviteTeamMember(org.id, body); + } + + @Delete('/team/:id') + @CheckPolicies([AuthorizationActions.Create, Sections.TEAM_MEMBERS], [AuthorizationActions.Create, Sections.ADMIN]) + deleteTeamMember( + @GetOrgFromRequest() org: Organization, + @Param('id') id: string + ) { + return this._organizationService.deleteTeamMember(org, id); + } +} diff --git a/apps/backend/src/api/routes/stripe.controller.ts b/apps/backend/src/api/routes/stripe.controller.ts index abf1dda0..e5063650 100644 --- a/apps/backend/src/api/routes/stripe.controller.ts +++ b/apps/backend/src/api/routes/stripe.controller.ts @@ -14,7 +14,7 @@ export class StripeController { const event = this._stripeService.validateRequest( req.rawBody, req.headers['stripe-signature'], - process.env.PAYMENT_SIGNING_SECRET + process.env.STRIPE_SIGNING_KEY ); // Maybe it comes from another stripe webhook diff --git a/apps/backend/src/api/routes/users.controller.ts b/apps/backend/src/api/routes/users.controller.ts index 592dd137..517ba6b7 100644 --- a/apps/backend/src/api/routes/users.controller.ts +++ b/apps/backend/src/api/routes/users.controller.ts @@ -1,13 +1,112 @@ -import {Controller, Get} from '@nestjs/common'; -import {GetUserFromRequest} from "@gitroom/nestjs-libraries/user/user.from.request"; -import {User} from "@prisma/client"; +import { + Body, + Controller, + Get, + HttpException, + Post, + Req, + Res, +} from '@nestjs/common'; +import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request'; +import { Organization, User } from '@prisma/client'; +import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; +import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; +import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service'; +import { Response } from 'express'; +import { AuthService } from '@gitroom/backend/services/auth/auth.service'; +import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service'; +import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability'; +import { + AuthorizationActions, + Sections, +} from '@gitroom/backend/services/auth/permissions/permissions.service'; @Controller('/user') export class UsersController { - @Get('/self') - async getSelf( - @GetUserFromRequest() user: User - ) { - return user; + constructor( + private _subscriptionService: SubscriptionService, + private _stripeService: StripeService, + private _authService: AuthService, + private _orgService: OrganizationService + ) {} + @Get('/self') + async getSelf( + @GetUserFromRequest() user: User, + @GetOrgFromRequest() organization: Organization + ) { + if (!organization) { + throw new HttpException('Organization not found', 401); } + + return { + ...user, + orgId: organization.id, + // @ts-ignore + tier: organization?.subscription?.subscriptionTier || 'FREE', + // @ts-ignore + role: organization?.users[0]?.role, + }; + } + + @Get('/subscription') + @CheckPolicies([AuthorizationActions.Create, Sections.ADMIN]) + async getSubscription(@GetOrgFromRequest() organization: Organization) { + const subscription = + await this._subscriptionService.getSubscriptionByOrganizationId( + organization.id + ); + + return subscription ? { subscription } : { subscription: undefined }; + } + + @Get('/subscription/tiers') + @CheckPolicies([AuthorizationActions.Create, Sections.ADMIN]) + async tiers() { + return this._stripeService.getPackages(); + } + + @Post('/join-org') + async joinOrg( + @GetUserFromRequest() user: User, + @Body('org') org: string, + @Res({ passthrough: true }) response: Response + ) { + const getOrgFromCookie = this._authService.getOrgFromCookie(org); + + if (!getOrgFromCookie) { + return response.status(200).json({ id: null }); + } + + const addedOrg = await this._orgService.addUserToOrg( + user.id, + getOrgFromCookie.id, + getOrgFromCookie.orgId, + getOrgFromCookie.role + ); + + response.status(200).json({ + id: typeof addedOrg !== 'boolean' ? addedOrg.organizationId : null, + }); + } + + @Get('/organizations') + async getOrgs(@GetUserFromRequest() user: User) { + return this._orgService.getOrgsByUserId(user.id); + } + + @Post('/change-org') + changeOrg( + @Body('id') id: string, + @Res({ passthrough: true }) response: Response + ) { + response.cookie('showorg', id, { + domain: '.' + new URL(process.env.FRONTEND_URL!).hostname, + secure: true, + httpOnly: true, + sameSite: 'none', + expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), + }); + + response.status(200).send(); + } } diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 58f4eace..fd6de6fa 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -8,6 +8,7 @@ import {SubscriptionExceptionFilter} from "@gitroom/backend/services/auth/permis async function bootstrap() { const app = await NestFactory.create(AppModule, { + rawBody: true, cors: { credentials: true, exposedHeaders: ['reload'], diff --git a/apps/backend/src/services/auth/auth.middleware.ts b/apps/backend/src/services/auth/auth.middleware.ts index 42a09318..726eb24a 100644 --- a/apps/backend/src/services/auth/auth.middleware.ts +++ b/apps/backend/src/services/auth/auth.middleware.ts @@ -17,12 +17,16 @@ export class AuthMiddleware implements NestMiddleware { } try { const user = AuthService.verifyJWT(auth) as User | null; + const orgHeader = req.cookies.showorg || req.headers.showorg; + if (!user) { throw new Error('Unauthorized'); } delete user.password; - const organization = await this._organizationService.getFirstOrgByUserId(user.id); + const organization = await this._organizationService.getOrgsByUserId(user.id); + const setOrg = organization.find((org) => org.id === orgHeader) || organization[0]; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error @@ -30,7 +34,7 @@ export class AuthMiddleware implements NestMiddleware { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error - req.org = organization; + req.org = setOrg; } catch (err) { throw new Error('Unauthorized'); diff --git a/apps/backend/src/services/auth/auth.service.ts b/apps/backend/src/services/auth/auth.service.ts index e58ec775..d63fd04b 100644 --- a/apps/backend/src/services/auth/auth.service.ts +++ b/apps/backend/src/services/auth/auth.service.ts @@ -1,69 +1,117 @@ -import {Injectable} from "@nestjs/common"; -import {Provider, User} from '@prisma/client'; -import {CreateOrgUserDto} from "@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto"; -import {LoginUserDto} from "@gitroom/nestjs-libraries/dtos/auth/login.user.dto"; -import {UsersService} from "@gitroom/nestjs-libraries/database/prisma/users/users.service"; -import {OrganizationService} from "@gitroom/nestjs-libraries/database/prisma/organizations/organization.service"; -import {AuthService as AuthChecker} from "@gitroom/helpers/auth/auth.service"; -import {ProvidersFactory} from "@gitroom/backend/services/auth/providers/providers.factory"; +import { Injectable } from '@nestjs/common'; +import { Provider, User } from '@prisma/client'; +import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto'; +import { LoginUserDto } from '@gitroom/nestjs-libraries/dtos/auth/login.user.dto'; +import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service'; +import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service'; +import { AuthService as AuthChecker } from '@gitroom/helpers/auth/auth.service'; +import { ProvidersFactory } from '@gitroom/backend/services/auth/providers/providers.factory'; +import dayjs from 'dayjs'; @Injectable() export class AuthService { - constructor( - private _user: UsersService, - private _organization: OrganizationService, - ) { - } - async routeAuth( - provider: Provider, - body: CreateOrgUserDto | LoginUserDto - ) { - if (provider === Provider.LOCAL) { - const user = await this._user.getUserByEmail(body.email); - if (body instanceof CreateOrgUserDto) { - if (user) { - throw new Error('User already exists'); - } - - const create = await this._organization.createOrgAndUser(body); - return this.jwt(create.users[0].user); - } - - if (!user || !AuthChecker.comparePassword(body.password, user.password)) { - throw new Error('Invalid user'); - } - - return this.jwt(user); - } - - const user = await this.loginOrRegisterProvider(provider, body as LoginUserDto); - return this.jwt(user); - } - - private async loginOrRegisterProvider(provider: Provider, body: LoginUserDto) { - const providerInstance = ProvidersFactory.loadProvider(provider); - const providerUser = await providerInstance.getUser(body.providerToken); - if (!providerUser) { - throw new Error('Invalid provider token'); - } - - const user = await this._user.getUserByProvider(providerUser.id, provider); + constructor( + private _user: UsersService, + private _organization: OrganizationService, + ) {} + async routeAuth( + provider: Provider, + body: CreateOrgUserDto | LoginUserDto, + addToOrg?: boolean | { orgId: string; role: 'USER' | 'ADMIN'; id: string } + ) { + if (provider === Provider.LOCAL) { + const user = await this._user.getUserByEmail(body.email); + if (body instanceof CreateOrgUserDto) { if (user) { - return user; + throw new Error('User already exists'); } - const create = await this._organization.createOrgAndUser({ - company: '', - email: providerUser.email, - password: '', - provider, - providerId: providerUser.id - }); + const create = await this._organization.createOrgAndUser(body); + const addedOrg = + addToOrg && typeof addToOrg !== 'boolean' + ? await this._organization.addUserToOrg( + create.users[0].user.id, + addToOrg.id, + addToOrg.orgId, + addToOrg.role + ) + : false; + return { addedOrg, jwt: await this.jwt(create.users[0].user) }; + } - return create.users[0].user; + if (!user || !AuthChecker.comparePassword(body.password, user.password)) { + throw new Error('Invalid user'); + } + + return { jwt: await this.jwt(user) }; } - private async jwt(user: User) { - return AuthChecker.signJWT(user); + const user = await this.loginOrRegisterProvider( + provider, + body as LoginUserDto + ); + + const addedOrg = + addToOrg && typeof addToOrg !== 'boolean' + ? await this._organization.addUserToOrg( + user.id, + addToOrg.id, + addToOrg.orgId, + addToOrg.role + ) + : false; + return { addedOrg, jwt: await this.jwt(user) }; + } + + public getOrgFromCookie(cookie?: string) { + if (!cookie) { + return false; } -} \ No newline at end of file + + try { + const getOrg: any = AuthChecker.verifyJWT(cookie); + if (dayjs(getOrg.timeLimit).isBefore(dayjs())) { + return false; + } + + return getOrg as { + email: string; + role: 'USER' | 'ADMIN'; + orgId: string; + id: string; + }; + } catch (err) { + return false; + } + } + + private async loginOrRegisterProvider( + provider: Provider, + body: LoginUserDto + ) { + const providerInstance = ProvidersFactory.loadProvider(provider); + const providerUser = await providerInstance.getUser(body.providerToken); + if (!providerUser) { + throw new Error('Invalid provider token'); + } + + const user = await this._user.getUserByProvider(providerUser.id, provider); + if (user) { + return user; + } + + const create = await this._organization.createOrgAndUser({ + company: '', + email: providerUser.email, + password: '', + provider, + providerId: providerUser.id, + }); + + return create.users[0].user; + } + + private async jwt(user: User) { + return AuthChecker.signJWT(user); + } +} diff --git a/apps/backend/src/services/auth/permissions/permissions.guard.ts b/apps/backend/src/services/auth/permissions/permissions.guard.ts index 4855a907..648ce13f 100644 --- a/apps/backend/src/services/auth/permissions/permissions.guard.ts +++ b/apps/backend/src/services/auth/permissions/permissions.guard.ts @@ -33,7 +33,9 @@ export class PoliciesGuard implements CanActivate { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error const { org } : {org: Organization} = request; - const ability = await this._authorizationService.check(org.id); + + // @ts-ignore + const ability = await this._authorizationService.check(org.id, org.createdAt, org.users[0].role, policyHandlers); const item = policyHandlers.find((handler) => !this.execPolicyHandler(handler, ability), diff --git a/apps/backend/src/services/auth/permissions/permissions.service.ts b/apps/backend/src/services/auth/permissions/permissions.service.ts index 9b6fc934..7da47600 100644 --- a/apps/backend/src/services/auth/permissions/permissions.service.ts +++ b/apps/backend/src/services/auth/permissions/permissions.service.ts @@ -1,16 +1,20 @@ -import {Ability, AbilityBuilder, AbilityClass} from "@casl/ability"; -import {Injectable} from "@nestjs/common"; -import {pricing} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing"; -import {SubscriptionService} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service"; +import { Ability, AbilityBuilder, AbilityClass } from '@casl/ability'; +import { Injectable } from '@nestjs/common'; +import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing'; +import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; +import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service'; +import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service'; +import dayjs from 'dayjs'; export enum Sections { - FRIENDS = 'friends', - CROSSPOSTING = 'crossposting', - AI = 'ai', - INTEGRATIONS = 'integrations', - TOTALPOSTS = 'totalPosts', - MEDIAS = 'medias', - INFLUENCERS = 'influencers', + CHANNEL = 'channel', + POSTS_PER_MONTH = 'posts_per_month', + TEAM_MEMBERS = 'team_members', + COMMUNITY_FEATURES = 'community_features', + FEATURED_BY_GITROOM = 'featured_by_gitroom', + AI = 'ai', + IMPORT_FROM_CHANNELS = 'import_from_channels', + ADMIN = 'admin', } export enum AuthorizationActions { @@ -26,23 +30,124 @@ export type AppAbility = Ability<[AuthorizationActions, Sections]>; export class PermissionsService { constructor( private _subscriptionService: SubscriptionService, - ) { + private _postsService: PostsService, + private _integrationService: IntegrationService + ) {} + async getPackageOptions(orgId: string) { + const subscription = + await this._subscriptionService.getSubscriptionByOrganizationId(orgId); + return { + subscription, + options: + pricing[ + subscription?.subscriptionTier || !process.env.STRIPE_PUBLISHABLE_KEY + ? 'PRO' + : 'FREE' + ], + }; } - async getPackageOptions(orgId: string) { - const subscription = await this._subscriptionService.getSubscriptionByOrganizationId(orgId); - return pricing[subscription?.subscriptionTier || !process.env.PAYMENT_PUBLIC_KEY ? 'PRO' : 'FREE']; + + async check( + orgId: string, + created_at: Date, + permission: 'USER' | 'ADMIN' | 'SUPERADMIN', + requestedPermission: Array<[AuthorizationActions, Sections]> + ) { + const { can, build } = new AbilityBuilder< + Ability<[AuthorizationActions, Sections]> + >(Ability as AbilityClass); + + if ( + requestedPermission.length === 0 || + !process.env.STRIPE_PUBLISHABLE_KEY + ) { + return build(); } - async check(orgId: string) { - const { can, build } = new AbilityBuilder>(Ability as AbilityClass); + const { subscription, options } = await this.getPackageOptions(orgId); + for (const [action, section] of requestedPermission) { + // check for the amount of channels + if (section === Sections.CHANNEL) { + const totalChannels = ( + await this._integrationService.getIntegrationsList(orgId) + ).length; - // const options = await this.getPackageOptions(orgId); + if ( + (options.channel && options.channel > totalChannels) || + (subscription?.totalChannels || 0) > totalChannels + ) { + can(action, section); + continue; + } + } - return build({ - detectSubjectType: (item) => - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - item.constructor - }); + // check for posts per month + if (section === Sections.POSTS_PER_MONTH) { + const createdAt = + (await this._subscriptionService.getSubscription(orgId))?.createdAt || + created_at; + const totalMonthPast = Math.abs( + dayjs(createdAt).diff(dayjs(), 'month') + ); + const checkFrom = dayjs(createdAt).add(totalMonthPast, 'month'); + const count = await this._postsService.countPostsFromDay( + orgId, + checkFrom.toDate() + ); + + if (count < options.posts_per_month) { + can(action, section); + continue; + } + } + + if (section === Sections.TEAM_MEMBERS && options.team_members) { + can(action, section); + continue; + } + + if ( + section === Sections.ADMIN && + ['ADMIN', 'SUPERADMIN'].includes(permission) + ) { + can(action, section); + continue; + } + + if ( + section === Sections.COMMUNITY_FEATURES && + options.community_features + ) { + can(action, section); + continue; + } + + if ( + section === Sections.FEATURED_BY_GITROOM && + options.featured_by_gitroom + ) { + can(action, section); + continue; + } + + if (section === Sections.AI && options.ai) { + can(action, section); + continue; + } + + if ( + section === Sections.IMPORT_FROM_CHANNELS && + options.import_from_channels + ) { + can(action, section); + } } + + return build({ + detectSubjectType: (item) => + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + item.constructor, + }); + } } diff --git a/apps/backend/src/services/auth/permissions/subscription.exception.ts b/apps/backend/src/services/auth/permissions/subscription.exception.ts index 6734dd06..09b6cc06 100644 --- a/apps/backend/src/services/auth/permissions/subscription.exception.ts +++ b/apps/backend/src/services/auth/permissions/subscription.exception.ts @@ -30,15 +30,15 @@ export class SubscriptionExceptionFilter implements ExceptionFilter { const getErrorMessage = (error: {section: Sections, action: AuthorizationActions}) => { switch (error.section) { - case Sections.AI: + case Sections.POSTS_PER_MONTH: switch (error.action) { default: - return 'You have reached the maximum number of FAQ\'s for your subscription. Please upgrade your subscription to add more FAQ\'s.'; + return 'You have reached the maximum number of posts for your subscription. Please upgrade your subscription to add more posts.'; } - case Sections.CROSSPOSTING: + case Sections.CHANNEL: switch (error.action) { default: - return 'You have reached the maximum number of categories for your subscription. Please upgrade your subscription to add more categories.'; + return 'You have reached the maximum number of channels for your subscription. Please upgrade your subscription to add more channels.'; } } } diff --git a/apps/frontend/src/app/(site)/billing/page.tsx b/apps/frontend/src/app/(site)/billing/page.tsx new file mode 100644 index 00000000..5cdcacdc --- /dev/null +++ b/apps/frontend/src/app/(site)/billing/page.tsx @@ -0,0 +1,21 @@ +import { internalFetch } from '@gitroom/helpers/utils/internal.fetch'; +import { BillingComponent } from '@gitroom/frontend/components/billing/billing.component'; +import { Metadata } from 'next'; +import { redirect } from 'next/navigation'; + +export const metadata: Metadata = { + title: 'Gitroom Billing', + description: '', +}; + +export default async function Page() { + const tiers = await (await internalFetch('/user/subscription/tiers')).json(); + if (tiers?.statusCode === 402) { + return redirect('/'); + } + const { subscription } = await ( + await internalFetch('/user/subscription') + ).json(); + + return ; +} diff --git a/apps/frontend/src/app/(site)/settings/page.tsx b/apps/frontend/src/app/(site)/settings/page.tsx index 3d435470..aacb59f2 100644 --- a/apps/frontend/src/app/(site)/settings/page.tsx +++ b/apps/frontend/src/app/(site)/settings/page.tsx @@ -1,28 +1,37 @@ -import {SettingsComponent} from "@gitroom/frontend/components/settings/settings.component"; -import {internalFetch} from "@gitroom/helpers/utils/internal.fetch"; -import {redirect} from "next/navigation"; -import {RedirectType} from "next/dist/client/components/redirect"; -import {Metadata} from "next"; +import { SettingsComponent } from '@gitroom/frontend/components/settings/settings.component'; +import { internalFetch } from '@gitroom/helpers/utils/internal.fetch'; +import { redirect } from 'next/navigation'; +import { RedirectType } from 'next/dist/client/components/redirect'; +import { Metadata } from 'next'; export const metadata: Metadata = { title: 'Gitroom Settings', description: '', -} -export default async function Index({searchParams}: {searchParams: {code: string}}) { - if (searchParams.code) { - await internalFetch('/settings/github', { - method: 'POST', - body: JSON.stringify({code: searchParams.code}) - }); - - return redirect('/settings', RedirectType.replace); - } - - const {github} = await (await internalFetch('/settings/github')).json(); - const emptyOnes = github.find((p: {login: string}) => !p.login); - const {organizations} = emptyOnes ? await (await internalFetch(`/settings/organizations/${emptyOnes.id}`)).json() : {organizations: []}; - - return ( - - ); +}; +export default async function Index({ + searchParams, +}: { + searchParams: { code: string }; +}) { + if (searchParams.code) { + await internalFetch('/settings/github', { + method: 'POST', + body: JSON.stringify({ code: searchParams.code }), + }); + + return redirect('/settings', RedirectType.replace); + } + + const { github } = await (await internalFetch('/settings/github')).json(); + if (!github) { + return redirect('/'); + } + const emptyOnes = github.find((p: { login: string }) => !p.login); + const { organizations } = emptyOnes + ? await ( + await internalFetch(`/settings/organizations/${emptyOnes.id}`) + ).json() + : { organizations: [] }; + + return ; } diff --git a/apps/frontend/src/components/billing/billing.component.tsx b/apps/frontend/src/components/billing/billing.component.tsx new file mode 100644 index 00000000..13fefe91 --- /dev/null +++ b/apps/frontend/src/components/billing/billing.component.tsx @@ -0,0 +1,14 @@ +import { FC } from 'react'; +import { Subscription } from '@prisma/client'; +import { + NoBillingComponent, + Tiers, +} from '@gitroom/frontend/components/billing/no.billing.component'; + +export const BillingComponent: FC<{ + subscription?: Subscription; + tiers: Tiers; +}> = (props) => { + const { subscription, tiers } = props; + return ; +}; diff --git a/apps/frontend/src/components/billing/no.billing.component.tsx b/apps/frontend/src/components/billing/no.billing.component.tsx new file mode 100644 index 00000000..0d8cc000 --- /dev/null +++ b/apps/frontend/src/components/billing/no.billing.component.tsx @@ -0,0 +1,374 @@ +'use client'; + +import { Slider } from '@gitroom/react/form/slider'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { Button } from '@gitroom/react/form/button'; +import { sortBy } from 'lodash'; +import { Track } from '@gitroom/react/form/track'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { Subscription } from '@prisma/client'; +import { useDebouncedCallback } from 'use-debounce'; +import ReactLoading from 'react-loading'; +import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; +import { useToaster } from '@gitroom/react/toaster/toaster'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import clsx from 'clsx'; +import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing'; +import {useRouter} from "next/navigation"; +dayjs.extend(utc); + +export interface Tiers { + month: Array<{ + name: 'Pro' | 'Standard'; + recurring: 'month' | 'year'; + price: number; + }>; + year: Array<{ + name: 'Pro' | 'Standard'; + recurring: 'month' | 'year'; + price: number; + }>; +} + +export const Prorate: FC<{ + totalChannels: number; + period: 'MONTHLY' | 'YEARLY'; + pack: 'STANDARD' | 'PRO'; +}> = (props) => { + const { totalChannels, period, pack } = props; + const fetch = useFetch(); + const [price, setPrice] = useState(0); + const [loading, setLoading] = useState(false); + + const calculatePrice = useDebouncedCallback(async () => { + setLoading(true); + setPrice( + ( + await ( + await fetch('/billing/prorate', { + method: 'POST', + body: JSON.stringify({ + total: totalChannels, + period, + billing: pack, + }), + }) + ).json() + ).price + ); + setLoading(false); + }, 500); + + useEffect(() => { + setPrice(false); + calculatePrice(); + }, [totalChannels, period, pack]); + + if (loading) { + return ( +
+ +
+ ); + } + + if (price === false) { + return null; + } + + return ( +
+ (Pay Today ${(price < 0 ? 0 : price).toFixed(1)}) +
+ ); +}; + +export const Features: FC<{ + pack: 'FREE' | 'STANDARD' | 'PRO'; + channels: number; +}> = (props) => { + const { pack, channels } = props; + const features = useMemo(() => { + const currentPricing = pricing[pack]; + const channelsOr = currentPricing.channel || channels; + const list = []; + list.push(`${channelsOr} ${channelsOr === 1 ? 'channel' : 'channels'}`); + list.push( + `${ + currentPricing.posts_per_month > 10000 + ? 'Unlimited' + : currentPricing.posts_per_month + } posts per month` + ); + if (currentPricing.team_members) { + list.push(`Unlimited team members`); + } + + if (currentPricing.import_from_channels) { + list.push(`Import content from channels (coming soon)`); + } + + if (currentPricing.community_features) { + list.push(`Community features (coming soon)`); + } + + if (currentPricing.ai) { + list.push(`AI auto-complete (coming soon)`); + } + + if (currentPricing.featured_by_gitroom) { + list.push(`Become featured by Gitroom (coming soon)`); + } + + return list; + }, [pack, channels]); + + return ( +
+ {features.map((feature) => ( +
+
+ + + +
+
{feature}
+
+ ))} +
+ ); +}; + +export const NoBillingComponent: FC<{ + tiers: Tiers; + sub?: Subscription; +}> = (props) => { + const { tiers, sub } = props; + const fetch = useFetch(); + const router = useRouter(); + const toast = useToaster(); + + const [subscription, setSubscription] = useState( + sub + ); + const [loading, setLoading] = useState(false); + const [period, setPeriod] = useState<'MONTHLY' | 'YEARLY'>( + subscription?.period || 'MONTHLY' + ); + const [monthlyOrYearly, setMonthlyOrYearly] = useState<'on' | 'off'>( + period === 'MONTHLY' ? 'off' : 'on' + ); + + const [initialChannels, setInitialChannels] = useState( + subscription?.totalChannels || 1 + ); + const [totalChannels, setTotalChannels] = useState(initialChannels); + + const currentPackage = useMemo(() => { + if (!subscription) { + return 'FREE'; + } + if (initialChannels !== totalChannels) { + return ''; + } + + if (period === 'YEARLY' && monthlyOrYearly === 'off') { + return ''; + } + + if (period === 'MONTHLY' && monthlyOrYearly === 'on') { + return ''; + } + + return subscription?.subscriptionTier; + }, [subscription, totalChannels, initialChannels, monthlyOrYearly, period]); + + const currentDisplay = useMemo(() => { + return sortBy( + [ + { name: 'Free', price: 0 }, + ...(monthlyOrYearly === 'on' ? tiers.year : tiers.month), + ], + (p) => ['Free', 'Standard', 'Pro'].indexOf(p.name) + ); + }, [monthlyOrYearly]); + + const moveToCheckout = useCallback( + (billing: 'STANDARD' | 'PRO' | 'FREE') => async () => { + if (billing === 'FREE') { + if ( + subscription?.cancelAt || + (await deleteDialog( + 'Are you sure you want to cancel your subscription?', + 'Yes, cancel', + 'Cancel Subscription' + )) + ) { + setLoading(true); + const { cancel_at } = await ( + await fetch('/billing/cancel', { + method: 'POST', + }) + ).json(); + + setSubscription((subs) => ({ ...subs!, cancelAt: cancel_at })); + if (cancel_at) + toast.show('Subscription set to canceled successfully'); + if (!cancel_at) toast.show('Subscription reactivated successfully'); + + setLoading(false); + } + return; + } + setLoading(true); + const { url, portal } = await ( + await fetch('/billing/subscribe', { + method: 'POST', + body: JSON.stringify({ + total: totalChannels, + period: monthlyOrYearly === 'on' ? 'YEARLY' : 'MONTHLY', + billing, + }), + }) + ).json(); + + if (url) { + window.location.href = url; + return; + } + + if (portal) { + if ( + await deleteDialog( + 'We could not charge your credit card, please update your payment method', + 'Update', + 'Payment Method Required' + ) + ) { + window.open(portal); + } + } else { + setTotalChannels(totalChannels); + setInitialChannels(totalChannels); + setPeriod(monthlyOrYearly === 'on' ? 'YEARLY' : 'MONTHLY'); + setSubscription((subs) => ({ + ...subs!, + subscriptionTier: billing, + cancelAt: null, + })); + router.refresh(); + toast.show('Subscription updated successfully'); + } + + setLoading(false); + }, + [monthlyOrYearly, totalChannels] + ); + + return ( +
+
+
Plans
+
+
MONTHLY
+
+ +
+
YEARLY
+
+
+
+
Total Channels
+
+ +
+
+
+ {currentDisplay.map((p) => ( +
+
{p.name}
+
+
{p.price ? '$' + totalChannels * p.price : p.name}
+ {!!p.price && ( +
+ {monthlyOrYearly === 'on' ? '/year' : '/month'} +
+ )} +
+
+ {currentPackage === p.name.toUpperCase() && + subscription?.cancelAt ? ( +
+
+ +
+
+ ) : ( + + )} + {subscription && + currentPackage !== p.name.toUpperCase() && + !!p.price && ( + + )} +
+ +
+ ))} +
+
+ ); +}; diff --git a/apps/frontend/src/components/launches/calendar.tsx b/apps/frontend/src/components/launches/calendar.tsx index 01dc4e7d..4696bb2d 100644 --- a/apps/frontend/src/components/launches/calendar.tsx +++ b/apps/frontend/src/components/launches/calendar.tsx @@ -1,6 +1,6 @@ 'use client'; -import { FC, useCallback, useEffect, useMemo, useState, JSX } from 'react'; +import { FC, useCallback, useMemo } from 'react'; import { Integrations, useCalendar, diff --git a/apps/frontend/src/components/layout/layout.context.tsx b/apps/frontend/src/components/layout/layout.context.tsx index b2c27d19..4295a5e3 100644 --- a/apps/frontend/src/components/layout/layout.context.tsx +++ b/apps/frontend/src/components/layout/layout.context.tsx @@ -2,6 +2,7 @@ import {ReactNode, useCallback} from "react"; import {FetchWrapperComponent} from "@gitroom/helpers/utils/custom.fetch"; +import {deleteDialog} from "@gitroom/react/helpers/delete.dialog"; export default async function LayoutContext(params: {children: ReactNode}) { if (params?.children) { @@ -16,6 +17,15 @@ function LayoutContextInner(params: {children: ReactNode}) { if (response?.headers?.get('reload')) { window.location.reload(); } + + if (response.status === 402) { + if (await deleteDialog((await response.json()).message, 'Move to billing', 'Payment Required')) { + window.open('/billing', '_blank'); + } + return false; + } + + return true; }, []); return ( diff --git a/apps/frontend/src/components/layout/layout.settings.tsx b/apps/frontend/src/components/layout/layout.settings.tsx index dba078ca..7121b959 100644 --- a/apps/frontend/src/components/layout/layout.settings.tsx +++ b/apps/frontend/src/components/layout/layout.settings.tsx @@ -7,18 +7,10 @@ import { MantineWrapper } from '@gitroom/react/helpers/mantine.wrapper'; import { ToolTip } from '@gitroom/frontend/components/layout/top.tip'; import { ShowMediaBoxModal } from '@gitroom/frontend/components/media/media.component'; import Image from 'next/image'; -import dynamic from 'next/dynamic'; import { Toaster } from '@gitroom/react/toaster/toaster'; import { ShowPostSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector'; - -const NotificationComponent = dynamic( - () => - import('@gitroom/frontend/components/notifications/notification.component'), - { - loading: () => <>, - ssr: false, - } -); +import { OrganizationSelector } from '@gitroom/frontend/components/layout/organization.selector'; +import NotificationComponent from "@gitroom/frontend/components/notifications/notification.component"; export const LayoutSettings = ({ children }: { children: ReactNode }) => { const user = JSON.parse(headers().get('user')!); @@ -38,8 +30,9 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
Gitroom
-
+
+
diff --git a/apps/frontend/src/components/layout/organization.selector.tsx b/apps/frontend/src/components/layout/organization.selector.tsx new file mode 100644 index 00000000..f4f74c1f --- /dev/null +++ b/apps/frontend/src/components/layout/organization.selector.tsx @@ -0,0 +1,86 @@ +'use client'; + +import { useCallback, useMemo } from 'react'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import useSWR from 'swr'; +import { useUser } from '@gitroom/frontend/components/layout/user.context'; + +export const OrganizationSelector = () => { + const fetch = useFetch(); + const user = useUser(); + + const load = useCallback(async () => { + return await (await fetch('/user/organizations')).json(); + }, []); + + const { data } = useSWR('organizations', load, { + revalidateIfStale: false, + revalidateOnFocus: false, + refreshWhenOffline: false, + refreshWhenHidden: false, + revalidateOnReconnect: false, + }); + + const current = useMemo(() => { + return data?.find((d: any) => d.id === user?.orgId); + }, [data]); + + const withoutCurrent = useMemo(() => { + return data?.filter((d: any) => d.id !== user?.orgId); + }, [current, data]); + + const changeOrg = useCallback( + (org: { name: string; id: string }) => async () => { + await fetch('/user/change-org', { + method: 'POST', + body: JSON.stringify({ id: org.id }), + }); + + window.location.reload(); + }, + [] + ); + + return ( +
+
+
{current?.name || 'Loading...'}
+ {data?.length > 1 && ( +
+ + + + + + + + + + +
+ )} +
+ {data?.length > 1 && ( +
+ {withoutCurrent?.map((org: { name: string; id: string }) => ( +
+ {org.name} +
+ ))} +
+ )} +
+ ); +}; diff --git a/apps/frontend/src/components/layout/top.menu.tsx b/apps/frontend/src/components/layout/top.menu.tsx index 41365738..21480dc0 100644 --- a/apps/frontend/src/components/layout/top.menu.tsx +++ b/apps/frontend/src/components/layout/top.menu.tsx @@ -1,51 +1,75 @@ -"use client"; +'use client'; -import {FC} from "react"; -import Link from "next/link"; -import clsx from "clsx"; -import {usePathname} from "next/navigation"; +import { FC } from 'react'; +import Link from 'next/link'; +import clsx from 'clsx'; +import { usePathname } from 'next/navigation'; +import { useUser } from '@gitroom/frontend/components/layout/user.context'; export const menuItems = [ - { - name: 'Analytics', - icon: 'analytics', - path: '/analytics', - }, - { - name: 'Launches', - icon: 'launches', - path: '/launches', - }, - { - name: 'Media', - icon: 'media', - path: '/media', - }, - { - name: 'Settings', - icon: 'settings', - path: '/settings', - }, - { - name: 'Billing', - icon: 'billing', - path: '/billing', - }, + { + name: 'Analytics', + icon: 'analytics', + path: '/analytics', + }, + { + name: 'Launches', + icon: 'launches', + path: '/launches', + }, + { + name: 'Settings', + icon: 'settings', + path: '/settings', + role: ['ADMIN', 'SUPERADMIN'], + }, + { + name: 'Billing', + icon: 'billing', + path: '/billing', + role: ['ADMIN', 'SUPERADMIN'], + }, ]; export const TopMenu: FC = () => { - const path = usePathname(); - return ( -
-
    - {menuItems.map((item, index) => ( -
  • - p.path).indexOf(path) === index ? 'text-primary showbox' : 'text-gray')}> - {item.name} - -
  • - ))} -
-
- ); -} \ No newline at end of file + const path = usePathname(); + const user = useUser(); + + return ( +
+
    + {menuItems + .filter((f) => { + if (f.role) { + return f.role.includes(user?.role!); + } + return true; + }) + .map((item, index) => ( +
  • + { + if (f.role) { + return f.role.includes(user?.role!); + } + return true; + }) + .map((p) => p.path) + .indexOf(path) === index + ? 'text-primary showbox' + : 'text-gray' + )} + > + {item.name} + +
  • + ))} +
+
+ ); +}; diff --git a/apps/frontend/src/components/layout/user.context.tsx b/apps/frontend/src/components/layout/user.context.tsx index ed21b166..7ca614c3 100644 --- a/apps/frontend/src/components/layout/user.context.tsx +++ b/apps/frontend/src/components/layout/user.context.tsx @@ -1,16 +1,34 @@ -"use client"; +'use client'; -import {createContext, FC, ReactNode, useContext} from "react"; -import {User} from "@prisma/client"; +import { createContext, FC, ReactNode, useContext } from 'react'; +import { User } from '@prisma/client'; +import { + pricing, + PricingInnerInterface, +} from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing'; -export const UserContext = createContext(undefined); +export const UserContext = createContext< + | undefined + | (User & { + orgId: string; + tier: PricingInnerInterface; + role: 'USER' | 'ADMIN' | 'SUPERADMIN'; + }) +>(undefined); -export const ContextWrapper: FC<{user: User, children: ReactNode}> = ({user, children}) => { - return ( - - {children} - - ) -} +export const ContextWrapper: FC<{ + user: User & { + orgId: string; + tier: 'FREE' | 'STANDARD' | 'PRO'; + role: 'USER' | 'ADMIN' | 'SUPERADMIN'; + }; + children: ReactNode; +}> = ({ user, children }) => { + return ( + + {children} + + ); +}; -export const useUser = () => useContext(UserContext); \ No newline at end of file +export const useUser = () => useContext(UserContext); diff --git a/apps/frontend/src/components/notifications/notification.component.tsx b/apps/frontend/src/components/notifications/notification.component.tsx index 0c6dbe00..88fbacdd 100644 --- a/apps/frontend/src/components/notifications/notification.component.tsx +++ b/apps/frontend/src/components/notifications/notification.component.tsx @@ -1,20 +1,110 @@ -"use client"; +'use client'; -import {NotificationBell, NovuProvider, PopoverNotificationCenter} from "@novu/notification-center"; -import {useUser} from "@gitroom/frontend/components/layout/user.context"; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import useSWR from 'swr'; +import { FC, useCallback, useState } from 'react'; +import clsx from 'clsx'; +import { useClickAway } from '@uidotdev/usehooks'; + +export const ShowNotification: FC<{ + notification: { createdAt: string; content: string }; + lastReadNotification: string; +}> = (props) => { + const { notification } = props; + const [newNotification] = useState( + new Date(notification.createdAt) > new Date(props.lastReadNotification) + ); + + return ( +
+ {notification.content} +
+ ); +}; +export const NotificationOpenComponent = () => { + const fetch = useFetch(); + const loadNotifications = useCallback(async () => { + return await (await fetch('/notifications/list')).json(); + }, []); + + const { data, isLoading } = useSWR('notifications', loadNotifications); + + return ( +
+
+ Notifications +
+ +
+ {!isLoading && + data.notifications.map( + ( + notification: { createdAt: string; content: string }, + index: number + ) => ( + + ) + )} +
+
+ ); +}; const NotificationComponent = () => { - const user = useUser(); - return ( - - - {({ unseenCount }) => } - - - ) -} + const fetch = useFetch(); + const [show, setShow] = useState(false); -export default NotificationComponent; \ No newline at end of file + const loadNotifications = useCallback(async () => { + return await (await fetch('/notifications')).json(); + }, []); + + const { data, mutate } = useSWR('notifications-list', loadNotifications); + + const changeShow = useCallback(() => { + mutate( + { ...data, total: 0 }, + { + revalidate: false, + } + ); + setShow(!show); + }, [show, data]); + + const ref = useClickAway(() => setShow(false)); + + return ( +
+
+ {data && data.total > 0 && ( +
+ {data.total} +
+ )} + + + +
+ {show && } +
+ ); +}; + +export default NotificationComponent; diff --git a/apps/frontend/src/components/settings/settings.component.tsx b/apps/frontend/src/components/settings/settings.component.tsx index 1d60f15a..c93b23a8 100644 --- a/apps/frontend/src/components/settings/settings.component.tsx +++ b/apps/frontend/src/components/settings/settings.component.tsx @@ -1,38 +1,34 @@ -import {Button} from "@gitroom/react/form/button"; -import {Checkbox} from "@gitroom/react/form/checkbox"; -import {GithubComponent} from "@gitroom/frontend/components/settings/github.component"; -import {FC} from "react"; +'use client'; -export const SettingsComponent: FC<{organizations: Array<{login: string, id: string}>, github: Array<{id: string, login: string}>}> = (props) => { - const {github, organizations} = props; - return ( -
-
-

Your Git Repository

-
Connect your GitHub repository to receive updates and analytics
- -
-
-
Show news with everybody in Gitroom
-
-
-
-

Team Members

-

Account Managers

-
Invite your assistant or team member to manage your Gitroom account
-
-
-
-
Nevo David
-
Administrator
-
Remove
-
-
-
- -
-
-
+import { Checkbox } from '@gitroom/react/form/checkbox'; +import { GithubComponent } from '@gitroom/frontend/components/settings/github.component'; +import { FC } from 'react'; +import { useUser } from '@gitroom/frontend/components/layout/user.context'; +import { TeamsComponent } from '@gitroom/frontend/components/settings/teams.component'; + +export const SettingsComponent: FC<{ + organizations: Array<{ login: string; id: string }>; + github: Array<{ id: string; login: string }>; +}> = (props) => { + const { github, organizations } = props; + const user = useUser(); + + return ( +
+
+

Your Git Repository

+
+ Connect your GitHub repository to receive updates and analytics
- ); -} \ No newline at end of file + +
+
+ +
+
Show news with everybody in Gitroom
+
+
+ {!!user?.tier.team_members && } +
+ ); +}; diff --git a/apps/frontend/src/components/settings/teams.component.tsx b/apps/frontend/src/components/settings/teams.component.tsx new file mode 100644 index 00000000..3cb8d288 --- /dev/null +++ b/apps/frontend/src/components/settings/teams.component.tsx @@ -0,0 +1,247 @@ +import { Button } from '@gitroom/react/form/button'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import useSWR from 'swr'; +import React, { useCallback, useMemo } from 'react'; +import { useUser } from '@gitroom/frontend/components/layout/user.context'; +import { capitalize } from 'lodash'; +import { useModals } from '@mantine/modals'; +import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; +import { Input } from '@gitroom/react/form/input'; +import { useForm, FormProvider, useWatch } from 'react-hook-form'; +import { Select } from '@gitroom/react/form/select'; +import { Checkbox } from '@gitroom/react/form/checkbox'; +import { classValidatorResolver } from '@hookform/resolvers/class-validator'; +import { AddTeamMemberDto } from '@gitroom/nestjs-libraries/dtos/settings/add.team.member.dto'; +import { useToaster } from '@gitroom/react/toaster/toaster'; +import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; +import copy from 'copy-to-clipboard'; + +const roles = [ + { + name: 'User', + value: 'USER', + }, + { + name: 'Admin', + value: 'ADMIN', + }, +]; + +export const AddMember = () => { + const modals = useModals(); + const fetch = useFetch(); + const toast = useToaster(); + + const resolver = useMemo(() => { + return classValidatorResolver(AddTeamMemberDto); + }, []); + + const form = useForm({ + values: { + email: '', + role: '', + sendEmail: true, + }, + resolver, + mode: 'onChange', + }); + + const sendEmail = useWatch({ + control: form.control, + name: 'sendEmail', + }); + + const submit = useCallback( + async (values: { email: string; role: string; sendEmail: boolean }) => { + const { url } = await ( + await fetch('/settings/team', { + method: 'POST', + body: JSON.stringify(values), + }) + ).json(); + + if (values.sendEmail) { + modals.closeAll(); + toast.show('Invitation link sent'); + return; + } + + copy(url); + modals.closeAll(); + toast.show('Link copied to clipboard'); + }, + [] + ); + + const closeModal = useCallback(() => { + return modals.closeAll(); + }, []); + + return ( + +
+
+ + + + {sendEmail && ( + + )} + +
+
+ +
+
Send invitation via email?
+
+ +
+
+
+ ); +}; + +export const TeamsComponent = () => { + const fetch = useFetch(); + const user = useUser(); + const modals = useModals(); + + const myLevel = user?.role === 'USER' ? 0 : user?.role === 'ADMIN' ? 1 : 2; + const getLevel = useCallback( + (role: 'USER' | 'ADMIN' | 'SUPERADMIN') => + role === 'USER' ? 0 : role === 'ADMIN' ? 1 : 2, + [] + ); + const loadTeam = useCallback(async () => { + return (await (await fetch('/settings/team')).json()).users as Array<{ + id: string; + role: 'SUPERADMIN' | 'ADMIN' | 'USER'; + user: { email: string; id: string }; + }>; + }, []); + + const addMember = useCallback(() => { + modals.openModal({ + classNames: { + modal: 'bg-transparent text-white', + }, + withCloseButton: false, + children: , + }); + }, []); + + const { data, mutate } = useSWR('/api/teams', loadTeam, { + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + }); + + const remove = useCallback( + (toRemove: { user: { id: string } }) => async () => { + console.log(toRemove); + if ( + !(await deleteDialog( + 'Are you sure you want to remove this team member?' + )) + ) { + return; + } + + await fetch(`/settings/team/${toRemove.user.id}`, { + method: 'DELETE', + }); + + await mutate(); + }, + [] + ); + + return ( +
+

Team Members

+

Account Managers

+
+ Invite your assistant or team member to manage your Gitroom account +
+
+
+ {(data || []).map((p) => ( +
+
+ {capitalize(p.user.email.split('@')[0]).split('.')[0]} +
+
+ {p.role === 'USER' + ? 'User' + : p.role === 'ADMIN' + ? 'Admin' + : 'Super Admin'} +
+ {+myLevel > +getLevel(p.role) ? ( +
+ +
+ ) : ( +
+ )} +
+ ))} +
+
+ +
+
+
+ ); +}; diff --git a/apps/frontend/src/middleware.ts b/apps/frontend/src/middleware.ts index e5a8c741..2afce1d0 100644 --- a/apps/frontend/src/middleware.ts +++ b/apps/frontend/src/middleware.ts @@ -1,67 +1,115 @@ -import { NextResponse } from 'next/server' -import type { NextRequest } from 'next/server' -import {fetchBackend} from "@gitroom/helpers/utils/custom.fetch.func"; +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import { fetchBackend } from '@gitroom/helpers/utils/custom.fetch.func'; // This function can be marked `async` if using `await` inside export async function middleware(request: NextRequest) { - const nextUrl = request.nextUrl; - const authCookie = request.cookies.get('auth'); - // If the URL is logout, delete the cookie and redirect to login - if (nextUrl.href.indexOf('/auth/logout') > -1) { - const response = NextResponse.redirect(new URL('/auth/login', nextUrl.href)); - response.cookies.set('auth', '', { - path: '/', - sameSite: false, - httpOnly: true, - secure: true, - maxAge: -1, - domain: '.' + new URL(process.env.FRONTEND_URL!).hostname + const nextUrl = request.nextUrl; + const authCookie = request.cookies.get('auth'); + const showorg = request.cookies.get('showorg'); + + // If the URL is logout, delete the cookie and redirect to login + if (nextUrl.href.indexOf('/auth/logout') > -1) { + const response = NextResponse.redirect( + new URL('/auth/login', nextUrl.href) + ); + response.cookies.set('auth', '', { + path: '/', + sameSite: false, + httpOnly: true, + secure: true, + maxAge: -1, + domain: '.' + new URL(process.env.FRONTEND_URL!).hostname, + }); + return response; + } + + const org = nextUrl.searchParams.get('org'); + const orgUrl = org ? '?org=' + org : ''; + + if (nextUrl.href.indexOf('/auth') === -1 && !authCookie) { + return NextResponse.redirect(new URL(`/auth${orgUrl}`, nextUrl.href)); + } + + // If the url is /auth and the cookie exists, redirect to / + if (nextUrl.href.indexOf('/auth') > -1 && authCookie) { + return NextResponse.redirect(new URL(`/${orgUrl}`, nextUrl.href)); + } + + if (nextUrl.href.indexOf('/auth') > -1 && !authCookie) { + if (org) { + const redirect = NextResponse.redirect(new URL(`/`, nextUrl.href)); + redirect.cookies.set('org', org, { + path: '/', + sameSite: false, + httpOnly: true, + secure: true, + expires: new Date(Date.now() + 15 * 60 * 1000), + domain: '.' + new URL(process.env.FRONTEND_URL!).hostname, + }); + return redirect; + } + return NextResponse.next(); + } + + try { + if (org) { + const { id } = await ( + await fetchBackend('/user/join-org', { + body: JSON.stringify({ + org, + }), + headers: { + auth: authCookie?.value!, + }, + method: 'POST', + }) + ).json(); + + const redirect = NextResponse.redirect( + new URL(`/?added=true`, nextUrl.href) + ); + if (id) { + redirect.cookies.set('showorg', id, { + path: '/', + sameSite: false, + httpOnly: true, + secure: true, + expires: new Date(Date.now() + 15 * 60 * 1000), + domain: '.' + new URL(process.env.FRONTEND_URL!).hostname, }); - return response; + } + + return redirect; } - if (nextUrl.href.indexOf('/auth') === -1 && !authCookie) { - return NextResponse.redirect(new URL('/auth', nextUrl.href)); + const userResponse = await fetchBackend('/user/self', { + headers: { + auth: authCookie?.value!, + ...(showorg?.value ? { showorg: showorg?.value! } : {}), + }, + }); + + if (userResponse.status === 401) { + return NextResponse.redirect(new URL('/auth/logout', nextUrl.href)); } - // If the url is /auth and the cookie exists, redirect to / - if (nextUrl.href.indexOf('/auth') > -1 && authCookie) { - return NextResponse.redirect(new URL('/', nextUrl.href)); + if ([200, 201].indexOf(userResponse.status) === -1) { + return NextResponse.redirect(new URL('/err', nextUrl.href)); } - if (nextUrl.href.indexOf('/auth') > -1) { - return NextResponse.next(); - } + const user = await userResponse.json(); - try { - const userResponse = await fetchBackend('/user/self', { - headers: { - auth: authCookie?.value! - } - }); + const next = NextResponse.next(); + next.headers.set('user', JSON.stringify(user)); - if (userResponse.status === 401) { - return NextResponse.redirect(new URL('/auth/logout', nextUrl.href)); - } - - if ([200, 201].indexOf(userResponse.status) === -1) { - return NextResponse.redirect(new URL('/err', nextUrl.href)); - } - - const user = await userResponse.json(); - - const next = NextResponse.next(); - next.headers.set('user', JSON.stringify(user)); - - return next; - } - catch (err) { - return NextResponse.redirect(new URL('/auth/logout', nextUrl.href)); - } + return next; + } catch (err) { + return NextResponse.redirect(new URL('/auth/logout', nextUrl.href)); + } } // See "Matching Paths" below to learn more export const config = { - matcher: "/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)", -} - + matcher: '/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)', +}; diff --git a/apps/frontend/tailwind.config.js b/apps/frontend/tailwind.config.js index a4b1c8f9..932cc408 100644 --- a/apps/frontend/tailwind.config.js +++ b/apps/frontend/tailwind.config.js @@ -43,10 +43,12 @@ module.exports = { overflow: 'overFlow 0.5s ease-in-out forwards', overflowReverse: 'overFlowReverse 0.5s ease-in-out forwards', fadeDown: 'fadeDown 4s ease-in-out forwards', + normalFadeDown: 'normalFadeDown 0.5s ease-in-out forwards', + newMessages: 'newMessages 1s ease-in-out 4s forwards', }, boxShadow: { yellow: '0 0 60px 20px #6b6237', - green: '0px 0px 50px rgba(60, 124, 90, 0.3)' + green: '0px 0px 50px rgba(60, 124, 90, 0.3)', }, // that is actual animation keyframes: (theme) => ({ @@ -71,6 +73,15 @@ module.exports = { '90%': { opacity: 1, transform: 'translateY(10px)' }, '100%': { opacity: 0, transform: 'translateY(-30px)' }, }, + normalFadeDown: { + '0%': { opacity: 0, transform: 'translateY(-30px)' }, + '100%': { opacity: 1, transform: 'translateY(0)' }, + }, + newMessages: { + '0%': { backgroundColor: '#d7d7d7', fontWeight: 'bold' }, + '99%': { backgroundColor: '#fff', fontWeight: 'bold' }, + '100%': { backgroundColor: '#fff', fontWeight: 'normal' }, + }, }), }, }, diff --git a/libraries/helpers/src/utils/custom.fetch.func.ts b/libraries/helpers/src/utils/custom.fetch.func.ts index b118c43a..b1372bee 100644 --- a/libraries/helpers/src/utils/custom.fetch.func.ts +++ b/libraries/helpers/src/utils/custom.fetch.func.ts @@ -5,9 +5,13 @@ export interface Params { url: string, options: RequestInit, response: Response - ) => Promise; + ) => Promise; } -export const customFetch = (params: Params, auth?: string) => { +export const customFetch = ( + params: Params, + auth?: string, + showorg?: string +) => { return async function newFetch(url: string, options: RequestInit = {}) { const newRequestObject = await params?.beforeRequest?.(url, options); const fetchRequest = await fetch(params.baseUrl + url, { @@ -15,16 +19,28 @@ export const customFetch = (params: Params, auth?: string) => { ...(newRequestObject || options), headers: { ...(auth ? { auth } : {}), + ...(showorg ? { showorg } : {}), ...(options.body instanceof FormData ? {} : { 'Content-Type': 'application/json' }), Accept: 'application/json', ...options?.headers, }, - cache: options.cache || 'no-store', + // @ts-ignore + ...(!options.next && options.cache !== 'force-cache' + ? { cache: options.cache || 'no-store' } + : {}), }); - await params?.afterRequest?.(url, options, fetchRequest); - return fetchRequest; + + if ( + !params?.afterRequest || + (await params?.afterRequest?.(url, options, fetchRequest)) + ) { + return fetchRequest; + } + + // @ts-ignore + return new Promise((res) => {}) as Response; }; }; diff --git a/libraries/helpers/src/utils/custom.fetch.tsx b/libraries/helpers/src/utils/custom.fetch.tsx index 0ba3ef38..ee9bb6a8 100644 --- a/libraries/helpers/src/utils/custom.fetch.tsx +++ b/libraries/helpers/src/utils/custom.fetch.tsx @@ -8,7 +8,9 @@ const FetchProvider = createContext(customFetch( { baseUrl: '', beforeRequest: () => {}, - afterRequest: () => {} + afterRequest: () => { + return true; + } } as Params)); export const FetchWrapperComponent: FC = (props) => { diff --git a/libraries/helpers/src/utils/internal.fetch.ts b/libraries/helpers/src/utils/internal.fetch.ts index c948ec02..86e6cd38 100644 --- a/libraries/helpers/src/utils/internal.fetch.ts +++ b/libraries/helpers/src/utils/internal.fetch.ts @@ -1,4 +1,9 @@ -import {customFetch} from "./custom.fetch.func"; -import {cookies} from "next/headers"; +import { customFetch } from './custom.fetch.func'; +import { cookies } from 'next/headers'; -export const internalFetch = (url: string, options: RequestInit = {}) => customFetch({baseUrl: process.env.BACKEND_INTERNAL_URL!}, cookies()?.get('auth')?.value!)(url, options); \ No newline at end of file +export const internalFetch = (url: string, options: RequestInit = {}) => + customFetch( + { baseUrl: process.env.BACKEND_INTERNAL_URL! }, + cookies()?.get('auth')?.value!, + cookies()?.get('showorg')?.value! + )(url, options); diff --git a/libraries/nestjs-libraries/src/database/prisma/database.module.ts b/libraries/nestjs-libraries/src/database/prisma/database.module.ts index dfe6f039..1d294d5c 100644 --- a/libraries/nestjs-libraries/src/database/prisma/database.module.ts +++ b/libraries/nestjs-libraries/src/database/prisma/database.module.ts @@ -8,7 +8,7 @@ import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/st import { StarsRepository } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.repository'; import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; import { SubscriptionRepository } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.repository'; -import { NotificationService } from '@gitroom/nestjs-libraries/notifications/notification.service'; +import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service'; import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service'; import { IntegrationRepository } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository'; import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service'; @@ -18,6 +18,8 @@ import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/me import { MediaRepository } from '@gitroom/nestjs-libraries/database/prisma/media/media.repository'; import { CommentsRepository } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.repository'; import { CommentsService } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.service'; +import { NotificationsRepository } from '@gitroom/nestjs-libraries/database/prisma/notifications/notifications.repository'; +import { EmailService } from '@gitroom/nestjs-libraries/services/email.service'; @Global() @Module({ @@ -35,6 +37,7 @@ import { CommentsService } from '@gitroom/nestjs-libraries/database/prisma/comme SubscriptionService, SubscriptionRepository, NotificationService, + NotificationsRepository, IntegrationService, IntegrationRepository, PostsService, @@ -44,6 +47,7 @@ import { CommentsService } from '@gitroom/nestjs-libraries/database/prisma/comme CommentsRepository, CommentsService, IntegrationManager, + EmailService, ], get exports() { return this.providers; diff --git a/libraries/nestjs-libraries/src/database/prisma/notifications/notification.service.ts b/libraries/nestjs-libraries/src/database/prisma/notifications/notification.service.ts new file mode 100644 index 00000000..bbe8d5b7 --- /dev/null +++ b/libraries/nestjs-libraries/src/database/prisma/notifications/notification.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common'; +import { NotificationsRepository } from '@gitroom/nestjs-libraries/database/prisma/notifications/notifications.repository'; +import { EmailService } from '@gitroom/nestjs-libraries/services/email.service'; +import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository'; + +@Injectable() +export class NotificationService { + constructor( + private _notificationRepository: NotificationsRepository, + private _emailService: EmailService, + private _organizationRepository: OrganizationRepository + ) {} + + getMainPageCount(organizationId: string, userId: string) { + return this._notificationRepository.getMainPageCount( + organizationId, + userId + ); + } + + getNotifications(organizationId: string, userId: string) { + return this._notificationRepository.getNotifications( + organizationId, + userId + ); + } + + async inAppNotification(orgId: string, subject: string, message: string, sendEmail = false) { + await this._notificationRepository.createNotification(orgId, message); + if (!sendEmail) { + return; + } + + const userOrg = await this._organizationRepository.getAllUsersOrgs(orgId); + for (const user of userOrg?.users || []) { + await this.sendEmail(user.user.email, subject, message); + } + } + + async sendEmail(to: string, subject: string, html: string) { + await this._emailService.sendEmail(to, subject, html); + } +} diff --git a/libraries/nestjs-libraries/src/database/prisma/notifications/notifications.repository.ts b/libraries/nestjs-libraries/src/database/prisma/notifications/notifications.repository.ts new file mode 100644 index 00000000..f9e721ac --- /dev/null +++ b/libraries/nestjs-libraries/src/database/prisma/notifications/notifications.repository.ts @@ -0,0 +1,79 @@ +import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class NotificationsRepository { + constructor( + private _notifications: PrismaRepository<'notifications'>, + private _user: PrismaRepository<'user'> + ) {} + + getLastReadNotification(userId: string) { + return this._user.model.user.findFirst({ + where: { + id: userId, + }, + select: { + lastReadNotifications: true, + }, + }); + } + + async getMainPageCount(organizationId: string, userId: string) { + const { lastReadNotifications } = (await this.getLastReadNotification( + userId + ))!; + + return { + total: await this._notifications.model.notifications.count({ + where: { + organizationId, + createdAt: { + gt: lastReadNotifications!, + }, + }, + }), + }; + } + + async createNotification(organizationId: string, content: string) { + await this._notifications.model.notifications.create({ + data: { + organizationId, + content, + }, + }); + } + + async getNotifications(organizationId: string, userId: string) { + const { lastReadNotifications } = (await this.getLastReadNotification( + userId + ))!; + + await this._user.model.user.update({ + where: { + id: userId, + }, + data: { + lastReadNotifications: new Date(), + }, + }); + + return { + lastReadNotifications, + notifications: await this._notifications.model.notifications.findMany({ + orderBy: { + createdAt: 'desc', + }, + take: 20, + where: { + organizationId, + }, + select: { + createdAt: true, + content: true, + }, + }), + }; + } +} diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts index a9966a5d..1bb222dc 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts @@ -1,63 +1,189 @@ -import {PrismaRepository} from "@gitroom/nestjs-libraries/database/prisma/prisma.service"; -import {Role} from '@prisma/client'; -import {Injectable} from "@nestjs/common"; -import {AuthService} from "@gitroom/helpers/auth/auth.service"; -import {CreateOrgUserDto} from "@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto"; +import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service'; +import { Role, SubscriptionTier } from '@prisma/client'; +import { Injectable } from '@nestjs/common'; +import { AuthService } from '@gitroom/helpers/auth/auth.service'; +import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto'; @Injectable() export class OrganizationRepository { - constructor( - private _organization: PrismaRepository<'organization'> + constructor( + private _organization: PrismaRepository<'organization'>, + private _userOrg: PrismaRepository<'userOrganization'>, + private _user: PrismaRepository<'user'> + ) {} + + async getOrgsByUserId(userId: string) { + return this._organization.model.organization.findMany({ + where: { + users: { + some: { + userId, + }, + }, + }, + include: { + users: { + where: { + userId, + }, + select: { + role: true, + }, + }, + subscription: { + select: { + subscriptionTier: true, + }, + }, + }, + }); + } + + async getOrgById(id: string) { + return this._organization.model.organization.findUnique({ + where: { + id, + }, + }); + } + + async addUserToOrg( + userId: string, + id: string, + orgId: string, + role: 'USER' | 'ADMIN' + ) { + const checkIfInviteExists = await this._user.model.user.findFirst({ + where: { + inviteId: id, + }, + }); + + if (checkIfInviteExists) { + return false; + } + + const checkForSubscription = + await this._organization.model.organization.findFirst({ + where: { + id: orgId, + }, + select: { + subscription: true, + }, + }); + + if ( + !process.env.STRIPE_PUBLISHABLE_KEY || + checkForSubscription?.subscription?.subscriptionTier !== + SubscriptionTier.PRO ) { + return false; } - async getFirstOrgByUserId(userId: string) { - return this._organization.model.organization.findFirst({ - where: { - users: { - some: { - userId - } - } - } - }); - } + const create = await this._userOrg.model.userOrganization.create({ + data: { + role, + userId, + organizationId: orgId, + }, + }); - async getOrgById(id: string) { - return this._organization.model.organization.findUnique({ - where: { - id - } - }); - } + await this._user.model.user.update({ + where: { + id: userId, + }, + data: { + inviteId: id, + }, + }); - async createOrgAndUser(body: Omit & {providerId?: string}) { - return this._organization.model.organization.create({ - data: { - name: body.company, - users: { - create: { - role: Role.USER, - user: { - create: { - email: body.email, - password: body.password ? AuthService.hashPassword(body.password) : '', - providerName: body.provider, - providerId: body.providerId || '', - timezone: 0 - } - } - } - } + return create; + } + + async createOrgAndUser( + body: Omit & { providerId?: string } + ) { + return this._organization.model.organization.create({ + data: { + name: body.company, + users: { + create: { + role: Role.SUPERADMIN, + user: { + create: { + email: body.email, + password: body.password + ? AuthService.hashPassword(body.password) + : '', + providerName: body.provider, + providerId: body.providerId || '', + timezone: 0, + }, }, - select: { + }, + }, + }, + select: { + id: true, + users: { + select: { + user: true, + }, + }, + }, + }); + } + + async getTeam(orgId: string) { + return this._organization.model.organization.findUnique({ + where: { + id: orgId, + }, + select: { + users: { + select: { + role: true, + user: { + select: { + email: true, id: true, - users: { - select: { - user: true - } - } - } - }); - } -} \ No newline at end of file + }, + }, + }, + }, + }, + }); + } + + getAllUsersOrgs(orgId: string) { + return this._organization.model.organization.findUnique({ + where: { + id: orgId, + }, + select: { + users: { + select: { + user: { + select: { + email: true, + id: true, + }, + }, + }, + }, + }, + }); + } + + async deleteTeamMember(orgId: string, userId: string) { + return this._userOrg.model.userOrganization.delete({ + where: { + userId_organizationId: { + userId, + organizationId: orgId, + }, + }, + }); + } +} diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts index 04436021..adfdeb9a 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts @@ -1,26 +1,79 @@ -import {CreateOrgUserDto} from "@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto"; -import {Injectable} from "@nestjs/common"; -import {OrganizationRepository} from "@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository"; -import {NotificationService} from "@gitroom/nestjs-libraries/notifications/notification.service"; +import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto'; +import { Injectable } from '@nestjs/common'; +import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository'; +import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service'; +import { AddTeamMemberDto } from '@gitroom/nestjs-libraries/dtos/settings/add.team.member.dto'; +import { AuthService } from '@gitroom/helpers/auth/auth.service'; +import dayjs from 'dayjs'; +import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; +import { Organization } from '@prisma/client'; @Injectable() export class OrganizationService { - constructor( - private _organizationRepository: OrganizationRepository, - private _notificationsService: NotificationService - ){} - async createOrgAndUser(body: Omit & {providerId?: string}) { - const register = await this._organizationRepository.createOrgAndUser(body); - await this._notificationsService.identifyUser(register.users[0].user); - await this._notificationsService.registerUserToTopic(register.users[0].user.id, `organization:${register.id}`); - return register; + constructor( + private _organizationRepository: OrganizationRepository, + private _notificationsService: NotificationService + ) {} + async createOrgAndUser( + body: Omit & { providerId?: string } + ) { + return this._organizationRepository.createOrgAndUser(body); + } + + addUserToOrg( + userId: string, + id: string, + orgId: string, + role: 'USER' | 'ADMIN' + ) { + return this._organizationRepository.addUserToOrg(userId, id, orgId, role); + } + + getOrgById(id: string) { + return this._organizationRepository.getOrgById(id); + } + + getOrgsByUserId(userId: string) { + return this._organizationRepository.getOrgsByUserId(userId); + } + + getTeam(orgId: string) { + return this._organizationRepository.getTeam(orgId); + } + + async inviteTeamMember(orgId: string, body: AddTeamMemberDto) { + const timeLimit = dayjs().add(15, 'minutes').format('YYYY-MM-DD HH:mm:ss'); + const id = makeId(5); + const url = + process.env.FRONTEND_URL + + `/?org=${AuthService.signJWT({ ...body, orgId, timeLimit, id })}`; + if (body.sendEmail) { + await this._notificationsService.sendEmail( + body.email, + 'You have been invited to join an organization', + `You have been invited to join an organization. Click here to join.
The link will expire in 15 minutes.` + ); + } + return { url }; + } + + async deleteTeamMember(org: Organization, userId: string) { + const userOrgs = await this._organizationRepository.getOrgsByUserId(userId); + const findOrgToDelete = userOrgs.find((orgUser) => orgUser.id === org.id); + if (!findOrgToDelete) { + throw new Error('User is not part of this organization'); } - getOrgById(id: string) { - return this._organizationRepository.getOrgById(id); + // @ts-ignore + const myRole = org.users[0].role; + const userRole = findOrgToDelete.users[0].role; + const myLevel = myRole === 'USER' ? 0 : myRole === 'ADMIN' ? 1 : 2; + const userLevel = userRole === 'USER' ? 0 : userRole === 'ADMIN' ? 1 : 2; + + if (myLevel < userLevel) { + throw new Error('You do not have permission to delete this user'); } - getFirstOrgByUserId(userId: string) { - return this._organizationRepository.getFirstOrgByUserId(userId); - } -} \ No newline at end of file + return this._organizationRepository.deleteTeamMember(org.id, userId); + } +} diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts index eec43af9..5f5d7d5f 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts @@ -153,6 +153,28 @@ export class PostsRepository { }); } + countPostsFromDay(orgId: string, date: Date) { + return this._post.model.post.count({ + where: { + organizationId: orgId, + publishDate: { + gte: date, + }, + OR: [ + { + deletedAt: null, + state: { + in: ['QUEUE'], + }, + }, + { + state: 'PUBLISHED', + }, + ], + }, + }); + } + async createOrUpdatePost( state: 'draft' | 'schedule' | 'now', orgId: string, diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 5cff8904..44319521 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -157,6 +157,10 @@ export class PostsService { } } + async countPostsFromDay(orgId: string, date: Date) { + return this._postRepository.countPostsFromDay(orgId, date); + } + async createPost(orgId: string, body: CreatePostDto) { for (const post of body.posts) { const { previousPost, posts } = diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index fbd5a97a..1a032e31 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -24,6 +24,7 @@ model Organization { Integration Integration[] post Post[] Comments Comments[] + notifications Notifications[] } model User { @@ -37,8 +38,12 @@ model User { comments Comments[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + lastReadNotifications DateTime @default(now()) + inviteId String? @@unique([email, providerName]) + @@index([lastReadNotifications]) + @@index([inviteId]) } model UserOrganization { @@ -51,8 +56,7 @@ model UserOrganization { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - @@index([organizationId]) - @@index([userId]) + @@unique([userId, organizationId]) } model GitHub { @@ -118,14 +122,16 @@ model Subscription { organizationId String @unique organization Organization @relation(fields: [organizationId], references: [id]) subscriptionTier SubscriptionTier - subscriptionState SubscriptionState identifier String? cancelAt DateTime? period Period + totalChannels Int createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + deletedAt DateTime? @@index([organizationId]) + @@index([deletedAt]) } model Integration { @@ -203,6 +209,21 @@ model Post { @@index([parentPostId]) } +model Notifications { + id String @id @default(uuid()) + organizationId String + organization Organization @relation(fields: [organizationId], references: [id]) + content String + link String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + @@index([createdAt]) + @@index([organizationId]) + @@index([deletedAt]) +} + enum State { QUEUE PUBLISHED @@ -211,15 +232,10 @@ enum State { } enum SubscriptionTier { - BASIC + STANDARD PRO } -enum SubscriptionState { - ACTIVE - INACTIVE -} - enum Period { MONTHLY YEARLY diff --git a/libraries/nestjs-libraries/src/database/prisma/stars/stars.service.ts b/libraries/nestjs-libraries/src/database/prisma/stars/stars.service.ts index cfba6768..845e9335 100644 --- a/libraries/nestjs-libraries/src/database/prisma/stars/stars.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/stars/stars.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { StarsRepository } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.repository'; import { chunk, groupBy } from 'lodash'; import dayjs from 'dayjs'; -import { NotificationService } from '@gitroom/nestjs-libraries/notifications/notification.service'; +import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service'; import { StarsListDto } from '@gitroom/nestjs-libraries/dtos/analytics/stars.list.dto'; import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport/client/bull-mq.client'; import { mean } from 'simple-statistics'; @@ -189,33 +189,31 @@ export class StarsService { const getOrganizationsByGitHubLogin = await this._starsRepository.getOrganizationsByGitHubLogin(person.name); for (const org of getOrganizationsByGitHubLogin) { - const topic = `organization:${org.organizationId}`; switch (type) { case Inform.Removed: - return this._notificationsService.sendNotificationToTopic( - 'trending', - topic, - { message: `You are not trending anymore in ${language}` } + return this._notificationsService.inAppNotification( + org.organizationId, + 'You are not trending on GitHub anymore', + `You are not trending anymore in ${language}`, + true ); case Inform.New: - return this._notificationsService.sendNotificationToTopic( - 'trending', - topic, - { - message: `You are trending in ${ - language || 'On the main feed' - } position #${person.position}`, - } + return this._notificationsService.inAppNotification( + org.organizationId, + 'You are trending on GitHub', + `You are trending in ${ + language || 'On the main feed' + } position #${person.position}`, + true ); case Inform.Changed: - return this._notificationsService.sendNotificationToTopic( - 'trending', - topic, - { - message: `You changed position in ${ - language || 'On the main feed' - } position #${person.position}`, - } + return this._notificationsService.inAppNotification( + org.organizationId, + 'You have changed trending position on GitHub', + `You changed position in ${ + language || 'On the main feed' + } position #${person.position}`, + true ); } } @@ -336,10 +334,13 @@ export class StarsService { async predictTrending() { const trendings = (await this.getTrending('')).reverse(); const dates = await this.predictTrendingLoop(trendings); - return dates.map(d => dayjs(d).format('YYYY-MM-DDTHH:mm:00')); + return dates.map((d) => dayjs(d).format('YYYY-MM-DDTHH:mm:00')); } - async predictTrendingLoop(trendings: Array<{ date: Date }>, current = 0): Promise { + async predictTrendingLoop( + trendings: Array<{ date: Date }>, + current = 0 + ): Promise { const dates = trendings.map((result) => dayjs(result.date).toDate()); const intervals = dates .slice(1) @@ -357,7 +358,7 @@ export class StarsService { ).toDate(); if (!nextTrendingDate) { - return []; + return []; } return [ diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts index e2d913ec..574055ef 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts @@ -1,60 +1,39 @@ +export interface PricingInnerInterface { + channel?: number; + posts_per_month: number; + team_members: boolean; + community_features: boolean; + featured_by_gitroom: boolean; + ai: boolean; + import_from_channels: boolean; +} export interface PricingInterface { - [key: string]: { - pricing: { - monthly: number; - yearly: number; - }, - friends: boolean; - crossPosting: boolean; - repository: number; - ai: boolean; - integrations: number; - totalPosts: number; - medias: number; - influencers: boolean; - } + [key: string]: PricingInnerInterface; } export const pricing: PricingInterface = { - FREE: { - pricing: { - monthly: 0, - yearly: 0, - }, - friends: false, - crossPosting: false, - repository: 1, - ai: false, - integrations: 2, - totalPosts: 20, - medias: 2, - influencers: false, - }, - BASIC: { - pricing: { - monthly: 50, - yearly: 500, - }, - friends: false, - crossPosting: true, - repository: 2, - ai: false, - integrations: 4, - totalPosts: 100, - medias: 5, - influencers: true, - }, - PRO: { - pricing: { - monthly: 100, - yearly: 1000, - }, - friends: true, - crossPosting: true, - repository: 4, - ai: true, - integrations: 10, - totalPosts: 300, - medias: 10, - influencers: true, - } -} + FREE: { + channel: 3, + posts_per_month: 30, + team_members: false, + community_features: false, + featured_by_gitroom: false, + ai: false, + import_from_channels: false, + }, + STANDARD: { + posts_per_month: 400, + team_members: false, + ai: true, + community_features: false, + featured_by_gitroom: false, + import_from_channels: true, + }, + PRO: { + posts_per_month: 1000000, + community_features: true, + team_members: true, + featured_by_gitroom: true, + ai: true, + import_from_channels: true, + }, +}; diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts index 00bb28be..dc9458aa 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts @@ -1,19 +1,18 @@ -import {Injectable} from "@nestjs/common"; -import {PrismaRepository} from "@gitroom/nestjs-libraries/database/prisma/prisma.service"; +import { Injectable } from '@nestjs/common'; +import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service'; @Injectable() export class SubscriptionRepository { constructor( private readonly _subscription: PrismaRepository<'subscription'>, - private readonly _organization: PrismaRepository<'organization'>, - ) { - } + private readonly _organization: PrismaRepository<'organization'> + ) {} getSubscriptionByOrganizationId(organizationId: string) { return this._subscription.model.subscription.findFirst({ where: { organizationId, - subscriptionState: 'ACTIVE' + deletedAt: null, }, }); } @@ -23,7 +22,7 @@ export class SubscriptionRepository { where: { organizationId, identifier: subscriptionId, - subscriptionState: 'ACTIVE' + deletedAt: null, }, }); } @@ -32,20 +31,20 @@ export class SubscriptionRepository { return this._subscription.model.subscription.deleteMany({ where: { organization: { - paymentId: customerId - } - } + paymentId: customerId, + }, + }, }); } updateCustomerId(organizationId: string, customerId: string) { return this._organization.model.organization.update({ where: { - id: organizationId + id: organizationId, }, data: { - paymentId: customerId - } + paymentId: customerId, + }, }); } @@ -53,44 +52,62 @@ export class SubscriptionRepository { return this._subscription.model.subscription.findFirst({ where: { organization: { - paymentId: customerId - } - } + paymentId: customerId, + }, + }, }); } async getOrganizationByCustomerId(customerId: string) { return this._organization.model.organization.findFirst({ where: { - paymentId: customerId - } + paymentId: customerId, + }, }); } - async createOrUpdateSubscription(identifier: string, customerId: string, billing: 'BASIC' | 'PRO', period: 'MONTHLY' | 'YEARLY', cancelAt: number | null) { + async createOrUpdateSubscription( + identifier: string, + customerId: string, + totalChannels: number, + billing: 'STANDARD' | 'PRO', + period: 'MONTHLY' | 'YEARLY', + cancelAt: number | null + ) { const findOrg = (await this.getOrganizationByCustomerId(customerId))!; await this._subscription.model.subscription.upsert({ where: { organizationId: findOrg.id, organization: { paymentId: customerId, - } + }, }, update: { subscriptionTier: billing, + totalChannels, period, - subscriptionState: 'ACTIVE', identifier, cancelAt: cancelAt ? new Date(cancelAt * 1000) : null, + deletedAt: null, }, create: { organizationId: findOrg.id, subscriptionTier: billing, + totalChannels, period, - subscriptionState: 'ACTIVE', cancelAt: cancelAt ? new Date(cancelAt * 1000) : null, - identifier - } + identifier, + deletedAt: null, + }, + }); + } + + getSubscription(organizationId: string) { + return this._subscription.model.subscription.findFirst({ + where: { + organizationId, + deletedAt: null, + }, }); } } diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts index 24c31ddc..23a0d27d 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts @@ -25,7 +25,7 @@ export class SubscriptionService { return this._subscriptionRepository.checkSubscription(organizationId, subscriptionId); } - async modifySubscription(customerId: string, billing: 'FREE' | 'BASIC' | 'PRO') { + async modifySubscription(customerId: string, billing: 'FREE' | 'STANDARD' | 'PRO') { const getCurrentSubscription = (await this._subscriptionRepository.getSubscriptionByCustomerId(customerId))!; const from = pricing[getCurrentSubscription?.subscriptionTier || 'FREE']; const to = pricing[billing]; @@ -48,8 +48,12 @@ export class SubscriptionService { // } } - async createOrUpdateSubscription(identifier: string, customerId: string, billing: 'BASIC' | 'PRO', period: 'MONTHLY' | 'YEARLY', cancelAt: number | null) { + async createOrUpdateSubscription(identifier: string, customerId: string, totalChannels: number, billing: 'STANDARD' | 'PRO', period: 'MONTHLY' | 'YEARLY', cancelAt: number | null) { await this.modifySubscription(customerId, billing); - return this._subscriptionRepository.createOrUpdateSubscription(identifier, customerId, billing, period, cancelAt); + return this._subscriptionRepository.createOrUpdateSubscription(identifier, customerId, totalChannels, billing, period, cancelAt); + } + + async getSubscription(organizationId: string) { + return this._subscriptionRepository.getSubscription(organizationId); } } diff --git a/libraries/nestjs-libraries/src/dtos/billing/billing.subscribe.dto.ts b/libraries/nestjs-libraries/src/dtos/billing/billing.subscribe.dto.ts index 86e582db..2254497e 100644 --- a/libraries/nestjs-libraries/src/dtos/billing/billing.subscribe.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/billing/billing.subscribe.dto.ts @@ -1,9 +1,13 @@ -import {IsIn} from "class-validator"; +import {IsIn, Max, Min} from "class-validator"; export class BillingSubscribeDto { + @Min(1) + @Max(60) + total: number; + @IsIn(['MONTHLY', 'YEARLY']) period: 'MONTHLY' | 'YEARLY'; - @IsIn(['BASIC', 'PRO']) - billing: 'BASIC' | 'PRO'; + @IsIn(['STANDARD', 'PRO']) + billing: 'STANDARD' | 'PRO'; } diff --git a/libraries/nestjs-libraries/src/dtos/settings/add.team.member.dto.ts b/libraries/nestjs-libraries/src/dtos/settings/add.team.member.dto.ts new file mode 100644 index 00000000..16fc0e8e --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/settings/add.team.member.dto.ts @@ -0,0 +1,16 @@ +import {IsBoolean, IsDefined, IsEmail, IsIn, IsString, ValidateIf} from 'class-validator'; + +export class AddTeamMemberDto { + @IsDefined() + @IsEmail() + @ValidateIf((o) => o.sendEmail) + email: string; + + @IsString() + @IsIn(['USER', 'ADMIN']) + role: string; + + @IsDefined() + @IsBoolean() + sendEmail: boolean; +} diff --git a/libraries/nestjs-libraries/src/notifications/notification.service.ts b/libraries/nestjs-libraries/src/notifications/notification.service.ts deleted file mode 100644 index f86b8f90..00000000 --- a/libraries/nestjs-libraries/src/notifications/notification.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {Injectable} from "@nestjs/common"; -import {Novu, TriggerRecipientsTypeEnum} from '@novu/node'; -import {User} from "@prisma/client"; - -const novu = new Novu(process.env.NOVU_API_KEY!); - -@Injectable() -export class NotificationService { - async registerUserToTopic(userId: string, topic: string) { - try { - await novu.topics.create({ - name: 'organization topic', - key: topic - }); - } - catch (err) { /* empty */ } - await novu.topics.addSubscribers(topic, { - subscribers: [userId] - }); - } - - async identifyUser(user: User) { - await novu.subscribers.identify(user.id, { - email: user.email, - }); - } - - async sendNotificationToTopic(workflow: string, topic: string, payload = {}) { - await novu.trigger(workflow, { - to: [{type: TriggerRecipientsTypeEnum.TOPIC, topicKey: topic}], - payload - }); - } -} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/redis/redis.module.ts b/libraries/nestjs-libraries/src/redis/redis.module.ts index 4bbcc834..96c5bf9e 100644 --- a/libraries/nestjs-libraries/src/redis/redis.module.ts +++ b/libraries/nestjs-libraries/src/redis/redis.module.ts @@ -1,15 +1,13 @@ -import {Global, Module} from "@nestjs/common"; -import {RedisService} from "@gitroom/nestjs-libraries/redis/redis.service"; +import { Global, Module } from '@nestjs/common'; +import { RedisService } from '@gitroom/nestjs-libraries/redis/redis.service'; @Global() @Module({ - imports: [], - controllers: [], - providers: [RedisService], - get exports() { - return this.providers; - } + imports: [], + controllers: [], + providers: [RedisService], + get exports() { + return this.providers; + }, }) -export class RedisModule { - -} \ No newline at end of file +export class RedisModule {} diff --git a/libraries/nestjs-libraries/src/services/email.service.ts b/libraries/nestjs-libraries/src/services/email.service.ts new file mode 100644 index 00000000..3904a917 --- /dev/null +++ b/libraries/nestjs-libraries/src/services/email.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { Resend } from 'resend'; + +const resend = new Resend(process.env.RESEND_API_KEY); + +@Injectable() +export class EmailService { + async sendEmail(to: string, subject: string, html: string) { + await resend.emails.send({ + from: 'Gitroom ', + to, + subject, + html, + }); + } +} diff --git a/libraries/nestjs-libraries/src/services/stripe.service.ts b/libraries/nestjs-libraries/src/services/stripe.service.ts index b998321f..9f0cb7e7 100644 --- a/libraries/nestjs-libraries/src/services/stripe.service.ts +++ b/libraries/nestjs-libraries/src/services/stripe.service.ts @@ -1,40 +1,72 @@ import Stripe from 'stripe'; -import {Injectable} from "@nestjs/common"; -import {Organization} from "@prisma/client"; -import {SubscriptionService} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service"; -import {OrganizationService} from "@gitroom/nestjs-libraries/database/prisma/organizations/organization.service"; -import {makeId} from "@gitroom/nestjs-libraries/services/make.is"; -import {BillingSubscribeDto} from "@gitroom/nestjs-libraries/dtos/billing/billing.subscribe.dto"; +import { Injectable } from '@nestjs/common'; +import { Organization } from '@prisma/client'; +import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; +import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service'; +import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; +import { BillingSubscribeDto } from '@gitroom/nestjs-libraries/dtos/billing/billing.subscribe.dto'; +import { groupBy } from 'lodash'; -const stripe = new Stripe(process.env.PAYMENT_SECRET_KEY!, { - apiVersion: '2023-10-16' +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: '2023-10-16', }); @Injectable() export class StripeService { constructor( private _subscriptionService: SubscriptionService, - private _organizationService: OrganizationService, - ) { - } + private _organizationService: OrganizationService + ) {} validateRequest(rawBody: Buffer, signature: string, endpointSecret: string) { return stripe.webhooks.constructEvent(rawBody, signature, endpointSecret); } createSubscription(event: Stripe.CustomerSubscriptionCreatedEvent) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const {id, billing, period}: { billing: 'BASIC' | 'PRO', period: 'MONTHLY' | 'YEARLY', id: string} = event.data.object.metadata; - return this._subscriptionService.createOrUpdateSubscription(id, event.data.object.customer as string, billing, period, event.data.object.cancel_at); + const { + id, + billing, + period, + }: { + billing: 'STANDARD' | 'PRO'; + period: 'MONTHLY' | 'YEARLY'; + id: string; + } = event.data.object.metadata; + return this._subscriptionService.createOrUpdateSubscription( + id, + event.data.object.customer as string, + event?.data?.object?.items?.data?.[0]?.quantity || 0, + billing, + period, + event.data.object.cancel_at + ); } updateSubscription(event: Stripe.CustomerSubscriptionUpdatedEvent) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const {id, billing, period}: { billing: 'BASIC' | 'PRO', period: 'MONTHLY' | 'YEARLY', id: string} = event.data.object.metadata; - return this._subscriptionService.createOrUpdateSubscription(id, event.data.object.customer as string, billing, period, event.data.object.cancel_at); + const { + id, + billing, + period, + }: { + billing: 'STANDARD' | 'PRO'; + period: 'MONTHLY' | 'YEARLY'; + id: string; + } = event.data.object.metadata; + return this._subscriptionService.createOrUpdateSubscription( + id, + event.data.object.customer as string, + event?.data?.object?.items?.data?.[0]?.quantity || 0, + billing, + period, + event.data.object.cancel_at + ); } async deleteSubscription(event: Stripe.CustomerSubscriptionDeletedEvent) { - await this._subscriptionService.deleteSubscription(event.data.object.customer as string); + await this._subscriptionService.deleteSubscription( + event.data.object.customer as string + ); } async createOrGetCustomer(organization: Organization) { @@ -43,10 +75,86 @@ export class StripeService { } const customer = await stripe.customers.create(); - await this._subscriptionService.updateCustomerId(organization.id, customer.id); + await this._subscriptionService.updateCustomerId( + organization.id, + customer.id + ); return customer.id; } + async getPackages() { + const products = await stripe.prices.list({ + active: true, + expand: ['data.tiers', 'data.product'], + lookup_keys: [ + 'standard_monthly', + 'standard_yearly', + 'pro_monthly', + 'pro_yearly', + ], + }); + + const productsList = groupBy( + products.data.map((p) => ({ + // @ts-ignore + name: p.product?.name, + recurring: p?.recurring?.interval!, + price: p?.tiers?.[0]?.unit_amount! / 100, + })), + 'recurring' + ); + + return { ...productsList }; + } + + async prorate(organizationId: string, body: BillingSubscribeDto) { + const org = await this._organizationService.getOrgById(organizationId); + const customer = await this.createOrGetCustomer(org!); + + const allProducts = await stripe.products.list({ + active: true, + expand: ['data.prices'], + }); + const findProduct = allProducts.data.find( + (product) => product.name.toLowerCase() === body.billing.toLowerCase() + ); + const pricesList = await stripe.prices.list({ + active: true, + product: findProduct!.id, + }); + + const findPrice = pricesList.data.find( + (p) => + p?.recurring?.interval?.toLowerCase() === + body?.period?.toLowerCase().replace('ly', '') + ); + + const proration_date = Math.floor(Date.now() / 1000); + + const currentUserSubscription = await stripe.subscriptions.list({ + customer, + status: 'active', + }); + + const price = await stripe.invoices.retrieveUpcoming({ + customer, + subscription: currentUserSubscription.data[0].id, + subscription_proration_behavior: 'create_prorations', + subscription_billing_cycle_anchor: 'now', + subscription_items: [ + { + id: currentUserSubscription.data[0].items.data[0].id, + price: findPrice!.id, + quantity: body.total, + }, + ], + subscription_proration_date: proration_date, + }); + + console.log(price); + return { price: price.amount_remaining / 100 }; + } + async setToCancel(organizationId: string) { const id = makeId(10); const org = await this._organizationService.getOrgById(organizationId); @@ -56,15 +164,22 @@ export class StripeService { status: 'active', }); - await stripe.subscriptions.update(currentUserSubscription.data[0].id, { - cancel_at_period_end: !currentUserSubscription.data[0].cancel_at_period_end, - metadata: { - service: 'gitroom', - id + const { cancel_at } = await stripe.subscriptions.update( + currentUserSubscription.data[0].id, + { + cancel_at_period_end: + !currentUserSubscription.data[0].cancel_at_period_end, + metadata: { + service: 'gitroom', + id, + }, } - }); + ); - return {id}; + return { + id, + cancel_at: cancel_at ? new Date(cancel_at * 1000) : undefined, + }; } async getCustomerByOrganizationId(organizationId: string) { @@ -75,9 +190,41 @@ export class StripeService { async createBillingPortalLink(customer: string) { return stripe.billingPortal.sessions.create({ customer, + flow_data: { + type: 'payment_method_update', + }, }); } + private async createCheckoutSession( + uniqueId: string, + customer: string, + metaData: any, + price: string, + quantity: number + ) { + const { url } = await stripe.checkout.sessions.create({ + customer, + success_url: process.env['FRONTEND_URL'] + `/billing?check=${uniqueId}`, + mode: 'subscription', + subscription_data: { + metadata: { + service: 'gitroom', + ...metaData, + uniqueId, + }, + }, + line_items: [ + { + price: price, + quantity: quantity, + }, + ], + }); + + return { url }; + } + async subscribe(organizationId: string, body: BillingSubscribeDto) { const id = makeId(10); @@ -87,58 +234,58 @@ export class StripeService { active: true, expand: ['data.prices'], }); - const findProduct = allProducts.data.find(product => product.name.toLowerCase() === body.billing.toLowerCase()); + const findProduct = allProducts.data.find( + (product) => product.name.toLowerCase() === body.billing.toLowerCase() + ); const pricesList = await stripe.prices.list({ active: true, product: findProduct!.id, }); - const findPrice = pricesList.data.find(p => p?.recurring?.interval?.toLowerCase() === body?.period?.toLowerCase().replace('ly', '')); + const findPrice = pricesList.data.find( + (p) => + p?.recurring?.interval?.toLowerCase() === + body?.period?.toLowerCase().replace('ly', '') + ); const currentUserSubscription = await stripe.subscriptions.list({ customer, status: 'active', }); if (!currentUserSubscription.data.length) { - const {url} = await stripe.checkout.sessions.create({ + return this.createCheckoutSession( + id, customer, - success_url: process.env['FRONTEND_URL'] + `/billing?check=${id}`, - mode: 'subscription', - subscription_data: { - metadata: { - service: 'gitroom', - ...body, - id - } - }, - line_items: [ - { - price: findPrice!.id, - quantity: 1, - }], - }); - - return {url}; + body, + findPrice!.id, + body.total + ); } try { await stripe.subscriptions.update(currentUserSubscription.data[0].id, { + cancel_at_period_end: false, metadata: { service: 'gitroom', ...body, - id - }, items: [{ - id: currentUserSubscription.data[0].items.data[0].id, price: findPrice!.id, - }] + id, + }, + proration_behavior: 'always_invoice', + items: [ + { + id: currentUserSubscription.data[0].items.data[0].id, + price: findPrice!.id, + quantity: body.total, + }, + ], }); - return {id}; - } - catch (err) { - const {url} = await this.createBillingPortalLink(customer); + return { id }; + } catch (err) { + const { url } = await this.createBillingPortalLink(customer); return { - portal: url - } + portal: url, + }; } } } diff --git a/libraries/react-shared-libraries/src/form/button.tsx b/libraries/react-shared-libraries/src/form/button.tsx index c1c5f6c7..6c385847 100644 --- a/libraries/react-shared-libraries/src/form/button.tsx +++ b/libraries/react-shared-libraries/src/form/button.tsx @@ -1,8 +1,60 @@ -import {ButtonHTMLAttributes, DetailedHTMLProps, FC} from "react"; -import {clsx} from "clsx"; +'use client'; -export const Button: FC, HTMLButtonElement> & {secondary?: boolean}> = (props) => { - return ( - + ); +}; diff --git a/libraries/react-shared-libraries/src/form/checkbox.tsx b/libraries/react-shared-libraries/src/form/checkbox.tsx index fd75efdc..a335d39d 100644 --- a/libraries/react-shared-libraries/src/form/checkbox.tsx +++ b/libraries/react-shared-libraries/src/form/checkbox.tsx @@ -1,19 +1,46 @@ -"use client"; -import {FC, useCallback, useState} from "react"; -import clsx from "clsx"; -import Image from "next/image"; +'use client'; +import { FC, useCallback, useState } from 'react'; +import clsx from 'clsx'; +import Image from 'next/image'; +import { useFormContext, useWatch } from 'react-hook-form'; -export const Checkbox: FC<{checked: boolean, className?: string, onChange?: (event: {target: {value: string}}) => void}> = (props) => { - const {checked, className} = props; - const [currentStatus, setCurrentStatus] = useState(checked); - const changeStatus = useCallback(() => { - setCurrentStatus(!currentStatus); - props?.onChange?.({target: {value: `${!currentStatus}`}}); - }, [currentStatus]); +export const Checkbox: FC<{ + checked?: boolean; + disableForm?: boolean; + name?: string; + className?: string; + onChange?: (event: { target: { name?: string, value: boolean } }) => void; +}> = (props) => { + const { checked, className, disableForm } = props; + const form = useFormContext(); + const register = disableForm ? {} : form.register(props.name!); - return ( -
- {currentStatus && Checked} -
- ) -} \ No newline at end of file + const watch = disableForm ? undefined : useWatch({ + name: props.name!, + }); + + const [currentStatus, setCurrentStatus] = useState(watch || checked); + const changeStatus = useCallback(() => { + setCurrentStatus(!currentStatus); + props?.onChange?.({ target: { name: props.name!, value: !currentStatus } }); + if (!disableForm) { + // @ts-ignore + register?.onChange?.({ target: { name: props.name!, value: !currentStatus } }); + } + }, [currentStatus]); + + return ( +
+ {currentStatus && ( + Checked + )} +
+ ); +}; diff --git a/libraries/react-shared-libraries/src/form/slider.tsx b/libraries/react-shared-libraries/src/form/slider.tsx new file mode 100644 index 00000000..3e63fedb --- /dev/null +++ b/libraries/react-shared-libraries/src/form/slider.tsx @@ -0,0 +1,19 @@ +"use client"; + +import {FC, useCallback} from "react"; +import clsx from "clsx"; + +export const Slider: FC<{value: 'on' | 'off', onChange: (value: 'on' | 'off') => void}> = (props) => { + const {value, onChange} = props; + const change = useCallback(() => { + onChange(value === 'on' ? 'off' : 'on'); + }, [value]); + + return ( +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/libraries/react-shared-libraries/src/form/track.tsx b/libraries/react-shared-libraries/src/form/track.tsx new file mode 100644 index 00000000..81be8607 --- /dev/null +++ b/libraries/react-shared-libraries/src/form/track.tsx @@ -0,0 +1,32 @@ +'use client'; +import { Slider } from '@mantine/core'; +import { FC } from 'react'; + +export const Track: FC<{ + value: number; + min: number; + max: number; + onChange: (value: number) => void; +}> = (props) => { + const { value, onChange, min, max } = props; + return ( + + ); +}; diff --git a/libraries/react-shared-libraries/src/helpers/delete.dialog.tsx b/libraries/react-shared-libraries/src/helpers/delete.dialog.tsx index 26aa59e1..36a4cd91 100644 --- a/libraries/react-shared-libraries/src/helpers/delete.dialog.tsx +++ b/libraries/react-shared-libraries/src/helpers/delete.dialog.tsx @@ -1,8 +1,8 @@ import Swal from "sweetalert2"; -export const deleteDialog = async (message: string, confirmButton?: string) => { +export const deleteDialog = async (message: string, confirmButton?: string, title?: string) => { const fire = await Swal.fire({ - title: 'Are you sure?', + title: title || 'Are you sure?', text: message, icon: 'warning', showCancelButton: true, diff --git a/package-lock.json b/package-lock.json index 3dd448cd..c5ec177e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,14 +16,13 @@ "@hookform/resolvers": "^3.3.4", "@mantine/core": "^5.10.5", "@mantine/modals": "^5.10.5", + "@nestjs/cache-manager": "^2.2.1", "@nestjs/common": "^10.0.2", "@nestjs/core": "^10.0.2", "@nestjs/microservices": "^10.3.1", "@nestjs/platform-express": "^10.0.2", "@nestjs/schedule": "^4.0.0", "@nestjs/serve-static": "^4.0.1", - "@novu/node": "^0.23.0", - "@novu/notification-center": "^0.23.0", "@prisma/client": "^5.8.1", "@swc/helpers": "~0.5.2", "@sweetalert2/theme-dark": "^5.0.16", @@ -40,12 +39,14 @@ "@virtual-grid/react": "^2.0.2", "axios": "^1.0.0", "bcrypt": "^5.1.1", + "bufferutil": "^4.0.8", "bullmq": "^5.1.5", "chart.js": "^4.4.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "clsx": "^2.1.0", "cookie-parser": "^1.4.6", + "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.10", "ioredis": "^5.3.2", "json-to-graphql-query": "^2.2.5", @@ -62,14 +63,17 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "18.2.0", "react-hook-form": "^7.50.1", + "react-loading": "^2.0.3", "react-query": "^3.39.3", "react-router-dom": "6.11.2", + "react-slider": "^2.0.6", "react-tag-autocomplete": "^7.2.0", "react-tooltip": "^5.26.2", "react-use-keypress": "^1.3.1", "redis": "^4.6.12", "reflect-metadata": "^0.1.13", "remove-markdown": "^0.5.0", + "resend": "^3.2.0", "rxjs": "^7.8.0", "sharp": "^0.33.2", "simple-statistics": "^7.8.3", @@ -79,6 +83,7 @@ "tslib": "^2.3.0", "twitter-api-v2": "^1.16.0", "use-debounce": "^10.0.0", + "utf-8-validate": "^5.0.10", "yargs": "^17.7.2" }, "devDependencies": { @@ -2419,6 +2424,7 @@ "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", @@ -2437,6 +2443,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -2450,12 +2457,14 @@ "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "peer": true }, "node_modules/@emotion/babel-plugin/node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "peer": true, "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -2471,6 +2480,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "peer": true, "engines": { "node": ">=10" }, @@ -2482,6 +2492,7 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2490,6 +2501,7 @@ "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "peer": true, "dependencies": { "@emotion/memoize": "^0.8.1", "@emotion/sheet": "^1.2.2", @@ -2498,40 +2510,23 @@ "stylis": "4.2.0" } }, - "node_modules/@emotion/css": { - "version": "11.11.2", - "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.11.2.tgz", - "integrity": "sha512-VJxe1ucoMYMS7DkiMdC2T7PWNbrEI0a39YRiyDvK2qq4lXwjRbVP/z4lpG+odCsRzadlR+1ywwrTzhdm5HNdew==", - "dependencies": { - "@emotion/babel-plugin": "^11.11.0", - "@emotion/cache": "^11.11.0", - "@emotion/serialize": "^1.1.2", - "@emotion/sheet": "^1.2.2", - "@emotion/utils": "^1.2.1" - } - }, "node_modules/@emotion/hash": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", - "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", - "dependencies": { - "@emotion/memoize": "^0.8.1" - } + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==", + "peer": true }, "node_modules/@emotion/memoize": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "peer": true }, "node_modules/@emotion/react": { "version": "11.11.3", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz", "integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.11.0", @@ -2555,6 +2550,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz", "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==", + "peer": true, "dependencies": { "@emotion/hash": "^0.9.1", "@emotion/memoize": "^0.8.1", @@ -2566,39 +2562,20 @@ "node_modules/@emotion/sheet": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", - "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" - }, - "node_modules/@emotion/styled": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", - "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.11.0", - "@emotion/is-prop-valid": "^1.2.1", - "@emotion/serialize": "^1.1.2", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", - "@emotion/utils": "^1.2.1" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0-rc.0", - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==", + "peer": true }, "node_modules/@emotion/unitless": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "peer": true }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peer": true, "peerDependencies": { "react": ">=16.8.0" } @@ -2606,12 +2583,14 @@ "node_modules/@emotion/utils": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", - "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==", + "peer": true }, "node_modules/@emotion/weak-memoize": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", - "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==", + "peer": true }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.12", @@ -3593,7 +3572,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -3610,7 +3588,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -3622,7 +3599,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "engines": { "node": ">=12" }, @@ -3633,14 +3609,12 @@ "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -3657,7 +3631,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -3672,7 +3645,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -4183,6 +4155,7 @@ "version": "5.10.5", "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-5.10.5.tgz", "integrity": "sha512-hFQp71QZDfivPzfIUOQZfMKLiOL/Cn2EnzacRlbUr55myteTfzYN8YMt+nzniE/6c4IRopFHEAdbKEtfyQc6kg==", + "peer": true, "peerDependencies": { "react": ">=16.8.0" } @@ -4368,6 +4341,17 @@ "win32" ] }, + "node_modules/@nestjs/cache-manager": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.2.1.tgz", + "integrity": "sha512-mXj0zenuyMPJICokwVud4Kjh0+pzBNBAgfpx3I48LozNkd8Qfv/MAhZsb15GihGpbFRxafUo3p6XvtAqRm8GRw==", + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "cache-manager": "<=5", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/common": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.1.tgz", @@ -4831,93 +4815,6 @@ "node": ">= 8" } }, - "node_modules/@novu/client": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@novu/client/-/client-0.23.0.tgz", - "integrity": "sha512-iy4v1+uB38vxDn6IRXqU1yVA2Z2ZtIyKQW9S8bZVhw+zVfrqdhzyKUE/ZkjR62UrNWTzPPhSvZGsqdVAD55PwA==", - "dependencies": { - "@novu/shared": "^0.23.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@novu/node": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@novu/node/-/node-0.23.0.tgz", - "integrity": "sha512-buH4wCrxsb5RL6TZ3w2YfMhYPvmmoYO5+CkBfvn6hibGXufft5+4DPx8t002aRANQO8SKdMH3+HPvgVhsORMWw==", - "dependencies": { - "@novu/shared": "^0.23.0", - "axios-retry": "^3.8.0", - "handlebars": "^4.7.7", - "lodash.get": "^4.4.2", - "lodash.merge": "^4.6.2", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@novu/node/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@novu/notification-center": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@novu/notification-center/-/notification-center-0.23.0.tgz", - "integrity": "sha512-uZ/3jDrwJ3DWGzP0ckd650pCF93GlvluBoXulL99oeVNAJNOmH/UyVygG3KVPkVA0so/pZ3b8TWi9v8z20cYdQ==", - "dependencies": { - "@emotion/css": "^11.10.5", - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@mantine/core": "^5.7.1", - "@mantine/hooks": "^5.7.1", - "@novu/client": "^0.23.0", - "@novu/shared": "^0.23.0", - "@tanstack/react-query": "^4.20.4", - "acorn-jsx": "^5.3.2", - "axios": "^1.6.2", - "lodash.clonedeep": "^4.5.0", - "lodash.debounce": "^4.0.8", - "lodash.merge": "^4.6.2", - "react-infinite-scroll-component": "^6.0.0", - "socket.io-client": "4.7.2", - "tslib": "^2.3.1", - "webfontloader": "^1.6.28" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@novu/shared": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@novu/shared/-/shared-0.23.0.tgz", - "integrity": "sha512-mw71K74RojlbuKx+mYwxj8aYS1rqi+lHN1TeQ4hsKkM8+Kf3Ypu4IZHGa1DJhUSi7+UOXHlj3ypjkWHB65PGKg==", - "dependencies": { - "axios": "^1.6.2", - "class-transformer": "0.5.1", - "class-validator": "0.14.0" - } - }, - "node_modules/@novu/shared/node_modules/class-validator": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.0.tgz", - "integrity": "sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==", - "dependencies": { - "@types/validator": "^13.7.10", - "libphonenumber-js": "^1.10.14", - "validator": "^13.7.0" - } - }, "node_modules/@nrwl/devkit": { "version": "17.2.8", "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-17.2.8.tgz", @@ -5663,6 +5560,11 @@ "yargs-parser": "21.1.1" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==" + }, "node_modules/@phenomnomnominal/tsquery": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-5.0.1.tgz", @@ -5679,7 +5581,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "optional": true, "engines": { "node": ">=14" @@ -5953,6 +5854,20 @@ "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" }, + "node_modules/@react-email/render": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.12.tgz", + "integrity": "sha512-S8WRv/PqECEi6x0QJBj0asnAb5GFtJaHlnByxLETLkgJjc76cxMYDH4r9wdbuJ4sjkcbpwP3LPnVzwS+aIjT7g==", + "dependencies": { + "html-to-text": "9.0.5", + "js-beautify": "^1.14.11", + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@redis/bloom": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", @@ -6194,6 +6109,18 @@ "integrity": "sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA==", "dev": true }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -6230,11 +6157,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" - }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -6902,41 +6824,6 @@ "node": ">=10" } }, - "node_modules/@tanstack/query-core": { - "version": "4.36.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", - "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "4.36.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", - "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", - "dependencies": { - "@tanstack/query-core": "4.36.1", - "use-sync-external-store": "^1.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-native": "*" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, "node_modules/@tanstack/react-virtual": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.1.3.tgz", @@ -8512,6 +8399,7 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -8542,6 +8430,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -9018,15 +8907,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/axios-retry": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.9.1.tgz", - "integrity": "sha512-8PJDLJv7qTTMMwdnbMvrLYuvB47M81wRtxQmEdV5w4rgbTXTt+vtPkXwajOfOdSyv/wZICJOC+/UhXH4aQ/R+w==", - "dependencies": { - "@babel/runtime": "^7.15.4", - "is-retry-allowed": "^2.2.0" - } - }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -9713,6 +9593,18 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/builtins": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", @@ -9845,6 +9737,26 @@ "node": ">=8" } }, + "node_modules/cache-manager": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.4.0.tgz", + "integrity": "sha512-FS7o8vqJosnLpu9rh2gQTo8EOzCRJLF1BJ4XDEUDMqcfvs7SJZs5iuoFTXLauzQ3S5v8sBAST1pCwMaurpyi1A==", + "peer": true, + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^10.1.0", + "promise-coalesce": "^1.1.2" + } + }, + "node_modules/cache-manager/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "peer": true, + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -10504,6 +10416,15 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -10591,6 +10512,14 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/copy-webpack-plugin": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz", @@ -10841,7 +10770,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -11366,7 +11294,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -11682,7 +11609,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -11696,7 +11622,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, "funding": [ { "type": "github", @@ -11721,7 +11646,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, "dependencies": { "domelementtype": "^2.3.0" }, @@ -11736,7 +11660,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dev": true, "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -11786,8 +11709,7 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", @@ -11797,6 +11719,53 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -11866,46 +11835,6 @@ "once": "^1.4.0" } }, - "node_modules/engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -13430,7 +13359,8 @@ "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "peer": true }, "node_modules/find-up": { "version": "5.0.0", @@ -13524,7 +13454,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -13540,7 +13469,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -14027,26 +13955,6 @@ "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "dev": true }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, "node_modules/harmony-reflect": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", @@ -14519,6 +14427,21 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/html-url-attributes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.0.tgz", @@ -14537,6 +14460,24 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -14900,6 +14841,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/inline-style-parser": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.2.tgz", @@ -15267,17 +15213,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-retry-allowed": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", - "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-set": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", @@ -15426,8 +15361,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isobject": { "version": "3.0.1", @@ -15549,7 +15483,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -16398,6 +16331,99 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-beautify": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.3.3", + "js-cookie": "^3.0.5", + "nopt": "^7.2.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/js-beautify/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/nopt": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz", + "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } + }, "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -16696,6 +16722,14 @@ "shell-quote": "^1.8.1" } }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/less": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", @@ -16896,7 +16930,8 @@ "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "peer": true }, "node_modules/lodash.compact": { "version": "3.0.1", @@ -16906,7 +16941,8 @@ "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true }, "node_modules/lodash.defaults": { "version": "4.2.0", @@ -16918,11 +16954,6 @@ "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==" }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" - }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -16967,7 +16998,8 @@ "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, "node_modules/lodash.once": { "version": "4.1.1", @@ -18359,7 +18391,8 @@ "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true }, "node_modules/nestjs-command": { "version": "3.1.4", @@ -18525,6 +18558,16 @@ "node": ">= 6.13.0" } }, + "node_modules/node-gyp-build": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-gyp-build-optional-packages": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz", @@ -19141,6 +19184,18 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -19170,7 +19225,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -19184,7 +19238,6 @@ "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dev": true, "dependencies": { "lru-cache": "^9.1.1 || ^10.0.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -19200,7 +19253,6 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "dev": true, "engines": { "node": "14 || >=16.14" } @@ -19233,6 +19285,14 @@ "node": "*" } }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/peek-readable": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", @@ -20168,6 +20228,15 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/promise-coalesce": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz", + "integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==", + "peer": true, + "engines": { + "node": ">=16" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -20185,7 +20254,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -20195,8 +20263,7 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/property-information": { "version": "6.4.1", @@ -20207,6 +20274,11 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -20447,17 +20519,6 @@ "react": "^16.8.0 || ^17 || ^18" } }, - "node_modules/react-infinite-scroll-component": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", - "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", - "dependencies": { - "throttle-debounce": "^2.1.0" - }, - "peerDependencies": { - "react": ">=16.0.0" - } - }, "node_modules/react-intersection-observer": { "version": "9.8.1", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.8.1.tgz", @@ -20478,6 +20539,15 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/react-loading": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/react-loading/-/react-loading-2.0.3.tgz", + "integrity": "sha512-Vdqy79zq+bpeWJqC+xjltUjuGApyoItPgL0vgVfcJHhqwU7bAMKzysfGW/ADu6i0z0JiOCRJjo+IkFNkRNbA3A==", + "peerDependencies": { + "prop-types": "^15.6.0", + "react": ">=0.14.0" + } + }, "node_modules/react-markdown": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz", @@ -20567,6 +20637,17 @@ "react-dom": ">=16.8" } }, + "node_modules/react-slider": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/react-slider/-/react-slider-2.0.6.tgz", + "integrity": "sha512-gJxG1HwmuMTJ+oWIRCmVWvgwotNCbByTwRkFZC6U4MBsHqJBmxwbYRJUmxy4Tke1ef8r9jfXjgkmY/uHOCEvbA==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18" + } + }, "node_modules/react-tag-autocomplete": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/react-tag-autocomplete/-/react-tag-autocomplete-7.2.0.tgz", @@ -21110,6 +21191,17 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "node_modules/resend": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/resend/-/resend-3.2.0.tgz", + "integrity": "sha512-lDHhexiFYPoLXy7zRlJ8D5eKxoXy6Tr9/elN3+Vv7PkUoYuSSD1fpiIfa/JYXEWyiyN2UczkCTLpkT8dDPJ4Pg==", + "dependencies": { + "@react-email/render": "0.0.12" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -21471,6 +21563,17 @@ "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", "dev": true }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -21813,7 +21916,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -21825,7 +21927,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -21923,32 +22024,6 @@ "tslib": "^2.0.3" } }, - "node_modules/socket.io-client": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", - "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -21988,6 +22063,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -22187,7 +22263,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -22291,7 +22366,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -22473,7 +22547,8 @@ "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "peer": true }, "node_modules/stylus": { "version": "0.59.0", @@ -23065,14 +23140,6 @@ "node": ">=0.8" } }, - "node_modules/throttle-debounce": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", - "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -23152,6 +23219,11 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -23596,18 +23668,6 @@ "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", "dev": true }, - "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/uid": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", @@ -23988,6 +24048,18 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -24328,11 +24400,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/webfontloader": { - "version": "1.6.28", - "resolved": "https://registry.npmjs.org/webfontloader/-/webfontloader-1.6.28.tgz", - "integrity": "sha512-Egb0oFEga6f+nSgasH3E0M405Pzn6y3/9tOVanv/DLfa1YBIgcv90L18YyWnvXkRbIM17v5Kv6IT2N6g1x5tvQ==" - }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -24722,7 +24789,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -24813,11 +24879,6 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -24839,7 +24900,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -24906,14 +24966,6 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 98f8ea94..ec9b4be3 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "license": "MIT", "scripts": { - "dev": "concurrently \"stripe listen --forward-to localhost:3000/payment\" \"nx run-many --target=serve --projects=frontend,backend,workers --parallel=4\"", + "dev": "concurrently \"stripe listen --forward-to localhost:3000/stripe\" \"nx run-many --target=serve --projects=frontend,backend,workers --parallel=4\"", "workers": "nx run workers:serve:development", "cron": "nx run cron:serve:development", "command": "nx run commands:build && nx run commands:command", @@ -16,14 +16,13 @@ "@hookform/resolvers": "^3.3.4", "@mantine/core": "^5.10.5", "@mantine/modals": "^5.10.5", + "@nestjs/cache-manager": "^2.2.1", "@nestjs/common": "^10.0.2", "@nestjs/core": "^10.0.2", "@nestjs/microservices": "^10.3.1", "@nestjs/platform-express": "^10.0.2", "@nestjs/schedule": "^4.0.0", "@nestjs/serve-static": "^4.0.1", - "@novu/node": "^0.23.0", - "@novu/notification-center": "^0.23.0", "@prisma/client": "^5.8.1", "@swc/helpers": "~0.5.2", "@sweetalert2/theme-dark": "^5.0.16", @@ -40,12 +39,14 @@ "@virtual-grid/react": "^2.0.2", "axios": "^1.0.0", "bcrypt": "^5.1.1", + "bufferutil": "^4.0.8", "bullmq": "^5.1.5", "chart.js": "^4.4.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "clsx": "^2.1.0", "cookie-parser": "^1.4.6", + "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.10", "ioredis": "^5.3.2", "json-to-graphql-query": "^2.2.5", @@ -62,14 +63,17 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "18.2.0", "react-hook-form": "^7.50.1", + "react-loading": "^2.0.3", "react-query": "^3.39.3", "react-router-dom": "6.11.2", + "react-slider": "^2.0.6", "react-tag-autocomplete": "^7.2.0", "react-tooltip": "^5.26.2", "react-use-keypress": "^1.3.1", "redis": "^4.6.12", "reflect-metadata": "^0.1.13", "remove-markdown": "^0.5.0", + "resend": "^3.2.0", "rxjs": "^7.8.0", "sharp": "^0.33.2", "simple-statistics": "^7.8.3", @@ -79,6 +83,7 @@ "tslib": "^2.3.0", "twitter-api-v2": "^1.16.0", "use-debounce": "^10.0.0", + "utf-8-validate": "^5.0.10", "yargs": "^17.7.2" }, "devDependencies": {