feat: change password page

This commit is contained in:
Sylver 2024-02-15 19:20:35 +08:00
parent eb15ce1ea6
commit 126be8d8c6
14 changed files with 304 additions and 73 deletions

View File

@ -30,7 +30,7 @@ export class UserResolver {
@InjectRepository(UserVerification) private readonly verificationRepo: EntityRepository<UserVerification>,
private readonly authService: AuthService,
private readonly userService: UserService,
private readonly inviteService: InviteService
private readonly inviteService: InviteService,
) {}
@Query(() => User)
@ -51,7 +51,7 @@ export class UserResolver {
@UserId() userId: string,
@Parent() user: User,
@Args('first', { nullable: true }) limit: number = 0,
@Args('after', { nullable: true }) cursor?: string
@Args('after', { nullable: true }) cursor?: string,
): Promise<FilePage> {
if (userId !== user.id) throw new UnauthorizedException();
if (limit > 100) limit = 100;
@ -74,7 +74,7 @@ export class UserResolver {
@UserId() userId: string,
@Parent() user: User,
@Args('first', { nullable: true }) limit: number = 0,
@Args('after', { nullable: true }) cursor?: string
@Args('after', { nullable: true }) cursor?: string,
): Promise<PastePage> {
if (userId !== user.id) throw new UnauthorizedException();
if (limit > 100) limit = 100;
@ -119,11 +119,22 @@ export class UserResolver {
return this.userService.createUser(data, invite);
}
@Mutation(() => Boolean)
@UseGuards(JWTAuthGuard)
async changePassword(
@UserId() userId: string,
@Args('currentPassword') currentPassword: string,
@Args('newPassword') newPassword: string,
) {
await this.userService.changePassword(userId, currentPassword, newPassword);
return true;
}
@Mutation(() => Boolean)
@UseGuards(JWTAuthGuard)
async resendVerificationEmail(
@UserId() userId: string,
@Args('data', { nullable: true }) body?: ResendVerificationEmailDto
@Args('data', { nullable: true }) body?: ResendVerificationEmailDto,
) {
const user = await this.userService.getUser(userId, false);
const latestVerification = await this.verificationRepo.findOne(
@ -137,7 +148,7 @@ export class UserResolver {
orderBy: {
expiresAt: 'DESC',
},
}
},
);
if (latestVerification && latestVerification.expiresAt.getTime() > Date.now() + UserResolver.MIN_RESEND_INTERVAL) {

View File

@ -125,7 +125,7 @@ export class UserService {
throw new ConflictException('You must provide an email address to create a user.');
}
const hashedPassword = await bcrypt.hash(data.password, 10);
const hashedPassword = await bcrypt.hash(data.password, 12);
const user = this.userRepo.create({
id: generateContentId(),
secret: nanoid(),
@ -190,6 +190,17 @@ export class UserService {
}
}
async changePassword(userId: string, currentPassword: string, newPassword: string) {
const user = await this.userRepo.findOneOrFail(userId);
const passwordMatches = await bcrypt.compare(currentPassword, user.password);
if (!passwordMatches) {
throw new BadRequestException('Invalid password');
}
user.password = await bcrypt.hash(newPassword, 12);
await this.userRepo.persistAndFlush(user);
}
@Cron(CronExpression.EVERY_HOUR)
async deleteExpiredVerifications() {
await this.verificationRepo.nativeDelete({

View File

@ -102,6 +102,7 @@ type Link {
}
type Mutation {
changePassword(currentPassword: String!, newPassword: String!): Boolean!
confirmOTP(otpCode: String!): Boolean!
createInvite: Invite!
createLink(destination: String!, host: String): Link!

View File

@ -11,6 +11,7 @@
},
"scripts": {
"build": "tsc --noEmit && rm -rf ./dist/* && vavite build && tsup && rm -rf ./dist/server",
"lint": "eslint --fix ./src/**/*.{ts,tsx}",
"start": "node ./dist/index.js",
"watch": "vavite serve"
},
@ -57,7 +58,6 @@
"vavite": "^4.0.3",
"vike": "^0.4.161",
"vite": "^5.1.1",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-graphql-codegen": "^3.3.6",
"yup": "^1.3.3"
}

View File

@ -38,6 +38,8 @@ const documents = {
'\n mutation DisableOTP($otpCode: String!) {\n disableOTP(otpCode: $otpCode)\n }\n': types.DisableOtpDocument,
'\n query UserQueryWithToken {\n user {\n ...RegularUser\n token\n otpEnabled\n }\n }\n':
types.UserQueryWithTokenDocument,
'\n mutation ChangePassword($oldPassword: String!, $newPassword: String!) {\n changePassword(currentPassword: $oldPassword, newPassword: $newPassword)\n }\n':
types.ChangePasswordDocument,
'\n query GetFile($fileId: ID!) {\n file(fileId: $fileId) {\n id\n type\n displayName\n size\n sizeFormatted\n textContent\n isOwner\n metadata {\n height\n width\n }\n paths {\n view\n thumbnail\n direct\n }\n urls {\n view\n }\n }\n }\n':
types.GetFileDocument,
'\n mutation DeleteFile($fileId: ID!, $deleteKey: String) {\n deleteFile(fileId: $fileId, key: $deleteKey)\n }\n':
@ -158,6 +160,12 @@ export function graphql(
export function graphql(
source: '\n query UserQueryWithToken {\n user {\n ...RegularUser\n token\n otpEnabled\n }\n }\n',
): (typeof documents)['\n query UserQueryWithToken {\n user {\n ...RegularUser\n token\n otpEnabled\n }\n }\n'];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: '\n mutation ChangePassword($oldPassword: String!, $newPassword: String!) {\n changePassword(currentPassword: $oldPassword, newPassword: $newPassword)\n }\n',
): (typeof documents)['\n mutation ChangePassword($oldPassword: String!, $newPassword: String!) {\n changePassword(currentPassword: $oldPassword, newPassword: $newPassword)\n }\n'];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@ -118,6 +118,7 @@ export type Link = {
export type Mutation = {
__typename?: 'Mutation';
changePassword: Scalars['Boolean']['output'];
confirmOTP: Scalars['Boolean']['output'];
createInvite: Invite;
createLink: Link;
@ -131,6 +132,11 @@ export type Mutation = {
resendVerificationEmail: Scalars['Boolean']['output'];
};
export type MutationChangePasswordArgs = {
currentPassword: Scalars['String']['input'];
newPassword: Scalars['String']['input'];
};
export type MutationConfirmOtpArgs = {
otpCode: Scalars['String']['input'];
};
@ -458,6 +464,13 @@ export type UserQueryWithTokenQuery = {
};
};
export type ChangePasswordMutationVariables = Exact<{
oldPassword: Scalars['String']['input'];
newPassword: Scalars['String']['input'];
}>;
export type ChangePasswordMutation = { __typename?: 'Mutation'; changePassword: boolean };
export type GetFileQueryVariables = Exact<{
fileId: Scalars['ID']['input'];
}>;
@ -1250,6 +1263,49 @@ export const UserQueryWithTokenDocument = {
},
],
} as unknown as DocumentNode<UserQueryWithTokenQuery, UserQueryWithTokenQueryVariables>;
export const ChangePasswordDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'ChangePassword' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: { kind: 'Variable', name: { kind: 'Name', value: 'oldPassword' } },
type: { kind: 'NonNullType', type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } },
},
{
kind: 'VariableDefinition',
variable: { kind: 'Variable', name: { kind: 'Name', value: 'newPassword' } },
type: { kind: 'NonNullType', type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } },
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'changePassword' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'currentPassword' },
value: { kind: 'Variable', name: { kind: 'Name', value: 'oldPassword' } },
},
{
kind: 'Argument',
name: { kind: 'Name', value: 'newPassword' },
value: { kind: 'Variable', name: { kind: 'Name', value: 'newPassword' } },
},
],
},
],
},
},
],
} as unknown as DocumentNode<ChangePasswordMutation, ChangePasswordMutationVariables>;
export const GetFileDocument = {
kind: 'Document',
definitions: [

View File

@ -1,12 +1,11 @@
import { useContext } from "react";
import { ToastContext } from "./context";
import { useContext } from 'react';
import { ToastContext } from './context';
export const useToasts = () => {
const createToast = useContext(ToastContext);
if (!createToast) {
// todo: this should be an error, but it seems like it can be undefined.
// maybe due to concurrent rendering? idk shit about fuck.
return () => undefined;
if (typeof window === 'undefined') return () => {};
throw new Error('useToasts must be used within a ToastProvider');
}
return createToast;

View File

@ -39,11 +39,9 @@ export const LoginForm: FC = () => {
setLoginInfo(values);
setInvalidOTP(false);
await user.login(values);
console.log('ball');
setError(null);
redirect();
} catch (error: any) {
console.log(error);
if (user.otpRequired && error.message.toLowerCase().includes('invalid otp')) {
setInvalidOTP(true);
return;

View File

@ -0,0 +1,45 @@
import { Form, Formik } from 'formik';
import type { FC } from 'react';
import { Fragment } from 'react';
import * as Yup from 'yup';
import { Input } from '../components/input/input';
import { Submit } from '../components/input/submit';
interface PasswordData {
oldPassword: string;
newPassword: string;
}
interface PasswordFormProps {
onSubmit: (data: PasswordData) => Promise<void> | void;
}
const schema = Yup.object().shape({
oldPassword: Yup.string().required().min(5),
newPassword: Yup.string().required().min(5),
});
export const PasswordForm: FC<PasswordFormProps> = ({ onSubmit }) => {
return (
<Fragment>
<Formik
validationSchema={schema}
initialValues={{
oldPassword: '',
newPassword: '',
}}
onSubmit={async (values) => {
await onSubmit(values);
}}
>
<Form className="space-y-2">
<Input id="oldPassword" type="password" placeholder="Current Password" autoComplete="current-password" />
<Input id="newPassword" type="password" placeholder="New Password" autoComplete="new-password" />
<Submit className="mt-4 w-full" type="submit">
Change Password
</Submit>
</Form>
</Formik>
</Fragment>
);
};

View File

@ -1,10 +1,10 @@
import { useEffect, useState } from 'react';
import type { CombinedError, TypedDocumentNode } from '@urql/preact';
import { useMutation, useQuery } from '@urql/preact';
import { useEffect, useState } from 'react';
import { graphql } from '../@generated/gql';
import type { GetUserQuery, LoginMutationVariables } from '../@generated/graphql';
import { navigate, reload } from '../helpers/routing';
import { type RegularUserFragment } from '../@generated/graphql';
import { navigate } from '../helpers/routing';
import { useAsync } from './useAsync';
const RegularUserFragment = graphql(`
@ -42,15 +42,16 @@ export const useLoginUser = () => {
const [otp, setOtp] = useState(false);
const [, loginMutation] = useMutation(LoginMutation);
const [login] = useAsync(async (variables: LoginMutationVariables) => {
try {
await loginMutation(variables);
const result = await loginMutation(variables);
if (result.data) {
navigate('/dashboard');
} catch (error: any) {
if (error.message.toLowerCase().includes('otp')) {
} else if (result.error) {
if (result.error.message.toLowerCase().includes('otp')) {
setOtp(true);
return;
}
throw error;
throw result.error;
}
});
@ -64,7 +65,7 @@ export const useLogoutUser = () => {
const [, logoutMutation] = useMutation(LogoutMutation);
const [logout] = useAsync(async () => {
await logoutMutation({});
reload();
navigate('/');
});
return { logout };

View File

@ -1,6 +1,6 @@
import { useMutation, useQuery } from '@urql/preact';
import type { FC } from 'react';
import { Fragment } from 'react';
import { useMutation, useQuery } from '@urql/preact';
import { graphql } from '../../../@generated/gql';
import { Breadcrumbs } from '../../../components/breadcrumbs';
import { Button } from '../../../components/button';
@ -98,6 +98,32 @@ export const Page: FC = () => {
{!user.data && <InputSkeleton />}
</div>
</div>
<div className="grid grid-cols-2 gap-4 mt-8">
<div className="left col-span-full md:col-span-1">
{user.data && (
<Fragment>
<div className="font-bold text-xl">Change Password</div>
<p className="text-sm mt-2 text-gray-400">
Change your account password. This will not sign out other devices.
</p>
</Fragment>
)}
{!user.data && (
<Fragment>
<Skeleton className="w-1/2 mb-1" />
<Skeleton className="w-3/4" />
</Fragment>
)}
</div>
<div className="right flex items-center col-span-full md:col-span-1">
{user.data && (
<Button className="w-auto ml-auto" onClick={() => navigate(`/dashboard/preferences/change-password`)}>
Change
</Button>
)}
{!user.data && <ButtonSkeleton className="ml-auto" />}
</div>
</div>
<div className="mt-10">
<ConfigGenerator user={user.data?.user} />
</div>

View File

@ -0,0 +1,57 @@
import { useMutation } from '@urql/preact';
import type { FC } from 'react';
import { graphql } from '../../../../@generated';
import type { ChangePasswordMutationVariables } from '../../../../@generated/graphql';
import { Breadcrumbs } from '../../../../components/breadcrumbs';
import { Container } from '../../../../components/container';
import { Title } from '../../../../components/title';
import { useToasts } from '../../../../components/toast';
import { PasswordForm } from '../../../../containers/password-form';
import { navigate, prefetch } from '../../../../helpers/routing';
import { useAsync } from '../../../../hooks/useAsync';
import { useUser } from '../../../../hooks/useUser';
const ChangePassword = graphql(`
mutation ChangePassword($oldPassword: String!, $newPassword: String!) {
changePassword(currentPassword: $oldPassword, newPassword: $newPassword)
}
`);
export const Page: FC = () => {
const createToast = useToasts();
const [, changeInner] = useMutation(ChangePassword);
const [change] = useAsync(async (values: ChangePasswordMutationVariables) => {
prefetch('/dashboard/preferences');
const result = await changeInner(values);
if (result.data) {
createToast({ text: 'Your password has been changed.' });
navigate('/dashboard/preferences');
} else if (result.error) {
if (result.error.message.toLowerCase().includes('unauthorized')) {
createToast({ text: 'Invalid password.' });
return;
} else {
createToast({ text: 'An error occurred changing your password.' });
}
throw result.error;
}
});
useUser();
return (
<Container>
<Title>Change Password</Title>
<div className="flex justify-center top-[40vh]">
<div className="w-[80vw] md:w-[50vw] lg:w-[30vw]">
<Breadcrumbs href="/dashboard/preferences" className="mb-4">
Dashboard / Preferences
</Breadcrumbs>
<h1 className="my-5 text-4xl font-bold">Change Password</h1>
<PasswordForm onSubmit={change} />
</div>
</div>
</Container>
);
};

View File

@ -2,7 +2,6 @@ import { preact } from '@preact/preset-vite';
import { vavite } from 'vavite';
import ssr from 'vike/plugin';
import { defineConfig } from 'vite';
import eslint from 'vite-plugin-eslint';
import codegen from 'vite-plugin-graphql-codegen';
export default defineConfig({
@ -25,7 +24,6 @@ export default defineConfig({
},
plugins: [
codegen(),
eslint({ cache: true }),
preact(),
ssr({ disableAutoFullBuild: true }),
vavite({

View File

@ -353,10 +353,7 @@ importers:
version: 0.4.161(vite@5.1.1)
vite:
specifier: ^5.1.1
version: 5.1.1(@types/node@20.11.17)
vite-plugin-eslint:
specifier: ^1.8.1
version: 1.8.1(eslint@8.56.0)(vite@5.1.1)
version: 5.1.1
vite-plugin-graphql-codegen:
specifier: ^3.3.6
version: 3.3.6(@graphql-codegen/cli@5.0.2)(graphql@16.8.1)(vite@5.1.1)
@ -1389,7 +1386,7 @@ packages:
'@fastify/reply-from': 9.7.0
fast-querystring: 1.1.2
fastify-plugin: 4.5.1
ws: 8.16.0(utf-8-validate@6.0.3)
ws: 8.16.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@ -1783,7 +1780,7 @@ packages:
graphql-ws: 5.14.3(graphql@16.8.1)
isomorphic-ws: 5.0.0(ws@8.16.0)
tslib: 2.6.2
ws: 8.16.0(utf-8-validate@6.0.3)
ws: 8.16.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@ -1818,7 +1815,7 @@ packages:
graphql: 16.8.1
isomorphic-ws: 5.0.0(ws@8.16.0)
tslib: 2.6.2
ws: 8.16.0(utf-8-validate@6.0.3)
ws: 8.16.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@ -2044,7 +2041,7 @@ packages:
isomorphic-ws: 5.0.0(ws@8.16.0)
tslib: 2.6.2
value-or-promise: 1.0.12
ws: 8.16.0(utf-8-validate@6.0.3)
ws: 8.16.0
transitivePeerDependencies:
- '@types/node'
- bufferutil
@ -3099,7 +3096,7 @@ packages:
magic-string: 0.30.5
node-html-parser: 6.1.12
resolve: 1.22.8
vite: 5.1.1(@types/node@20.11.17)
vite: 5.1.1
transitivePeerDependencies:
- preact
- supports-color
@ -3133,7 +3130,7 @@ packages:
'@prefresh/utils': 1.2.0
'@rollup/pluginutils': 4.2.1
preact: 10.19.4
vite: 5.1.1(@types/node@20.11.17)
vite: 5.1.1
transitivePeerDependencies:
- supports-color
dev: true
@ -3848,13 +3845,6 @@ packages:
resolution: {integrity: sha512-kRiitIeUg1mPV9yH4VUJ/1uk2XjyANfeL8/7rH1tsjvHeO9PJLBHJIYsFWmAvmGj5u8rj+1TZx7PZzW2qLw3Lw==}
dev: true
/@types/eslint@8.56.2:
resolution: {integrity: sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==}
dependencies:
'@types/estree': 1.0.5
'@types/json-schema': 7.0.15
dev: true
/@types/estree-jsx@1.0.3:
resolution: {integrity: sha512-pvQ+TKeRHeiUGRhvYwRrQ/ISnohKkSJR14fT2yqyZ4e9K5vqc7hrtY2Y1Dw0ZwAzQ6DQsxsaCUuSIIi8v0Cq6w==}
dependencies:
@ -4251,7 +4241,7 @@ packages:
vite: ^2.8.1 || 3 || 4 || 5
dependencies:
'@types/node': 20.11.17
vite: 5.1.1(@types/node@20.11.17)
vite: 5.1.1
dev: true
/@vavite/expose-vite-dev-server@4.0.3(vite@5.1.1):
@ -4259,7 +4249,7 @@ packages:
peerDependencies:
vite: ^2.8.1 || 3 || 4 || 5
dependencies:
vite: 5.1.1(@types/node@20.11.17)
vite: 5.1.1
dev: true
/@vavite/multibuild-cli@4.0.3(vite@5.1.1):
@ -4272,7 +4262,7 @@ packages:
'@vavite/multibuild': 4.0.3(vite@5.1.1)
cac: 6.7.14
picocolors: 1.0.0
vite: 5.1.1(@types/node@20.11.17)
vite: 5.1.1
dev: true
/@vavite/multibuild@4.0.3(vite@5.1.1):
@ -4283,7 +4273,7 @@ packages:
'@types/node': 20.11.17
cac: 6.7.14
picocolors: 1.0.0
vite: 5.1.1(@types/node@20.11.17)
vite: 5.1.1
dev: true
/@vavite/node-loader@4.0.3(vite@5.1.1):
@ -4292,7 +4282,7 @@ packages:
peerDependencies:
vite: ^2.8.1 || 3 || 4 || 5
dependencies:
vite: 5.1.1(@types/node@20.11.17)
vite: 5.1.1
dev: true
/@vavite/reloader@4.0.3(vite@5.1.1):
@ -4300,7 +4290,7 @@ packages:
peerDependencies:
vite: ^2.8.1 || 3 || 4 || 5
dependencies:
vite: 5.1.1(@types/node@20.11.17)
vite: 5.1.1
dev: true
/@vitest/expect@1.2.2:
@ -7214,7 +7204,7 @@ packages:
peerDependencies:
ws: '*'
dependencies:
ws: 8.16.0(utf-8-validate@6.0.3)
ws: 8.16.0
dev: true
/istextorbinary@9.5.0:
@ -8356,6 +8346,7 @@ packages:
/node-gyp-build@4.8.0:
resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==}
hasBin: true
dev: false
/node-html-parser@6.1.12:
resolution: {integrity: sha512-/bT/Ncmv+fbMGX96XG9g05vFt43m/+SYKIs9oAemQVYyVcZmDAI2Xq/SbNcpOA35eF0Zk2av3Ksf+Xk8Vt8abA==}
@ -9509,14 +9500,6 @@ packages:
glob: 7.2.3
dev: true
/rollup@2.79.1:
resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==}
engines: {node: '>=10.0.0'}
hasBin: true
optionalDependencies:
fsevents: 2.3.3
dev: true
/rollup@4.10.0:
resolution: {integrity: sha512-t2v9G2AKxcQ8yrG+WGxctBes1AomT0M4ND7jTFBCVPXQ/WFTvNSefIrNSmLKhIKBrvN8SG+CZslimJcT3W2u2g==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@ -10808,6 +10791,7 @@ packages:
requiresBuild: true
dependencies:
node-gyp-build: 4.8.0
dev: false
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -10864,7 +10848,7 @@ packages:
'@vavite/reloader': 4.0.3(vite@5.1.1)
cac: 6.7.14
picocolors: 1.0.0
vite: 5.1.1(@types/node@20.11.17)
vite: 5.1.1
dev: true
/version-range@4.14.0:
@ -10910,7 +10894,7 @@ packages:
fast-glob: 3.3.2
sirv: 2.0.4
source-map-support: 0.5.21
vite: 5.1.1(@types/node@20.11.17)
vite: 5.1.1
dev: true
/vite-node@1.2.2(@types/node@20.11.17):
@ -10934,19 +10918,6 @@ packages:
- terser
dev: true
/vite-plugin-eslint@1.8.1(eslint@8.56.0)(vite@5.1.1):
resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==}
peerDependencies:
eslint: '>=7'
vite: '>=2'
dependencies:
'@rollup/pluginutils': 4.2.1
'@types/eslint': 8.56.2
eslint: 8.56.0
rollup: 2.79.1
vite: 5.1.1(@types/node@20.11.17)
dev: true
/vite-plugin-graphql-codegen@3.3.6(@graphql-codegen/cli@5.0.2)(graphql@16.8.1)(vite@5.1.1):
resolution: {integrity: sha512-TXMaUpPCfqzSpujjzFjVeeCH9JOSBwFWxOJottZ+gouQtNhnNpgXcj4nZep3om5Wq0UlDwDYLqXWrAa8XaZW1w==}
peerDependencies:
@ -10957,7 +10928,42 @@ packages:
'@graphql-codegen/cli': 5.0.2(@parcel/watcher@2.4.0)(graphql@16.8.1)(typescript@5.3.3)
'@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1)
graphql: 16.8.1
vite: 5.1.1(@types/node@20.11.17)
vite: 5.1.1
dev: true
/vite@5.1.1:
resolution: {integrity: sha512-wclpAgY3F1tR7t9LL5CcHC41YPkQIpKUGeIuT8MdNwNZr6OqOTLs7JX5vIHAtzqLWXts0T+GDrh9pN2arneKqg==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || >=20.0.0
less: '*'
lightningcss: ^1.21.0
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
dependencies:
esbuild: 0.19.12
postcss: 8.4.35
rollup: 4.10.0
optionalDependencies:
fsevents: 2.3.3
dev: true
/vite@5.1.1(@types/node@20.11.17):
@ -11224,6 +11230,19 @@ packages:
utf-8-validate: 6.0.3
dev: false
/ws@8.16.0:
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
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
dev: true
/ws@8.16.0(utf-8-validate@6.0.3):
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
engines: {node: '>=10.0.0'}
@ -11237,6 +11256,7 @@ packages:
optional: true
dependencies:
utf-8-validate: 6.0.3
dev: false
/xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}