feat: connect user register with backend api

add-signin-page
Meng Zhang 2023-12-05 22:46:20 +08:00
parent 20f7c107a9
commit b85585ad72
6 changed files with 239 additions and 59 deletions

View File

@ -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>
) )

View File

@ -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] ?? {}

View File

@ -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
>

View File

@ -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 };
}

View File

@ -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())
} }

View File

@ -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",