feat: connect user register with backend api
parent
20f7c107a9
commit
b85585ad72
|
|
@ -3,7 +3,7 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useForm } from "react-hook-form"
|
import { FieldError, FieldErrors, useForm, useFormContext, useFormState } from "react-hook-form"
|
||||||
import * as z from "zod"
|
import * as z from "zod"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
@ -11,15 +11,28 @@ import { IconSpinner } from "@/components/ui/icons"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
|
||||||
|
import { graphql } from "@/lib/gql/generates"
|
||||||
|
import { useGraphQLForm } from "@/lib/tabby-gql-client"
|
||||||
|
|
||||||
|
export const registerUser = graphql(/* GraphQL */ `
|
||||||
|
mutation register($email: String!, $password1: String!, $password2: String!, $invitationCode: String) {
|
||||||
|
register(
|
||||||
|
email: $email
|
||||||
|
password1: $password1
|
||||||
|
password2: $password2
|
||||||
|
invitationCode: $invitationCode
|
||||||
|
) {
|
||||||
|
accessToken
|
||||||
|
refreshToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z.string().email("Invalid email address"),
|
email: z.string().email("Invalid email address"),
|
||||||
password1: z.string(),
|
password1: z.string(),
|
||||||
password2: z.string(),
|
password2: z.string(),
|
||||||
invitationCode: z.string().optional(),
|
invitationCode: z.string().optional(),
|
||||||
}).refine((data) => data.password1 === data.password2, {
|
|
||||||
message: "Passwords don't match",
|
|
||||||
path: ["password2"],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
|
@ -30,18 +43,17 @@ export function UserAuthForm({ className, invitationCode, ...props }: UserAuthFo
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: "a@a.com",
|
||||||
password1: "",
|
password1: "a@a.comacom",
|
||||||
password2: "",
|
password2: "a@a.comacom",
|
||||||
invitationCode,
|
invitationCode,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const { isSubmitSuccessful, isSubmitting } = form.formState;
|
const { isSubmitting } = form.formState;
|
||||||
|
const { onSubmit } = useGraphQLForm(registerUser, {
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
onError: (path, message) => form.setError(path as any, { message })
|
||||||
await new Promise(resolve => setTimeout(resolve, 500000))
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("grid gap-6", className)} {...props}>
|
<div className={cn("grid gap-6", className)} {...props}>
|
||||||
|
|
@ -78,6 +90,7 @@ export function UserAuthForm({ className, invitationCode, ...props }: UserAuthFo
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" {...field} />
|
<Input type="password" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)} />
|
)} />
|
||||||
<FormField control={form.control} name="invitationCode" render={({ field }) => (
|
<FormField control={form.control} name="invitationCode" render={({ field }) => (
|
||||||
|
|
@ -91,9 +104,10 @@ export function UserAuthForm({ className, invitationCode, ...props }: UserAuthFo
|
||||||
{isSubmitting && (
|
{isSubmitting && (
|
||||||
<IconSpinner className="mr-2 h-4 w-4 animate-spin" />
|
<IconSpinner className="mr-2 h-4 w-4 animate-spin" />
|
||||||
)}
|
)}
|
||||||
Sign In
|
Register
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
<FormMessage className="text-center" />
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,14 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
|
||||||
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
||||||
*/
|
*/
|
||||||
const documents = {
|
const documents = {
|
||||||
|
'\n query GetIsAdminInitialized {\n isAdminInitialized\n }\n':
|
||||||
|
types.GetIsAdminInitializedDocument,
|
||||||
|
'\n mutation register($email: String!, $password1: String!, $password2: String!, $invitationCode: String) {\n register(\n email: $email\n password1: $password1\n password2: $password2\n invitationCode: $invitationCode\n ) {\n accessToken\n refreshToken\n }\n }\n':
|
||||||
|
types.RegisterDocument,
|
||||||
'\n query GetWorkers {\n workers {\n kind\n name\n addr\n device\n arch\n cpuInfo\n cpuCount\n cudaDevices\n }\n }\n':
|
'\n query GetWorkers {\n workers {\n kind\n name\n addr\n device\n arch\n cpuInfo\n cpuCount\n cudaDevices\n }\n }\n':
|
||||||
types.GetWorkersDocument,
|
types.GetWorkersDocument,
|
||||||
'\n query GetRegistrationToken {\n registrationToken\n }\n':
|
'\n query GetRegistrationToken {\n registrationToken\n }\n':
|
||||||
types.GetRegistrationTokenDocument,
|
types.GetRegistrationTokenDocument
|
||||||
'\n query GetIsAdminInitialized {\n isAdminInitialized\n }\n':
|
|
||||||
types.GetIsAdminInitializedDocument
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -35,6 +37,18 @@ const documents = {
|
||||||
*/
|
*/
|
||||||
export function graphql(source: string): unknown
|
export function graphql(source: string): unknown
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
|
*/
|
||||||
|
export function graphql(
|
||||||
|
source: '\n query GetIsAdminInitialized {\n isAdminInitialized\n }\n'
|
||||||
|
): (typeof documents)['\n query GetIsAdminInitialized {\n isAdminInitialized\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 register($email: String!, $password1: String!, $password2: String!, $invitationCode: String) {\n register(\n email: $email\n password1: $password1\n password2: $password2\n invitationCode: $invitationCode\n ) {\n accessToken\n refreshToken\n }\n }\n'
|
||||||
|
): (typeof documents)['\n mutation register($email: String!, $password1: String!, $password2: String!, $invitationCode: String) {\n register(\n email: $email\n password1: $password1\n password2: $password2\n invitationCode: $invitationCode\n ) {\n accessToken\n refreshToken\n }\n }\n']
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
|
@ -47,12 +61,6 @@ export function graphql(
|
||||||
export function graphql(
|
export function graphql(
|
||||||
source: '\n query GetRegistrationToken {\n registrationToken\n }\n'
|
source: '\n query GetRegistrationToken {\n registrationToken\n }\n'
|
||||||
): (typeof documents)['\n query GetRegistrationToken {\n registrationToken\n }\n']
|
): (typeof documents)['\n query GetRegistrationToken {\n registrationToken\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 query GetIsAdminInitialized {\n isAdminInitialized\n }\n'
|
|
||||||
): (typeof documents)['\n query GetIsAdminInitialized {\n isAdminInitialized\n }\n']
|
|
||||||
|
|
||||||
export function graphql(source: string) {
|
export function graphql(source: string) {
|
||||||
return (documents as any)[source] ?? {}
|
return (documents as any)[source] ?? {}
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,31 @@ export enum WorkerKind {
|
||||||
Completion = 'COMPLETION'
|
Completion = 'COMPLETION'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GetIsAdminInitializedQueryVariables = Exact<{
|
||||||
|
[key: string]: never
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type GetIsAdminInitializedQuery = {
|
||||||
|
__typename?: 'Query'
|
||||||
|
isAdminInitialized: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RegisterMutationVariables = Exact<{
|
||||||
|
email: Scalars['String']['input']
|
||||||
|
password1: Scalars['String']['input']
|
||||||
|
password2: Scalars['String']['input']
|
||||||
|
invitationCode?: InputMaybe<Scalars['String']['input']>
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type RegisterMutation = {
|
||||||
|
__typename?: 'Mutation'
|
||||||
|
register: {
|
||||||
|
__typename?: 'RegisterResponse'
|
||||||
|
accessToken: string
|
||||||
|
refreshToken: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type GetWorkersQueryVariables = Exact<{ [key: string]: never }>
|
export type GetWorkersQueryVariables = Exact<{ [key: string]: never }>
|
||||||
|
|
||||||
export type GetWorkersQuery = {
|
export type GetWorkersQuery = {
|
||||||
|
|
@ -151,15 +176,128 @@ export type GetRegistrationTokenQuery = {
|
||||||
registrationToken: string
|
registrationToken: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetIsAdminInitializedQueryVariables = Exact<{
|
export const GetIsAdminInitializedDocument = {
|
||||||
[key: string]: never
|
kind: 'Document',
|
||||||
}>
|
definitions: [
|
||||||
|
{
|
||||||
export type GetIsAdminInitializedQuery = {
|
kind: 'OperationDefinition',
|
||||||
__typename?: 'Query'
|
operation: 'query',
|
||||||
isAdminInitialized: boolean
|
name: { kind: 'Name', value: 'GetIsAdminInitialized' },
|
||||||
}
|
selectionSet: {
|
||||||
|
kind: 'SelectionSet',
|
||||||
|
selections: [
|
||||||
|
{ kind: 'Field', name: { kind: 'Name', value: 'isAdminInitialized' } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as unknown as DocumentNode<
|
||||||
|
GetIsAdminInitializedQuery,
|
||||||
|
GetIsAdminInitializedQueryVariables
|
||||||
|
>
|
||||||
|
export const RegisterDocument = {
|
||||||
|
kind: 'Document',
|
||||||
|
definitions: [
|
||||||
|
{
|
||||||
|
kind: 'OperationDefinition',
|
||||||
|
operation: 'mutation',
|
||||||
|
name: { kind: 'Name', value: 'register' },
|
||||||
|
variableDefinitions: [
|
||||||
|
{
|
||||||
|
kind: 'VariableDefinition',
|
||||||
|
variable: {
|
||||||
|
kind: 'Variable',
|
||||||
|
name: { kind: 'Name', value: 'email' }
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
kind: 'NonNullType',
|
||||||
|
type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'VariableDefinition',
|
||||||
|
variable: {
|
||||||
|
kind: 'Variable',
|
||||||
|
name: { kind: 'Name', value: 'password1' }
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
kind: 'NonNullType',
|
||||||
|
type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'VariableDefinition',
|
||||||
|
variable: {
|
||||||
|
kind: 'Variable',
|
||||||
|
name: { kind: 'Name', value: 'password2' }
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
kind: 'NonNullType',
|
||||||
|
type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'VariableDefinition',
|
||||||
|
variable: {
|
||||||
|
kind: 'Variable',
|
||||||
|
name: { kind: 'Name', value: 'invitationCode' }
|
||||||
|
},
|
||||||
|
type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
selectionSet: {
|
||||||
|
kind: 'SelectionSet',
|
||||||
|
selections: [
|
||||||
|
{
|
||||||
|
kind: 'Field',
|
||||||
|
name: { kind: 'Name', value: 'register' },
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
kind: 'Argument',
|
||||||
|
name: { kind: 'Name', value: 'email' },
|
||||||
|
value: {
|
||||||
|
kind: 'Variable',
|
||||||
|
name: { kind: 'Name', value: 'email' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'Argument',
|
||||||
|
name: { kind: 'Name', value: 'password1' },
|
||||||
|
value: {
|
||||||
|
kind: 'Variable',
|
||||||
|
name: { kind: 'Name', value: 'password1' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'Argument',
|
||||||
|
name: { kind: 'Name', value: 'password2' },
|
||||||
|
value: {
|
||||||
|
kind: 'Variable',
|
||||||
|
name: { kind: 'Name', value: 'password2' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'Argument',
|
||||||
|
name: { kind: 'Name', value: 'invitationCode' },
|
||||||
|
value: {
|
||||||
|
kind: 'Variable',
|
||||||
|
name: { kind: 'Name', value: 'invitationCode' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
selectionSet: {
|
||||||
|
kind: 'SelectionSet',
|
||||||
|
selections: [
|
||||||
|
{ kind: 'Field', name: { kind: 'Name', value: 'accessToken' } },
|
||||||
|
{ kind: 'Field', name: { kind: 'Name', value: 'refreshToken' } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as unknown as DocumentNode<RegisterMutation, RegisterMutationVariables>
|
||||||
export const GetWorkersDocument = {
|
export const GetWorkersDocument = {
|
||||||
kind: 'Document',
|
kind: 'Document',
|
||||||
definitions: [
|
definitions: [
|
||||||
|
|
@ -211,22 +349,3 @@ export const GetRegistrationTokenDocument = {
|
||||||
GetRegistrationTokenQuery,
|
GetRegistrationTokenQuery,
|
||||||
GetRegistrationTokenQueryVariables
|
GetRegistrationTokenQueryVariables
|
||||||
>
|
>
|
||||||
export const GetIsAdminInitializedDocument = {
|
|
||||||
kind: 'Document',
|
|
||||||
definitions: [
|
|
||||||
{
|
|
||||||
kind: 'OperationDefinition',
|
|
||||||
operation: 'query',
|
|
||||||
name: { kind: 'Name', value: 'GetIsAdminInitialized' },
|
|
||||||
selectionSet: {
|
|
||||||
kind: 'SelectionSet',
|
|
||||||
selections: [
|
|
||||||
{ kind: 'Field', name: { kind: 'Name', value: 'isAdminInitialized' } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
} as unknown as DocumentNode<
|
|
||||||
GetIsAdminInitializedQuery,
|
|
||||||
GetIsAdminInitializedQueryVariables
|
|
||||||
>
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,42 @@
|
||||||
import { GraphQLClient } from 'graphql-request'
|
import { GraphQLClient, Variables } from 'graphql-request'
|
||||||
|
import { TypedDocumentNode } from '@graphql-typed-document-node/core'
|
||||||
|
import { GraphQLResponse } from 'graphql-request/build/esm/types'
|
||||||
|
|
||||||
export const gqlClient = new GraphQLClient(
|
export const gqlClient = new GraphQLClient(
|
||||||
`${process.env.NEXT_PUBLIC_TABBY_SERVER_URL ?? ''}/graphql`
|
`${process.env.NEXT_PUBLIC_TABBY_SERVER_URL ?? ''}/graphql`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export interface ValidationError {
|
||||||
|
path: string,
|
||||||
|
message: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationErrors {
|
||||||
|
errors: Array<ValidationError>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGraphQLForm<TResult, TVariables extends Variables | undefined>(
|
||||||
|
document: TypedDocumentNode<TResult, TVariables>,
|
||||||
|
options?: {
|
||||||
|
onError?: (path: string, message: string) => void,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const onSubmit = async (values: TVariables) => {
|
||||||
|
try {
|
||||||
|
await gqlClient.request(document, values)
|
||||||
|
} catch (err) {
|
||||||
|
const { errors = [] } = (err as any).response as GraphQLResponse
|
||||||
|
for (const error of errors) {
|
||||||
|
if (error.extensions && error.extensions["validation-errors"]) {
|
||||||
|
const validationErrors = error.extensions["validation-errors"] as ValidationErrors
|
||||||
|
for (const error of validationErrors.errors) {
|
||||||
|
options?.onError && options?.onError(error.path, error.message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
options?.onError && options?.onError("root", error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { onSubmit };
|
||||||
|
}
|
||||||
|
|
@ -191,9 +191,11 @@ fn from_validation_errors<S: ScalarValue>(error: ValidationErrors) -> FieldError
|
||||||
obj.into()
|
obj.into()
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut ext = Object::with_capacity(2);
|
let mut error = Object::with_capacity(1);
|
||||||
ext.add_field("code", Value::scalar("validation-error".to_string()));
|
error.add_field("errors", Value::list(errors));
|
||||||
ext.add_field("errors", Value::list(errors));
|
|
||||||
|
let mut ext = Object::with_capacity(1);
|
||||||
|
ext.add_field("validation-errors", error.into());
|
||||||
|
|
||||||
FieldError::new("Invalid input parameters", ext.into())
|
FieldError::new("Invalid input parameters", ext.into())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,17 +38,17 @@ struct RegisterInput {
|
||||||
code = "password1",
|
code = "password1",
|
||||||
message = "Password must be at most 20 characters"
|
message = "Password must be at most 20 characters"
|
||||||
))]
|
))]
|
||||||
#[validate(must_match(
|
|
||||||
code = "password1",
|
|
||||||
message = "Passwords do not match",
|
|
||||||
other = "password2"
|
|
||||||
))]
|
|
||||||
password1: String,
|
password1: String,
|
||||||
#[validate(length(
|
#[validate(length(
|
||||||
min = 8,
|
min = 8,
|
||||||
code = "password2",
|
code = "password2",
|
||||||
message = "Password must be at least 8 characters"
|
message = "Password must be at least 8 characters"
|
||||||
))]
|
))]
|
||||||
|
#[validate(must_match(
|
||||||
|
code = "password2",
|
||||||
|
message = "Passwords do not match",
|
||||||
|
other = "password1"
|
||||||
|
))]
|
||||||
#[validate(length(
|
#[validate(length(
|
||||||
max = 20,
|
max = 20,
|
||||||
code = "password2",
|
code = "password2",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue