added error handling, fixed authenticaton, added toaster
This commit is contained in:
parent
40c35bda13
commit
b6b7cea210
18 changed files with 1327 additions and 68 deletions
|
|
@ -3,3 +3,4 @@ VITE_PORT=4000
|
||||||
ACCESS_TOKEN_SECRET=sometoken1
|
ACCESS_TOKEN_SECRET=sometoken1
|
||||||
REFRESH_TOKEN_SECRET=sometoken2
|
REFRESH_TOKEN_SECRET=sometoken2
|
||||||
DATABASE_URL="mysql://janedoe:mypassword@localhost:3306/mydb"
|
DATABASE_URL="mysql://janedoe:mypassword@localhost:3306/mydb"
|
||||||
|
MODE="dev"
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,4 @@
|
||||||
app/client/components/ui
|
app/client/components/ui
|
||||||
app/client/assets
|
app/client/assets
|
||||||
|
|
||||||
|
vite.config.ts.timestamp-*.mjs
|
||||||
|
|
|
||||||
134
app/client/components/ui/password-input.tsx
Normal file
134
app/client/components/ui/password-input.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { ButtonProps, GroupProps, InputProps, StackProps } from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
HStack,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
Stack,
|
||||||
|
mergeRefs,
|
||||||
|
useControllableState
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { LuEye, LuEyeOff } from 'react-icons/lu';
|
||||||
|
|
||||||
|
export interface PasswordVisibilityProps {
|
||||||
|
defaultVisible?: boolean;
|
||||||
|
visible?: boolean;
|
||||||
|
onVisibleChange?: (visible: boolean) => void;
|
||||||
|
visibilityIcon?: { on: React.ReactNode; off: React.ReactNode };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasswordInputProps extends InputProps, PasswordVisibilityProps {
|
||||||
|
rootProps?: GroupProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
||||||
|
function PasswordInput(props, ref) {
|
||||||
|
const {
|
||||||
|
rootProps,
|
||||||
|
defaultVisible,
|
||||||
|
visible: visibleProp,
|
||||||
|
onVisibleChange,
|
||||||
|
visibilityIcon = { on: <LuEye />, off: <LuEyeOff /> },
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [visible, setVisible] = useControllableState({
|
||||||
|
value: visibleProp,
|
||||||
|
defaultValue: defaultVisible || false,
|
||||||
|
onChange: onVisibleChange
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputGroup
|
||||||
|
endElement={
|
||||||
|
<VisibilityTrigger
|
||||||
|
disabled={rest.disabled}
|
||||||
|
onPointerDown={e => {
|
||||||
|
if (rest.disabled) return;
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
e.preventDefault();
|
||||||
|
setVisible(!visible);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{visible ? visibilityIcon.off : visibilityIcon.on}
|
||||||
|
</VisibilityTrigger>
|
||||||
|
}
|
||||||
|
{...rootProps}
|
||||||
|
>
|
||||||
|
<Input {...rest} ref={mergeRefs(ref, inputRef)} type={visible ? 'text' : 'password'} />
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const VisibilityTrigger = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
function VisibilityTrigger(props, ref) {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
tabIndex={-1}
|
||||||
|
ref={ref}
|
||||||
|
me="-2"
|
||||||
|
aspectRatio="square"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
height="calc(100% - {spacing.2})"
|
||||||
|
aria-label="Toggle password visibility"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
interface PasswordStrengthMeterProps extends StackProps {
|
||||||
|
max?: number;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PasswordStrengthMeter = React.forwardRef<HTMLDivElement, PasswordStrengthMeterProps>(
|
||||||
|
function PasswordStrengthMeter(props, ref) {
|
||||||
|
const { max = 4, value, ...rest } = props;
|
||||||
|
|
||||||
|
const percent = (value / max) * 100;
|
||||||
|
const { label, colorPalette } = getColorPalette(percent);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack align="flex-end" gap="1" ref={ref} {...rest}>
|
||||||
|
<HStack width="full" ref={ref} {...rest}>
|
||||||
|
{Array.from({ length: max }).map((_, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
height="1"
|
||||||
|
flex="1"
|
||||||
|
rounded="sm"
|
||||||
|
data-selected={index < value ? '' : undefined}
|
||||||
|
layerStyle="fill.subtle"
|
||||||
|
colorPalette="gray"
|
||||||
|
_selected={{
|
||||||
|
colorPalette,
|
||||||
|
layerStyle: 'fill.solid'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
{label && <HStack textStyle="xs">{label}</HStack>}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function getColorPalette(percent: number) {
|
||||||
|
switch (true) {
|
||||||
|
case percent < 33:
|
||||||
|
return { label: 'Low', colorPalette: 'red' };
|
||||||
|
case percent < 66:
|
||||||
|
return { label: 'Medium', colorPalette: 'orange' };
|
||||||
|
default:
|
||||||
|
return { label: 'High', colorPalette: 'green' };
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/client/components/ui/toaster.tsx
Normal file
39
app/client/components/ui/toaster.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Toaster as ChakraToaster,
|
||||||
|
Portal,
|
||||||
|
Spinner,
|
||||||
|
Stack,
|
||||||
|
Toast,
|
||||||
|
createToaster
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export const toaster = createToaster({
|
||||||
|
placement: 'bottom-end',
|
||||||
|
pauseOnPageIdle: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Toaster = () => {
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<ChakraToaster toaster={toaster} insetInline={{ mdDown: '4' }}>
|
||||||
|
{toast => (
|
||||||
|
<Toast.Root width={{ md: 'sm' }}>
|
||||||
|
{toast.type === 'loading' ? (
|
||||||
|
<Spinner size="sm" color="blue.solid" />
|
||||||
|
) : (
|
||||||
|
<Toast.Indicator />
|
||||||
|
)}
|
||||||
|
<Stack gap="1" flex="1" maxWidth="100%">
|
||||||
|
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
|
||||||
|
{toast.description && <Toast.Description>{toast.description}</Toast.Description>}
|
||||||
|
</Stack>
|
||||||
|
{toast.action && <Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>}
|
||||||
|
{toast.meta?.closable && <Toast.CloseTrigger />}
|
||||||
|
</Toast.Root>
|
||||||
|
)}
|
||||||
|
</ChakraToaster>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
};
|
||||||
14
app/client/fragments.tsx
Normal file
14
app/client/fragments.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const sessionFragment = gql`
|
||||||
|
fragment Session on Session {
|
||||||
|
accessToken
|
||||||
|
refreshToken {
|
||||||
|
token
|
||||||
|
maxAge
|
||||||
|
secure
|
||||||
|
httpOnly
|
||||||
|
sameSite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
38
app/client/hooks/useToastMutation.tsx
Normal file
38
app/client/hooks/useToastMutation.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import {
|
||||||
|
ApolloCache,
|
||||||
|
DefaultContext,
|
||||||
|
DocumentNode,
|
||||||
|
FetchResult,
|
||||||
|
MaybeMasked,
|
||||||
|
MutationFunctionOptions,
|
||||||
|
OperationVariables,
|
||||||
|
useMutation
|
||||||
|
} from '@apollo/client';
|
||||||
|
import { toaster } from '../components/ui/toaster';
|
||||||
|
|
||||||
|
interface UseToastMutationProps {
|
||||||
|
mutation: DocumentNode;
|
||||||
|
successMsg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseToastMutationReturn<T> {
|
||||||
|
update: (
|
||||||
|
options?:
|
||||||
|
| MutationFunctionOptions<T, OperationVariables, DefaultContext, ApolloCache<any>>
|
||||||
|
| undefined
|
||||||
|
) => Promise<FetchResult<MaybeMasked<T>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useToastMutation<T>(props: UseToastMutationProps): UseToastMutationReturn<T> {
|
||||||
|
const [update] = useMutation<T>(props.mutation, {
|
||||||
|
onCompleted: () => toaster.create({ description: props.successMsg, type: 'success' }),
|
||||||
|
onError: error => {
|
||||||
|
toaster.create({ description: `${error.message}`, type: 'error' });
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
update
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
|
||||||
import '@client/assets/styles/index.css';
|
import '@client/assets/styles/index.css';
|
||||||
import '@client/assets/styles/nerd-fonts-icons.css';
|
import '@client/assets/styles/nerd-fonts-icons.css';
|
||||||
import { Provider } from '@client/components/ui/provider';
|
import { Provider } from '@client/components/ui/provider';
|
||||||
|
import { Toaster } from '@client/components/ui/toaster';
|
||||||
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
@ -37,6 +38,7 @@ if (!rootElement.innerHTML) {
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
<Provider>
|
<Provider>
|
||||||
|
<Toaster />
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</Provider>
|
</Provider>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,24 @@
|
||||||
// import { gql, useQuery } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
import { Heading, Input, Stack } from '@chakra-ui/react';
|
import { Heading, Input, Stack } from '@chakra-ui/react';
|
||||||
import { Button } from '@client/components/ui/button';
|
import { Button } from '@client/components/ui/button';
|
||||||
import { Field } from '@client/components/ui/field';
|
import { Field } from '@client/components/ui/field';
|
||||||
import { valibotResolver } from '@hookform/resolvers/valibot';
|
import { valibotResolver } from '@hookform/resolvers/valibot';
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
import { Session } from 'inspector';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import * as v from 'valibot';
|
import * as v from 'valibot';
|
||||||
|
import { sessionFragment } from '../fragments';
|
||||||
|
import { useToastMutation } from '../hooks/useToastMutation';
|
||||||
|
|
||||||
export const Route = createFileRoute('/login')({
|
export const Route = createFileRoute('/login')({
|
||||||
component: LoginComponent
|
component: LoginComponent
|
||||||
});
|
});
|
||||||
|
|
||||||
function LoginComponent() {
|
function LoginComponent() {
|
||||||
// const { data } = useQuery(getBooksQuery);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack flexDirection={'column'} justifyContent={'center'} alignItems={'center'}>
|
<Stack flexDirection={'column'} justifyContent={'center'} alignItems={'center'}>
|
||||||
<Heading>Login</Heading>
|
<Heading>Login</Heading>
|
||||||
<Login />
|
<LoginForm />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -27,7 +28,12 @@ interface FormValues {
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Login() {
|
function LoginForm() {
|
||||||
|
const { update: login } = useToastMutation<Session>({
|
||||||
|
mutation,
|
||||||
|
successMsg: 'Logged in successfully.'
|
||||||
|
});
|
||||||
|
|
||||||
const schema = v.pipe(
|
const schema = v.pipe(
|
||||||
v.object({
|
v.object({
|
||||||
email: v.pipe(v.string(), v.nonEmpty('Email is required'), v.email('Invalid email')),
|
email: v.pipe(v.string(), v.nonEmpty('Email is required'), v.email('Invalid email')),
|
||||||
|
|
@ -42,7 +48,19 @@ function Login() {
|
||||||
} = useForm<FormValues>({ resolver: valibotResolver(schema) });
|
} = useForm<FormValues>({ resolver: valibotResolver(schema) });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(data => alert(data))}>
|
<form
|
||||||
|
onSubmit={handleSubmit(async values => {
|
||||||
|
const { data } = await login({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
email: values.email,
|
||||||
|
password: values.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
alert(JSON.stringify(data));
|
||||||
|
})}
|
||||||
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Field label="Email" invalid={errors.email != null} errorText={errors.email?.message}>
|
<Field label="Email" invalid={errors.email != null} errorText={errors.email?.message}>
|
||||||
<Input {...register('email')} />
|
<Input {...register('email')} />
|
||||||
|
|
@ -60,12 +78,12 @@ function Login() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* const getBooksQuery = gql`
|
const mutation = gql`
|
||||||
query BooksMany {
|
mutation AuthLogin($input: LoginInput!) {
|
||||||
booksMany {
|
login(input: $input) {
|
||||||
id
|
...Session
|
||||||
author
|
|
||||||
title
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`; */
|
|
||||||
|
${sessionFragment}
|
||||||
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
// import { gql, useQuery } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
import { Heading, Input, Stack } from '@chakra-ui/react';
|
import { Heading, Input, Stack } from '@chakra-ui/react';
|
||||||
import { Button } from '@client/components/ui/button';
|
import { Button } from '@client/components/ui/button';
|
||||||
import { Field } from '@client/components/ui/field';
|
import { Field } from '@client/components/ui/field';
|
||||||
|
import { Session } from '@generated/types';
|
||||||
import { valibotResolver } from '@hookform/resolvers/valibot';
|
import { valibotResolver } from '@hookform/resolvers/valibot';
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import * as v from 'valibot';
|
import * as v from 'valibot';
|
||||||
|
import { useToastMutation } from '../hooks/useToastMutation';
|
||||||
|
|
||||||
export const Route = createFileRoute('/register')({
|
export const Route = createFileRoute('/register')({
|
||||||
component: RegisterComponent
|
component: RegisterComponent
|
||||||
});
|
});
|
||||||
|
|
||||||
function RegisterComponent() {
|
function RegisterComponent() {
|
||||||
// const { data } = useQuery(getBooksQuery);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack flexDirection={'column'} justifyContent={'center'} alignItems={'center'}>
|
<Stack flexDirection={'column'} justifyContent={'center'} alignItems={'center'}>
|
||||||
<Heading>Sign up</Heading>
|
<Heading>Sign up</Heading>
|
||||||
<Register />
|
<SignUpForm />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -29,7 +29,9 @@ interface FormValues {
|
||||||
confirm_password: string;
|
confirm_password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Register() {
|
function SignUpForm() {
|
||||||
|
const { update: signUp } = useToastMutation<Session>({ mutation });
|
||||||
|
|
||||||
const schema = v.pipe(
|
const schema = v.pipe(
|
||||||
v.object({
|
v.object({
|
||||||
email: v.pipe(v.string(), v.nonEmpty('Email is required'), v.email('Invalid email')),
|
email: v.pipe(v.string(), v.nonEmpty('Email is required'), v.email('Invalid email')),
|
||||||
|
|
@ -53,7 +55,20 @@ function Register() {
|
||||||
formState: { errors }
|
formState: { errors }
|
||||||
} = useForm<FormValues>({ resolver: valibotResolver(schema) });
|
} = useForm<FormValues>({ resolver: valibotResolver(schema) });
|
||||||
|
|
||||||
const onSubmit = handleSubmit(data => alert(data));
|
const onSubmit = handleSubmit(async values => {
|
||||||
|
await signUp({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
email: values.email,
|
||||||
|
username: values.username,
|
||||||
|
password: values.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
redirect({
|
||||||
|
to: '/login'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
|
|
@ -88,12 +103,8 @@ function Register() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* const getBooksQuery = gql`
|
const mutation = gql`
|
||||||
query BooksMany {
|
mutation AuthSignUp($input: RegisterInput!) {
|
||||||
booksMany {
|
signUp(input: $input)
|
||||||
id
|
|
||||||
author
|
|
||||||
title
|
|
||||||
}
|
}
|
||||||
}
|
`;
|
||||||
`; */
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { BookOneResolver } from './resolvers/bookOneResolver';
|
||||||
|
|
||||||
/* Mutation */
|
/* Mutation */
|
||||||
import { LoginResolver } from './resolvers/auth/login';
|
import { LoginResolver } from './resolvers/auth/login';
|
||||||
|
import { SignUpResolver } from './resolvers/auth/signUp';
|
||||||
|
|
||||||
export const Query: QueryResolvers = {
|
export const Query: QueryResolvers = {
|
||||||
booksMany: BooksManyResolver,
|
booksMany: BooksManyResolver,
|
||||||
|
|
@ -14,7 +15,8 @@ export const Query: QueryResolvers = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Mutation = {
|
export const Mutation = {
|
||||||
login: LoginResolver
|
login: LoginResolver,
|
||||||
|
signUp: SignUpResolver
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Subscription = {};
|
export const Subscription = {};
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@ export async function LoginResolver(
|
||||||
{ input }: MutationLoginArgs,
|
{ input }: MutationLoginArgs,
|
||||||
{ identity }: IdentityContext
|
{ identity }: IdentityContext
|
||||||
): Promise<Session> {
|
): Promise<Session> {
|
||||||
const error = 'Invalid username or password';
|
const error = 'Invalid password or email';
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { email: input.email }
|
where: { email: input.email }
|
||||||
});
|
});
|
||||||
if (!user) throw new Error(error);
|
if (user == null) throw new Error(error);
|
||||||
|
|
||||||
const match = await bcrypt.compare(input.password, user.password);
|
const match = await bcrypt.compare(input.password, user.password);
|
||||||
if (!match) throw new Error(error);
|
if (!match) throw new Error(error);
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
import { IdentityContext } from '@app/server/identity';
|
import { IdentityContext } from '@app/server/identity';
|
||||||
import { prisma, uuid } from '@app/server/prisma/client';
|
import { prisma, uuid } from '@app/server/prisma/client';
|
||||||
import { RegisterInput } from '@generated/types';
|
import { MutationRegisterArgs } from '@generated/types';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
|
|
||||||
export async function SignUpResolver(
|
export async function SignUpResolver(
|
||||||
_: any,
|
_: any,
|
||||||
input: RegisterInput,
|
{ input }: MutationRegisterArgs,
|
||||||
{ identity }: IdentityContext
|
{ identity }: IdentityContext
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const existingUsername = await prisma.user.findUnique({
|
const existingUsername = await prisma.user.findUnique({
|
||||||
where: { username: input.username },
|
where: { username: input.username },
|
||||||
select: { id: true }
|
select: { id: true }
|
||||||
});
|
});
|
||||||
if (existingUsername != null) throw Error('User with this username already exists.');
|
if (existingUsername != null) throw new Error('User with this username already exists.');
|
||||||
|
|
||||||
const existingEmail = await prisma.user.findUnique({
|
const existingEmail = await prisma.user.findUnique({
|
||||||
where: { email: input.email },
|
where: { email: input.email },
|
||||||
select: { id: true }
|
select: { id: true }
|
||||||
});
|
});
|
||||||
if (existingEmail != null) throw Error('Email is already in use.');
|
if (existingEmail != null) throw new Error('Email is already in use.');
|
||||||
|
|
||||||
const hashedPassword = await bcrypt.hash(input.password, 10);
|
const hashedPassword = await bcrypt.hash(input.password, 10);
|
||||||
|
|
||||||
|
|
@ -7,7 +7,7 @@ type Query {
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
login(input: LoginInput!): Session!
|
login(input: LoginInput!): Session!
|
||||||
register(input: RegisterInput!): Void!
|
signUp(input: RegisterInput!): Void
|
||||||
}
|
}
|
||||||
|
|
||||||
input LoginInput {
|
input LoginInput {
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,14 @@ const resolvers = {
|
||||||
const apollo = new ApolloServer<IdentityContext>({
|
const apollo = new ApolloServer<IdentityContext>({
|
||||||
typeDefs,
|
typeDefs,
|
||||||
resolvers,
|
resolvers,
|
||||||
plugins: [ApolloServerPluginDrainHttpServer({ httpServer: server })]
|
plugins: [ApolloServerPluginDrainHttpServer({ httpServer: server })],
|
||||||
|
formatError: (formattedError, error) => {
|
||||||
|
process.stdout.write('\n```\n');
|
||||||
|
console.error(`${error}`);
|
||||||
|
process.stdout.write('\n```\n');
|
||||||
|
|
||||||
|
return formattedError;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await apollo.start();
|
await apollo.start();
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ model User {
|
||||||
username String @unique @db.VarChar(255)
|
username String @unique @db.VarChar(255)
|
||||||
password String
|
password String
|
||||||
|
|
||||||
refreshToken String?
|
refreshToken String? @db.VarChar(255)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
|
||||||
1044
package-lock.json
generated
1044
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -9,12 +9,14 @@
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"gql-types": "graphql-codegen --config codegen.ts",
|
"gql-types": "graphql-codegen --config codegen.ts",
|
||||||
"checkup": "opinionated checkup",
|
"checkup": "opinionated checkup",
|
||||||
"prisma": "prisma"
|
"prisma": "prisma",
|
||||||
|
"chakra": "chakra"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.12.2",
|
"@apollo/client": "^3.12.2",
|
||||||
"@apollo/server": "^4.11.2",
|
"@apollo/server": "^4.11.2",
|
||||||
"@chakra-ui/react": "^3.2.3",
|
"@chakra-ui/react": "^3.2.3",
|
||||||
|
"@chakra-ui/toast": "^7.0.2",
|
||||||
"@deftomat/opinionated": "^1.1.3",
|
"@deftomat/opinionated": "^1.1.3",
|
||||||
"@emotion/react": "^11.13.5",
|
"@emotion/react": "^11.13.5",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
|
|
@ -33,7 +35,7 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.5.0",
|
||||||
"tsx": "^4.3.0",
|
"tsx": "^4.3.0",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
|
|
@ -41,6 +43,7 @@
|
||||||
"vite-express": "^0.20.0"
|
"vite-express": "^0.20.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@chakra-ui/cli": "^3.15.0",
|
||||||
"@graphql-codegen/cli": "5.0.3",
|
"@graphql-codegen/cli": "5.0.3",
|
||||||
"@graphql-codegen/client-preset": "4.5.1",
|
"@graphql-codegen/client-preset": "4.5.1",
|
||||||
"@graphql-codegen/introspection": "4.0.3",
|
"@graphql-codegen/introspection": "4.0.3",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue