mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-27 02:52:22 +03:00
Signup Customisation [RFC] (#1395)
This commit is contained in:
parent
cb88dfc618
commit
99c9021f82
@ -2,6 +2,72 @@
|
|||||||
|
|
||||||
## 0.11.4
|
## 0.11.4
|
||||||
|
|
||||||
|
### 🎉 [New Feature] Signup Fields Customization
|
||||||
|
|
||||||
|
We added an API for extending the default signup form with custom fields. This allows you to add fields like `age`, `address`, etc. to your signup form.
|
||||||
|
|
||||||
|
You first need to define the `auth.signup.additionalFields` property in your `.wasp` file:
|
||||||
|
```wasp
|
||||||
|
app crudTesting {
|
||||||
|
// ...
|
||||||
|
auth: {
|
||||||
|
userEntity: User,
|
||||||
|
methods: {
|
||||||
|
usernameAndPassword: {},
|
||||||
|
},
|
||||||
|
onAuthFailedRedirectTo: "/login",
|
||||||
|
signup: {
|
||||||
|
additionalFields: import { fields } from "@server/auth.js",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, you need to define the `fields` object in your `auth.js` file:
|
||||||
|
```js
|
||||||
|
import { defineAdditionalSignupFields } from '@wasp/auth/index.js'
|
||||||
|
|
||||||
|
export const fields = defineAdditionalSignupFields({
|
||||||
|
address: (data) => {
|
||||||
|
// Validate the address field
|
||||||
|
if (typeof data.address !== 'string') {
|
||||||
|
throw new Error('Address is required.')
|
||||||
|
}
|
||||||
|
if (data.address.length < 10) {
|
||||||
|
throw new Error('Address must be at least 10 characters long.')
|
||||||
|
}
|
||||||
|
// Return the address field
|
||||||
|
return data.address
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, you can extend the `SignupForm` component on the client:
|
||||||
|
```jsx
|
||||||
|
import { SignupForm } from "@wasp/auth/forms/Signup";
|
||||||
|
|
||||||
|
export const SignupPage = () => {
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<main>
|
||||||
|
<h1>Signup</h1>
|
||||||
|
<SignupForm
|
||||||
|
additionalFields={[
|
||||||
|
{
|
||||||
|
name: "address",
|
||||||
|
label: "Address",
|
||||||
|
type: "input",
|
||||||
|
validations: {
|
||||||
|
required: "Address is required",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
### 🎉 [New Feature] Support for PostgreSQL Extensions
|
### 🎉 [New Feature] Support for PostgreSQL Extensions
|
||||||
|
|
||||||
Wasp now supports PostgreSQL extensions! You can enable them in your `main.wasp` file:
|
Wasp now supports PostgreSQL extensions! You can enable them in your `main.wasp` file:
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
type State,
|
type State,
|
||||||
type CustomizationOptions,
|
type CustomizationOptions,
|
||||||
type ErrorMessage,
|
type ErrorMessage,
|
||||||
|
type AdditionalSignupFields,
|
||||||
} from './types'
|
} from './types'
|
||||||
import { LoginSignupForm } from './internal/common/LoginSignupForm'
|
import { LoginSignupForm } from './internal/common/LoginSignupForm'
|
||||||
import { MessageError, MessageSuccess } from './internal/Message'
|
import { MessageError, MessageSuccess } from './internal/Message'
|
||||||
@ -39,9 +40,11 @@ export const AuthContext = createContext({
|
|||||||
setSuccessMessage: (successMessage: string | null) => {},
|
setSuccessMessage: (successMessage: string | null) => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
function Auth ({ state, appearance, logo, socialLayout = 'horizontal' }: {
|
function Auth ({ state, appearance, logo, socialLayout = 'horizontal', additionalSignupFields }: {
|
||||||
state: State;
|
state: State;
|
||||||
} & CustomizationOptions) {
|
} & CustomizationOptions & {
|
||||||
|
additionalSignupFields?: AdditionalSignupFields;
|
||||||
|
}) {
|
||||||
const [errorMessage, setErrorMessage] = useState<ErrorMessage | null>(null);
|
const [errorMessage, setErrorMessage] = useState<ErrorMessage | null>(null);
|
||||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@ -82,6 +85,7 @@ function Auth ({ state, appearance, logo, socialLayout = 'horizontal' }: {
|
|||||||
<LoginSignupForm
|
<LoginSignupForm
|
||||||
state={state}
|
state={state}
|
||||||
socialButtonsDirection={socialButtonsDirection}
|
socialButtonsDirection={socialButtonsDirection}
|
||||||
|
additionalSignupFields={additionalSignupFields}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{=# isEmailAuthEnabled =}
|
{=# isEmailAuthEnabled =}
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
import Auth from './Auth'
|
import Auth from './Auth'
|
||||||
import { type CustomizationOptions, State } from './types'
|
import {
|
||||||
|
type CustomizationOptions,
|
||||||
|
type AdditionalSignupFields,
|
||||||
|
State,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
export function SignupForm({
|
export function SignupForm({
|
||||||
appearance,
|
appearance,
|
||||||
logo,
|
logo,
|
||||||
socialLayout,
|
socialLayout,
|
||||||
}: CustomizationOptions) {
|
additionalFields,
|
||||||
|
}: CustomizationOptions & { additionalFields?: AdditionalSignupFields; }) {
|
||||||
return (
|
return (
|
||||||
<Auth
|
<Auth
|
||||||
appearance={appearance}
|
appearance={appearance}
|
||||||
logo={logo}
|
logo={logo}
|
||||||
socialLayout={socialLayout}
|
socialLayout={socialLayout}
|
||||||
state={State.Signup}
|
state={State.Signup}
|
||||||
|
additionalSignupFields={additionalFields}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,10 @@ export const FormLabel = styled('label', {
|
|||||||
display: 'block',
|
display: 'block',
|
||||||
fontSize: '$sm',
|
fontSize: '$sm',
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
|
marginBottom: '0.5rem',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const FormInput = styled('input', {
|
const commonInputStyles = {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
lineHeight: '1.5rem',
|
lineHeight: '1.5rem',
|
||||||
fontSize: '$sm',
|
fontSize: '$sm',
|
||||||
@ -44,7 +45,18 @@ export const FormInput = styled('input', {
|
|||||||
paddingBottom: '0.375rem',
|
paddingBottom: '0.375rem',
|
||||||
paddingLeft: '0.75rem',
|
paddingLeft: '0.75rem',
|
||||||
paddingRight: '0.75rem',
|
paddingRight: '0.75rem',
|
||||||
|
margin: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormInput = styled('input', commonInputStyles)
|
||||||
|
|
||||||
|
export const FormTextarea = styled('textarea', commonInputStyles)
|
||||||
|
|
||||||
|
export const FormError = styled('div', {
|
||||||
|
display: 'block',
|
||||||
|
fontSize: '$sm',
|
||||||
|
fontWeight: '500',
|
||||||
|
color: '$formErrorText',
|
||||||
marginTop: '0.5rem',
|
marginTop: '0.5rem',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,10 +1,23 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import { useContext, type FormEvent } from 'react'
|
import { useContext } from 'react'
|
||||||
import { styled } from '../../../../stitches.config'
|
import { useForm, UseFormReturn } from 'react-hook-form'
|
||||||
import config from '../../../../config.js'
|
|
||||||
|
|
||||||
import { AuthContext } from '../../Auth'
|
import { AuthContext } from '../../Auth'
|
||||||
import { Form, FormInput, FormItemGroup, FormLabel, SubmitButton } from '../Form'
|
import {
|
||||||
|
Form,
|
||||||
|
FormInput,
|
||||||
|
FormItemGroup,
|
||||||
|
FormLabel,
|
||||||
|
FormError,
|
||||||
|
FormTextarea,
|
||||||
|
SubmitButton,
|
||||||
|
} from '../Form'
|
||||||
|
import type {
|
||||||
|
AdditionalSignupFields,
|
||||||
|
AdditionalSignupField,
|
||||||
|
AdditionalSignupFieldRenderFn,
|
||||||
|
FormState,
|
||||||
|
} from '../../types'
|
||||||
{=# isSocialAuthEnabled =}
|
{=# isSocialAuthEnabled =}
|
||||||
import * as SocialIcons from '../social/SocialIcons'
|
import * as SocialIcons from '../social/SocialIcons'
|
||||||
import { SocialButton } from '../social/SocialButton'
|
import { SocialButton } from '../social/SocialButton'
|
||||||
@ -97,12 +110,23 @@ const googleSignInUrl = `${config.apiUrl}{= googleSignInPath =}`
|
|||||||
const gitHubSignInUrl = `${config.apiUrl}{= gitHubSignInPath =}`
|
const gitHubSignInUrl = `${config.apiUrl}{= gitHubSignInPath =}`
|
||||||
{=/ isGitHubAuthEnabled =}
|
{=/ isGitHubAuthEnabled =}
|
||||||
|
|
||||||
|
{=!
|
||||||
|
// Since we allow users to add additional fields to the signup form, we don't
|
||||||
|
// know the exact shape of the form values. We are assuming that the form values
|
||||||
|
// will be a flat object with string values.
|
||||||
|
=}
|
||||||
|
export type LoginSignupFormFields = {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const LoginSignupForm = ({
|
export const LoginSignupForm = ({
|
||||||
state,
|
state,
|
||||||
socialButtonsDirection = 'horizontal',
|
socialButtonsDirection = 'horizontal',
|
||||||
|
additionalSignupFields,
|
||||||
}: {
|
}: {
|
||||||
state: 'login' | 'signup',
|
state: 'login' | 'signup'
|
||||||
socialButtonsDirection?: 'horizontal' | 'vertical';
|
socialButtonsDirection?: 'horizontal' | 'vertical'
|
||||||
|
additionalSignupFields?: AdditionalSignupFields
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
isLoading,
|
isLoading,
|
||||||
@ -110,16 +134,19 @@ export const LoginSignupForm = ({
|
|||||||
setSuccessMessage,
|
setSuccessMessage,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
} = useContext(AuthContext)
|
} = useContext(AuthContext)
|
||||||
const cta = state === 'login' ? 'Log in' : 'Sign up';
|
const isLogin = state === 'login'
|
||||||
|
const cta = isLogin ? 'Log in' : 'Sign up';
|
||||||
{=# isAnyPasswordBasedAuthEnabled =}
|
{=# isAnyPasswordBasedAuthEnabled =}
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const onErrorHandler = (error) => {
|
const onErrorHandler = (error) => {
|
||||||
setErrorMessage({ title: error.message, description: error.data?.data?.message })
|
setErrorMessage({ title: error.message, description: error.data?.data?.message })
|
||||||
};
|
};
|
||||||
{=/ isAnyPasswordBasedAuthEnabled =}
|
{=/ isAnyPasswordBasedAuthEnabled =}
|
||||||
|
const hookForm = useForm<LoginSignupFormFields>()
|
||||||
|
const { register, formState: { errors }, handleSubmit: hookFormHandleSubmit } = hookForm
|
||||||
{=# isUsernameAndPasswordAuthEnabled =}
|
{=# isUsernameAndPasswordAuthEnabled =}
|
||||||
const { handleSubmit, usernameFieldVal, passwordFieldVal, setUsernameFieldVal, setPasswordFieldVal } = useUsernameAndPassword({
|
const { handleSubmit } = useUsernameAndPassword({
|
||||||
isLogin: state === 'login',
|
isLogin,
|
||||||
onError: onErrorHandler,
|
onError: onErrorHandler,
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
history.push('{= onAuthSucceededRedirectTo =}')
|
history.push('{= onAuthSucceededRedirectTo =}')
|
||||||
@ -127,10 +154,11 @@ export const LoginSignupForm = ({
|
|||||||
});
|
});
|
||||||
{=/ isUsernameAndPasswordAuthEnabled =}
|
{=/ isUsernameAndPasswordAuthEnabled =}
|
||||||
{=# isEmailAuthEnabled =}
|
{=# isEmailAuthEnabled =}
|
||||||
const { handleSubmit, emailFieldVal, passwordFieldVal, setEmailFieldVal, setPasswordFieldVal } = useEmail({
|
const { handleSubmit } = useEmail({
|
||||||
isLogin: state === 'login',
|
isLogin,
|
||||||
onError: onErrorHandler,
|
onError: onErrorHandler,
|
||||||
showEmailVerificationPending() {
|
showEmailVerificationPending() {
|
||||||
|
hookForm.reset()
|
||||||
setSuccessMessage(`You've signed up successfully! Check your email for the confirmation link.`)
|
setSuccessMessage(`You've signed up successfully! Check your email for the confirmation link.`)
|
||||||
},
|
},
|
||||||
onLoginSuccess() {
|
onLoginSuccess() {
|
||||||
@ -145,13 +173,12 @@ export const LoginSignupForm = ({
|
|||||||
});
|
});
|
||||||
{=/ isEmailAuthEnabled =}
|
{=/ isEmailAuthEnabled =}
|
||||||
{=# isAnyPasswordBasedAuthEnabled =}
|
{=# isAnyPasswordBasedAuthEnabled =}
|
||||||
async function onSubmit (event: FormEvent<HTMLFormElement>) {
|
async function onSubmit (data) {
|
||||||
event.preventDefault();
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
setSuccessMessage(null);
|
setSuccessMessage(null);
|
||||||
try {
|
try {
|
||||||
await handleSubmit();
|
await handleSubmit(data);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@ -184,41 +211,49 @@ export const LoginSignupForm = ({
|
|||||||
</OrContinueWith>
|
</OrContinueWith>
|
||||||
{=/ areBothSocialAndPasswordBasedAuthEnabled =}
|
{=/ areBothSocialAndPasswordBasedAuthEnabled =}
|
||||||
{=# isAnyPasswordBasedAuthEnabled =}
|
{=# isAnyPasswordBasedAuthEnabled =}
|
||||||
<Form onSubmit={onSubmit}>
|
<Form onSubmit={hookFormHandleSubmit(onSubmit)}>
|
||||||
{=# isUsernameAndPasswordAuthEnabled =}
|
{=# isUsernameAndPasswordAuthEnabled =}
|
||||||
<FormItemGroup>
|
<FormItemGroup>
|
||||||
<FormLabel>Username</FormLabel>
|
<FormLabel>Username</FormLabel>
|
||||||
<FormInput
|
<FormInput
|
||||||
|
{...register('username', {
|
||||||
|
required: 'Username is required',
|
||||||
|
})}
|
||||||
type="text"
|
type="text"
|
||||||
required
|
|
||||||
value={usernameFieldVal}
|
|
||||||
onChange={e => setUsernameFieldVal(e.target.value)}
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||||
</FormItemGroup>
|
</FormItemGroup>
|
||||||
{=/ isUsernameAndPasswordAuthEnabled =}
|
{=/ isUsernameAndPasswordAuthEnabled =}
|
||||||
{=# isEmailAuthEnabled =}
|
{=# isEmailAuthEnabled =}
|
||||||
<FormItemGroup>
|
<FormItemGroup>
|
||||||
<FormLabel>E-mail</FormLabel>
|
<FormLabel>E-mail</FormLabel>
|
||||||
<FormInput
|
<FormInput
|
||||||
|
{...register('email', {
|
||||||
|
required: 'Email is required',
|
||||||
|
})}
|
||||||
type="email"
|
type="email"
|
||||||
required
|
|
||||||
value={emailFieldVal}
|
|
||||||
onChange={e => setEmailFieldVal(e.target.value)}
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
{errors.email && <FormError>{errors.email.message}</FormError>}
|
||||||
</FormItemGroup>
|
</FormItemGroup>
|
||||||
{=/ isEmailAuthEnabled =}
|
{=/ isEmailAuthEnabled =}
|
||||||
<FormItemGroup>
|
<FormItemGroup>
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>Password</FormLabel>
|
||||||
<FormInput
|
<FormInput
|
||||||
|
{...register('password', {
|
||||||
|
required: 'Password is required',
|
||||||
|
})}
|
||||||
type="password"
|
type="password"
|
||||||
required
|
|
||||||
value={passwordFieldVal}
|
|
||||||
onChange={e => setPasswordFieldVal(e.target.value)}
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
{errors.password && <FormError>{errors.password.message}</FormError>}
|
||||||
</FormItemGroup>
|
</FormItemGroup>
|
||||||
|
<AdditionalFormFields
|
||||||
|
hookForm={hookForm}
|
||||||
|
formState={{ isLoading }}
|
||||||
|
additionalSignupFields={additionalSignupFields}
|
||||||
|
/>
|
||||||
<FormItemGroup>
|
<FormItemGroup>
|
||||||
<SubmitButton type="submit" disabled={isLoading}>{cta}</SubmitButton>
|
<SubmitButton type="submit" disabled={isLoading}>{cta}</SubmitButton>
|
||||||
</FormItemGroup>
|
</FormItemGroup>
|
||||||
@ -226,3 +261,76 @@ export const LoginSignupForm = ({
|
|||||||
{=/ isAnyPasswordBasedAuthEnabled =}
|
{=/ isAnyPasswordBasedAuthEnabled =}
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AdditionalFormFields({
|
||||||
|
hookForm,
|
||||||
|
formState: { isLoading },
|
||||||
|
additionalSignupFields,
|
||||||
|
}: {
|
||||||
|
hookForm: UseFormReturn<LoginSignupFormFields>;
|
||||||
|
formState: FormState;
|
||||||
|
additionalSignupFields: AdditionalSignupFields;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
} = hookForm;
|
||||||
|
|
||||||
|
function renderField<ComponentType extends React.JSXElementConstructor<any>>(
|
||||||
|
field: AdditionalSignupField,
|
||||||
|
// Ideally we would use ComponentType here, but it doesn't work with react-hook-form
|
||||||
|
Component: any,
|
||||||
|
props?: React.ComponentProps<ComponentType>
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<FormItemGroup key={field.name}>
|
||||||
|
<FormLabel>{field.label}</FormLabel>
|
||||||
|
<Component
|
||||||
|
{...register(field.name, field.validations)}
|
||||||
|
{...props}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
{errors[field.name] && (
|
||||||
|
<FormError>{errors[field.name].message}</FormError>
|
||||||
|
)}
|
||||||
|
</FormItemGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areAdditionalFieldsRenderFn(additionalSignupFields)) {
|
||||||
|
return additionalSignupFields(hookForm, { isLoading })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
additionalSignupFields &&
|
||||||
|
additionalSignupFields.map((field) => {
|
||||||
|
if (isFieldRenderFn(field)) {
|
||||||
|
return field(hookForm, { isLoading })
|
||||||
|
}
|
||||||
|
switch (field.type) {
|
||||||
|
case 'input':
|
||||||
|
return renderField<typeof FormInput>(field, FormInput, {
|
||||||
|
type: 'text',
|
||||||
|
})
|
||||||
|
case 'textarea':
|
||||||
|
return renderField<typeof FormTextarea>(field, FormTextarea)
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported additional signup field type: ${field.type}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFieldRenderFn(
|
||||||
|
additionalSignupField: AdditionalSignupField | AdditionalSignupFieldRenderFn
|
||||||
|
): additionalSignupField is AdditionalSignupFieldRenderFn {
|
||||||
|
return typeof additionalSignupField === 'function'
|
||||||
|
}
|
||||||
|
|
||||||
|
function areAdditionalFieldsRenderFn(
|
||||||
|
additionalSignupFields: AdditionalSignupFields
|
||||||
|
): additionalSignupFields is AdditionalSignupFieldRenderFn {
|
||||||
|
return typeof additionalSignupFields === 'function'
|
||||||
|
}
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
|
import { useContext } from 'react'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
import { requestPasswordReset } from '../../../email/actions/passwordReset.js'
|
import { requestPasswordReset } from '../../../email/actions/passwordReset.js'
|
||||||
import { useState, useContext, FormEvent } from 'react'
|
import { Form, FormItemGroup, FormLabel, FormInput, SubmitButton, FormError } from '../Form'
|
||||||
import { Form, FormItemGroup, FormLabel, FormInput, SubmitButton } from '../Form'
|
|
||||||
import { AuthContext } from '../../Auth'
|
import { AuthContext } from '../../Auth'
|
||||||
|
|
||||||
export const ForgotPasswordForm = () => {
|
export const ForgotPasswordForm = () => {
|
||||||
|
const { register, handleSubmit, reset, formState: { errors } } = useForm<{ email: string }>()
|
||||||
const { isLoading, setErrorMessage, setSuccessMessage, setIsLoading } = useContext(AuthContext)
|
const { isLoading, setErrorMessage, setSuccessMessage, setIsLoading } = useContext(AuthContext)
|
||||||
const [email, setEmail] = useState('')
|
|
||||||
|
|
||||||
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
const onSubmit = async (data) => {
|
||||||
event.preventDefault()
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setErrorMessage(null)
|
setErrorMessage(null)
|
||||||
setSuccessMessage(null)
|
setSuccessMessage(null)
|
||||||
try {
|
try {
|
||||||
await requestPasswordReset({ email })
|
await requestPasswordReset(data)
|
||||||
|
reset()
|
||||||
setSuccessMessage('Check your email for a password reset link.')
|
setSuccessMessage('Check your email for a password reset link.')
|
||||||
setEmail('')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorMessage({
|
setErrorMessage({
|
||||||
title: error.message,
|
title: error.message,
|
||||||
@ -28,16 +28,17 @@ export const ForgotPasswordForm = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form onSubmit={onSubmit}>
|
<Form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<FormItemGroup>
|
<FormItemGroup>
|
||||||
<FormLabel>E-mail</FormLabel>
|
<FormLabel>E-mail</FormLabel>
|
||||||
<FormInput
|
<FormInput
|
||||||
|
{...register('email', {
|
||||||
|
required: 'Email is required',
|
||||||
|
})}
|
||||||
type="email"
|
type="email"
|
||||||
required
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
{errors.email && <FormError>{errors.email.message}</FormError>}
|
||||||
</FormItemGroup>
|
</FormItemGroup>
|
||||||
<FormItemGroup>
|
<FormItemGroup>
|
||||||
<SubmitButton type="submit" disabled={isLoading}>
|
<SubmitButton type="submit" disabled={isLoading}>
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
|
import { useContext } from 'react'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
import { resetPassword } from '../../../email/actions/passwordReset.js'
|
import { resetPassword } from '../../../email/actions/passwordReset.js'
|
||||||
import { useState, useContext, FormEvent } from 'react'
|
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { Form, FormItemGroup, FormLabel, FormInput, SubmitButton } from '../Form'
|
import { Form, FormItemGroup, FormLabel, FormInput, SubmitButton, FormError } from '../Form'
|
||||||
import { AuthContext } from '../../Auth'
|
import { AuthContext } from '../../Auth'
|
||||||
|
|
||||||
export const ResetPasswordForm = () => {
|
export const ResetPasswordForm = () => {
|
||||||
|
const { register, handleSubmit, reset, formState: { errors } } = useForm<{ password: string; passwordConfirmation: string }>()
|
||||||
const { isLoading, setErrorMessage, setSuccessMessage, setIsLoading } = useContext(AuthContext)
|
const { isLoading, setErrorMessage, setSuccessMessage, setIsLoading } = useContext(AuthContext)
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const token = new URLSearchParams(location.search).get('token')
|
const token = new URLSearchParams(location.search).get('token')
|
||||||
const [password, setPassword] = useState('')
|
const onSubmit = async (data) => {
|
||||||
const [passwordConfirmation, setPasswordConfirmation] = useState('')
|
|
||||||
|
|
||||||
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
setErrorMessage({
|
setErrorMessage({
|
||||||
title:
|
title:
|
||||||
@ -22,7 +19,7 @@ export const ResetPasswordForm = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!password || password !== passwordConfirmation) {
|
if (!data.password || data.password !== data.passwordConfirmation) {
|
||||||
setErrorMessage({ title: `Passwords don't match!` })
|
setErrorMessage({ title: `Passwords don't match!` })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -31,10 +28,9 @@ export const ResetPasswordForm = () => {
|
|||||||
setErrorMessage(null)
|
setErrorMessage(null)
|
||||||
setSuccessMessage(null)
|
setSuccessMessage(null)
|
||||||
try {
|
try {
|
||||||
await resetPassword({ password, token })
|
await resetPassword({ password: data.password, token })
|
||||||
|
reset()
|
||||||
setSuccessMessage('Your password has been reset.')
|
setSuccessMessage('Your password has been reset.')
|
||||||
setPassword('')
|
|
||||||
setPasswordConfirmation('')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorMessage({
|
setErrorMessage({
|
||||||
title: error.message,
|
title: error.message,
|
||||||
@ -47,26 +43,32 @@ export const ResetPasswordForm = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form onSubmit={onSubmit}>
|
<Form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<FormItemGroup>
|
<FormItemGroup>
|
||||||
<FormLabel>New password</FormLabel>
|
<FormLabel>New password</FormLabel>
|
||||||
<FormInput
|
<FormInput
|
||||||
|
{...register('password', {
|
||||||
|
required: 'Password is required',
|
||||||
|
})}
|
||||||
type="password"
|
type="password"
|
||||||
required
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
{errors.passwordConfirmation && (
|
||||||
|
<FormError>{errors.passwordConfirmation.message}</FormError>
|
||||||
|
)}
|
||||||
</FormItemGroup>
|
</FormItemGroup>
|
||||||
<FormItemGroup>
|
<FormItemGroup>
|
||||||
<FormLabel>Confirm new password</FormLabel>
|
<FormLabel>Confirm new password</FormLabel>
|
||||||
<FormInput
|
<FormInput
|
||||||
|
{...register('passwordConfirmation', {
|
||||||
|
required: 'Password confirmation is required',
|
||||||
|
})}
|
||||||
type="password"
|
type="password"
|
||||||
required
|
|
||||||
value={passwordConfirmation}
|
|
||||||
onChange={(e) => setPasswordConfirmation(e.target.value)}
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
{errors.passwordConfirmation && (
|
||||||
|
<FormError>{errors.passwordConfirmation.message}</FormError>
|
||||||
|
)}
|
||||||
</FormItemGroup>
|
</FormItemGroup>
|
||||||
<FormItemGroup>
|
<FormItemGroup>
|
||||||
<SubmitButton type="submit" disabled={isLoading}>
|
<SubmitButton type="submit" disabled={isLoading}>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import { signup } from '../../../email/actions/signup'
|
import { signup } from '../../../email/actions/signup'
|
||||||
import { login } from '../../../email/actions/login'
|
import { login } from '../../../email/actions/login'
|
||||||
|
|
||||||
@ -15,25 +14,20 @@ export function useEmail({
|
|||||||
isLogin: boolean
|
isLogin: boolean
|
||||||
isEmailVerificationRequired: boolean
|
isEmailVerificationRequired: boolean
|
||||||
}) {
|
}) {
|
||||||
const [emailFieldVal, setEmailFieldVal] = useState('')
|
async function handleSubmit(data) {
|
||||||
const [passwordFieldVal, setPasswordFieldVal] = useState('')
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
|
||||||
try {
|
try {
|
||||||
if (isLogin) {
|
if (isLogin) {
|
||||||
await login({ email: emailFieldVal, password: passwordFieldVal })
|
await login(data)
|
||||||
onLoginSuccess()
|
onLoginSuccess()
|
||||||
} else {
|
} else {
|
||||||
await signup({ email: emailFieldVal, password: passwordFieldVal })
|
await signup(data)
|
||||||
if (isEmailVerificationRequired) {
|
if (isEmailVerificationRequired) {
|
||||||
showEmailVerificationPending()
|
showEmailVerificationPending()
|
||||||
} else {
|
} else {
|
||||||
await login({ email: emailFieldVal, password: passwordFieldVal })
|
await login(data)
|
||||||
onLoginSuccess()
|
onLoginSuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setEmailFieldVal('')
|
|
||||||
setPasswordFieldVal('')
|
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
onError(err as Error)
|
onError(err as Error)
|
||||||
}
|
}
|
||||||
@ -41,9 +35,5 @@ export function useEmail({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
emailFieldVal,
|
|
||||||
passwordFieldVal,
|
|
||||||
setEmailFieldVal,
|
|
||||||
setPasswordFieldVal,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import signup from '../../../signup'
|
import signup from '../../../signup'
|
||||||
import login from '../../../login'
|
import login from '../../../login'
|
||||||
|
|
||||||
@ -11,21 +10,13 @@ export function useUsernameAndPassword({
|
|||||||
onSuccess: () => void
|
onSuccess: () => void
|
||||||
isLogin: boolean
|
isLogin: boolean
|
||||||
}) {
|
}) {
|
||||||
const [usernameFieldVal, setUsernameFieldVal] = useState('')
|
async function handleSubmit(data) {
|
||||||
const [passwordFieldVal, setPasswordFieldVal] = useState('')
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
|
||||||
try {
|
try {
|
||||||
if (!isLogin) {
|
if (!isLogin) {
|
||||||
await signup({
|
await signup(data)
|
||||||
username: usernameFieldVal,
|
|
||||||
password: passwordFieldVal,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
await login(usernameFieldVal, passwordFieldVal)
|
await login(data.username, data.password)
|
||||||
|
|
||||||
setUsernameFieldVal('')
|
|
||||||
setPasswordFieldVal('')
|
|
||||||
onSuccess()
|
onSuccess()
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
onError(err as Error)
|
onError(err as Error)
|
||||||
@ -34,9 +25,5 @@ export function useUsernameAndPassword({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
usernameFieldVal,
|
|
||||||
passwordFieldVal,
|
|
||||||
setUsernameFieldVal,
|
|
||||||
setPasswordFieldVal,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import { createTheme } from '@stitches/react'
|
import { createTheme } from '@stitches/react'
|
||||||
|
import { UseFormReturn, RegisterOptions } from 'react-hook-form'
|
||||||
|
import type { LoginSignupFormFields } from './internal/common/LoginSignupForm'
|
||||||
|
|
||||||
export enum State {
|
export enum State {
|
||||||
Login = 'login',
|
Login = 'login',
|
||||||
@ -21,3 +23,23 @@ export type ErrorMessage = {
|
|||||||
title: string
|
title: string
|
||||||
description?: string
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FormState = {
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdditionalSignupFieldRenderFn = (
|
||||||
|
hookForm: UseFormReturn<LoginSignupFormFields>,
|
||||||
|
formState: FormState
|
||||||
|
) => React.ReactNode
|
||||||
|
|
||||||
|
export type AdditionalSignupField = {
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
type: 'input' | 'textarea'
|
||||||
|
validations?: RegisterOptions<LoginSignupFormFields>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdditionalSignupFields =
|
||||||
|
| (AdditionalSignupField | AdditionalSignupFieldRenderFn)[]
|
||||||
|
| AdditionalSignupFieldRenderFn
|
||||||
|
@ -12,6 +12,7 @@ export const {
|
|||||||
gray500: 'gainsboro',
|
gray500: 'gainsboro',
|
||||||
gray400: '#f0f0f0',
|
gray400: '#f0f0f0',
|
||||||
red: '#FED7D7',
|
red: '#FED7D7',
|
||||||
|
darkRed: '#fa3838',
|
||||||
green: '#C6F6D5',
|
green: '#C6F6D5',
|
||||||
|
|
||||||
brand: '$waspYellow',
|
brand: '$waspYellow',
|
||||||
@ -23,6 +24,7 @@ export const {
|
|||||||
|
|
||||||
submitButtonText: 'black',
|
submitButtonText: 'black',
|
||||||
|
|
||||||
|
formErrorText: '$darkRed',
|
||||||
},
|
},
|
||||||
fontSizes: {
|
fontSizes: {
|
||||||
sm: '0.875rem'
|
sm: '0.875rem'
|
||||||
|
7
waspc/data/Generator/templates/server/src/auth/index.ts
Normal file
7
waspc/data/Generator/templates/server/src/auth/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{{={= =}=}}
|
||||||
|
{=# isEmailAuthEnabled =}
|
||||||
|
export { defineAdditionalSignupFields } from './providers/email/types.js';
|
||||||
|
{=/ isEmailAuthEnabled =}
|
||||||
|
{=# isLocalAuthEnabled =}
|
||||||
|
export { defineAdditionalSignupFields } from './providers/local/types.js';
|
||||||
|
{=/ isLocalAuthEnabled =}
|
@ -11,6 +11,7 @@ import {
|
|||||||
isEmailResendAllowed,
|
isEmailResendAllowed,
|
||||||
} from "../../utils.js";
|
} from "../../utils.js";
|
||||||
import { GetVerificationEmailContentFn } from './types.js';
|
import { GetVerificationEmailContentFn } from './types.js';
|
||||||
|
import { validateAndGetAdditionalFields } from '../../utils.js'
|
||||||
|
|
||||||
export function getSignupRoute({
|
export function getSignupRoute({
|
||||||
fromField,
|
fromField,
|
||||||
@ -41,8 +42,11 @@ export function getSignupRoute({
|
|||||||
}
|
}
|
||||||
await deleteUser(existingUser);
|
await deleteUser(existingUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const additionalFields = await validateAndGetAdditionalFields(userFields);
|
||||||
|
|
||||||
const user = await createUser({
|
const user = await createUser({
|
||||||
|
...additionalFields,
|
||||||
email: userFields.email,
|
email: userFields.email,
|
||||||
password: userFields.password,
|
password: userFields.password,
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { createDefineAdditionalSignupFieldsFn } from '../types.js'
|
||||||
|
|
||||||
export type GetVerificationEmailContentFn = (params: { verificationLink: string }) => EmailContent;
|
export type GetVerificationEmailContentFn = (params: { verificationLink: string }) => EmailContent;
|
||||||
|
|
||||||
export type GetPasswordResetEmailContentFn = (params: { passwordResetLink: string }) => EmailContent;
|
export type GetPasswordResetEmailContentFn = (params: { passwordResetLink: string }) => EmailContent;
|
||||||
@ -11,3 +13,5 @@ type EmailContent = {
|
|||||||
export const tokenVerificationErrors = {
|
export const tokenVerificationErrors = {
|
||||||
TokenExpiredError: 'TokenExpiredError',
|
TokenExpiredError: 'TokenExpiredError',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defineAdditionalSignupFields = createDefineAdditionalSignupFieldsFn<"email" | "password">()
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import { handleRejection } from '../../../utils.js'
|
import { handleRejection } from '../../../utils.js'
|
||||||
import { createUser } from '../../utils.js'
|
import { createUser } from '../../utils.js'
|
||||||
|
import { validateAndGetAdditionalFields } from '../../utils.js'
|
||||||
|
|
||||||
export default handleRejection(async (req, res) => {
|
export default handleRejection(async (req, res) => {
|
||||||
const userFields = req.body || {}
|
const userFields = req.body || {}
|
||||||
|
|
||||||
|
const additionalFields = await validateAndGetAdditionalFields(userFields)
|
||||||
|
|
||||||
await createUser({
|
await createUser({
|
||||||
|
...additionalFields,
|
||||||
username: userFields.username,
|
username: userFields.username,
|
||||||
password: userFields.password,
|
password: userFields.password,
|
||||||
})
|
})
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
import { createDefineAdditionalSignupFieldsFn } from '../types.js'
|
||||||
|
|
||||||
|
export const defineAdditionalSignupFields = createDefineAdditionalSignupFieldsFn<"username" | "password">()
|
@ -1,4 +1,6 @@
|
|||||||
import type { Router, Request } from "express"
|
import type { Router, Request } from 'express'
|
||||||
|
import type { User } from '../../entities'
|
||||||
|
import type { Expand } from '../../universal/types'
|
||||||
|
|
||||||
export type ProviderConfig = {
|
export type ProviderConfig = {
|
||||||
// Unique provider identifier, used as part of URL paths
|
// Unique provider identifier, used as part of URL paths
|
||||||
@ -17,3 +19,23 @@ export type InitData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type RequestWithWasp = Request & { wasp?: { [key: string]: any } }
|
export type RequestWithWasp = Request & { wasp?: { [key: string]: any } }
|
||||||
|
|
||||||
|
export function createDefineAdditionalSignupFieldsFn<
|
||||||
|
// Wasp already includes these fields in the signup process
|
||||||
|
ExistingFields extends keyof User,
|
||||||
|
PossibleAdditionalFields = Expand<
|
||||||
|
Partial<Omit<User, ExistingFields>>
|
||||||
|
>
|
||||||
|
>() {
|
||||||
|
return function defineFields(config: {
|
||||||
|
[key in keyof PossibleAdditionalFields]: FieldGetter<
|
||||||
|
PossibleAdditionalFields[key]
|
||||||
|
>
|
||||||
|
}) {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldGetter<T> = (
|
||||||
|
data: { [key: string]: unknown }
|
||||||
|
) => Promise<T | undefined> | T | undefined
|
||||||
|
@ -12,6 +12,19 @@ import { isValidEmail } from '../core/auth/validators.js'
|
|||||||
import { emailSender } from '../email/index.js';
|
import { emailSender } from '../email/index.js';
|
||||||
import { Email } from '../email/core/types.js';
|
import { Email } from '../email/core/types.js';
|
||||||
{=/ isEmailAuthEnabled =}
|
{=/ isEmailAuthEnabled =}
|
||||||
|
{=# additionalSignupFields.isDefined =}
|
||||||
|
{=& additionalSignupFields.importStatement =}
|
||||||
|
{=/ additionalSignupFields.isDefined =}
|
||||||
|
|
||||||
|
{=# additionalSignupFields.isDefined =}
|
||||||
|
const _waspAdditionalSignupFieldsConfig = {= additionalSignupFields.importIdentifier =}
|
||||||
|
{=/ additionalSignupFields.isDefined =}
|
||||||
|
{=^ additionalSignupFields.isDefined =}
|
||||||
|
import { createDefineAdditionalSignupFieldsFn } from './providers/types.js'
|
||||||
|
const _waspAdditionalSignupFieldsConfig = {} as ReturnType<
|
||||||
|
ReturnType<typeof createDefineAdditionalSignupFieldsFn<never>>
|
||||||
|
>
|
||||||
|
{=/ additionalSignupFields.isDefined =}
|
||||||
|
|
||||||
type {= userEntityUpper =}Id = {= userEntityUpper =}['id']
|
type {= userEntityUpper =}Id = {= userEntityUpper =}['id']
|
||||||
|
|
||||||
@ -218,4 +231,23 @@ function rethrowPossiblePrismaError(e: unknown): void {
|
|||||||
|
|
||||||
function throwValidationError(message: string): void {
|
function throwValidationError(message: string): void {
|
||||||
throw new HttpError(422, 'Validation failed', { message })
|
throw new HttpError(422, 'Validation failed', { message })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function validateAndGetAdditionalFields(data: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
password: _password,
|
||||||
|
...sanitizedData
|
||||||
|
} = data;
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
for (const [field, getFieldValue] of Object.entries(_waspAdditionalSignupFieldsConfig)) {
|
||||||
|
try {
|
||||||
|
const value = await getFieldValue(sanitizedData)
|
||||||
|
result[field] = value
|
||||||
|
} catch (e) {
|
||||||
|
throwValidationError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
@ -270,7 +270,7 @@
|
|||||||
"file",
|
"file",
|
||||||
"web-app/package.json"
|
"web-app/package.json"
|
||||||
],
|
],
|
||||||
"ee9766b7c88b3d4ac36b1dd4b3237ea750e4c26ad27bcd18006225707d042e18"
|
"8bacfb3d4e24886405c2a8fa94be0be7d3ec4b882063e5f22667893811cd4371"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
|
@ -1 +1 @@
|
|||||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.12.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"rate-limiter-flexible","version":"^2.4.1"},{"name":"superjson","version":"^1.12.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.12.0"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"},{"name":"@types/uuid","version":"^9.0.0"},{"name":"@types/cors","version":"^2.8.5"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"^4.29.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.12.0"},{"name":"superjson","version":"^1.12.2"},{"name":"mitt","version":"3.0.0"}],"devDependencies":[{"name":"vite","version":"^4.3.9"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^2.0.0"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.0.0"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}
|
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.12.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"rate-limiter-flexible","version":"^2.4.1"},{"name":"superjson","version":"^1.12.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.12.0"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"},{"name":"@types/uuid","version":"^9.0.0"},{"name":"@types/cors","version":"^2.8.5"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"^4.29.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.12.0"},{"name":"superjson","version":"^1.12.2"},{"name":"mitt","version":"3.0.0"},{"name":"react-hook-form","version":"^7.45.4"}],"devDependencies":[{"name":"vite","version":"^4.3.9"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^2.0.0"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.0.0"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}
|
@ -18,6 +18,7 @@
|
|||||||
"mitt": "3.0.0",
|
"mitt": "3.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hook-form": "^7.45.4",
|
||||||
"react-router-dom": "^5.3.3",
|
"react-router-dom": "^5.3.3",
|
||||||
"superjson": "^1.12.2"
|
"superjson": "^1.12.2"
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
app waspBuild {
|
app waspBuild {
|
||||||
db: { system: PostgreSQL },
|
db: { system: PostgreSQL },
|
||||||
wasp: {
|
wasp: {
|
||||||
version: "^0.11.3"
|
version: "^0.11.4"
|
||||||
},
|
},
|
||||||
title: "waspBuild"
|
title: "waspBuild"
|
||||||
}
|
}
|
||||||
|
@ -284,7 +284,7 @@
|
|||||||
"file",
|
"file",
|
||||||
"web-app/package.json"
|
"web-app/package.json"
|
||||||
],
|
],
|
||||||
"8a2249588d7cf9ac7c6c8cb979727c264446581f60c7062e5852aef4c1d8a675"
|
"c2b7000a7380cce059cdafe67fa755a8e5f4d1de5e13eb11e545a3ee4d32db0d"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
|
@ -1 +1 @@
|
|||||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.12.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"rate-limiter-flexible","version":"^2.4.1"},{"name":"superjson","version":"^1.12.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.12.0"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"},{"name":"@types/uuid","version":"^9.0.0"},{"name":"@types/cors","version":"^2.8.5"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"^4.29.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.12.0"},{"name":"superjson","version":"^1.12.2"},{"name":"mitt","version":"3.0.0"}],"devDependencies":[{"name":"vite","version":"^4.3.9"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^2.0.0"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.0.0"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}
|
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.12.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"rate-limiter-flexible","version":"^2.4.1"},{"name":"superjson","version":"^1.12.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.12.0"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"},{"name":"@types/uuid","version":"^9.0.0"},{"name":"@types/cors","version":"^2.8.5"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"^4.29.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.12.0"},{"name":"superjson","version":"^1.12.2"},{"name":"mitt","version":"3.0.0"},{"name":"react-hook-form","version":"^7.45.4"}],"devDependencies":[{"name":"vite","version":"^4.3.9"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^2.0.0"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.0.0"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}
|
@ -18,6 +18,7 @@
|
|||||||
"mitt": "3.0.0",
|
"mitt": "3.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hook-form": "^7.45.4",
|
||||||
"react-router-dom": "^5.3.3",
|
"react-router-dom": "^5.3.3",
|
||||||
"superjson": "^1.12.2"
|
"superjson": "^1.12.2"
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
app waspCompile {
|
app waspCompile {
|
||||||
wasp: {
|
wasp: {
|
||||||
version: "^0.11.3"
|
version: "^0.11.4"
|
||||||
},
|
},
|
||||||
title: "waspCompile"
|
title: "waspCompile"
|
||||||
}
|
}
|
||||||
|
@ -179,14 +179,14 @@
|
|||||||
"file",
|
"file",
|
||||||
"server/src/auth/providers/types.ts"
|
"server/src/auth/providers/types.ts"
|
||||||
],
|
],
|
||||||
"9859bf91e0abe0aaadf7b8c74c573607f6085a5414071836cef6ba84b2bebb69"
|
"323555d76755fe32b21084f063caf931faabcb5937c279cc706bbecad3361d43"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
"file",
|
"file",
|
||||||
"server/src/auth/utils.ts"
|
"server/src/auth/utils.ts"
|
||||||
],
|
],
|
||||||
"b611de9a6b546f6f1cec4497a4cb525150399131dfba32b53ba34afb04e62e96"
|
"1cfb0c8095ba0ed7686229b22e8a9edd971526d50660f88d2cbb988af9b5af30"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
@ -578,7 +578,7 @@
|
|||||||
"file",
|
"file",
|
||||||
"web-app/package.json"
|
"web-app/package.json"
|
||||||
],
|
],
|
||||||
"fc5d4d8e4a3ed36972eac6891122dfd3d8edb4074a8df041162dcb38bb6f6d2d"
|
"7857f3a9fc85cd39f16dbd9362bccc5e5662b5b216da45b843ebca6bba4142b5"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
@ -655,7 +655,7 @@
|
|||||||
"file",
|
"file",
|
||||||
"web-app/src/auth/forms/Auth.tsx"
|
"web-app/src/auth/forms/Auth.tsx"
|
||||||
],
|
],
|
||||||
"d40cf940a499fdd4b137dcf9f3cd4fbe0bbab4b7c44eb7819b41daeaa861050b"
|
"fc6c204f73999f556eab441772e66dd4dbd433bd69aa2c9e64e713fb2921a886"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
@ -669,14 +669,14 @@
|
|||||||
"file",
|
"file",
|
||||||
"web-app/src/auth/forms/Signup.tsx"
|
"web-app/src/auth/forms/Signup.tsx"
|
||||||
],
|
],
|
||||||
"cacd5348e84d42bc142f6c6e00051a8e86f6b17eb037b2772772f073925bf570"
|
"a38124a9a250a603ef6d04dbcb46c04084ace676e9de5fc31f229405d87f47d4"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
"file",
|
"file",
|
||||||
"web-app/src/auth/forms/internal/Form.tsx"
|
"web-app/src/auth/forms/internal/Form.tsx"
|
||||||
],
|
],
|
||||||
"ce6b409fda73d88e762b27aef7038618961e61e6ef8e5f66912325ab88a8223e"
|
"b9b21954b919f173b751c0078aed8303cc15d88df9e9874228efcae7976f26cd"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
@ -690,7 +690,7 @@
|
|||||||
"file",
|
"file",
|
||||||
"web-app/src/auth/forms/internal/common/LoginSignupForm.tsx"
|
"web-app/src/auth/forms/internal/common/LoginSignupForm.tsx"
|
||||||
],
|
],
|
||||||
"c92bab325f51159c3d1bb285c7e807acbb85069c5b16afc3a35fbd0121c91b6a"
|
"f211f57dca3f10f08a3e618d27bf8006d26f6832bb7e40f8a22ae44f2d42531e"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
@ -711,7 +711,7 @@
|
|||||||
"file",
|
"file",
|
||||||
"web-app/src/auth/forms/types.ts"
|
"web-app/src/auth/forms/types.ts"
|
||||||
],
|
],
|
||||||
"c4066fbd39ec20a3d43be9f9d5762d555dbc006057579ac168a67b2678918a13"
|
"992ca4b2c8e30536636143c556e6bdcc5d5d0d86c1eb2e119171e25d5c33b4e3"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
@ -935,7 +935,7 @@
|
|||||||
"file",
|
"file",
|
||||||
"web-app/src/stitches.config.js"
|
"web-app/src/stitches.config.js"
|
||||||
],
|
],
|
||||||
"7de37836b80021870f286ff14d275e2ca7a1c2aa113ba5a5624ed0c77e178f76"
|
"f238234a9db89d6a34c7a8c7c948a58c011da8e167ff94d72e7c6808beb4e177"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
|
@ -1 +1 @@
|
|||||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.12.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"rate-limiter-flexible","version":"^2.4.1"},{"name":"superjson","version":"^1.12.2"},{"name":"passport","version":"0.6.0"},{"name":"passport-google-oauth20","version":"2.0.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"@sendgrid/mail","version":"^7.7.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.12.0"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"},{"name":"@types/uuid","version":"^9.0.0"},{"name":"@types/cors","version":"^2.8.5"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"^4.29.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.12.0"},{"name":"superjson","version":"^1.12.2"},{"name":"mitt","version":"3.0.0"},{"name":"@stitches/react","version":"^1.2.8"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"vite","version":"^4.3.9"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^2.0.0"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.0.0"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}
|
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.12.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"rate-limiter-flexible","version":"^2.4.1"},{"name":"superjson","version":"^1.12.2"},{"name":"passport","version":"0.6.0"},{"name":"passport-google-oauth20","version":"2.0.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"@sendgrid/mail","version":"^7.7.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.12.0"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"},{"name":"@types/uuid","version":"^9.0.0"},{"name":"@types/cors","version":"^2.8.5"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"^4.29.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.12.0"},{"name":"superjson","version":"^1.12.2"},{"name":"mitt","version":"3.0.0"},{"name":"react-hook-form","version":"^7.45.4"},{"name":"@stitches/react","version":"^1.2.8"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"vite","version":"^4.3.9"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^2.0.0"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.0.0"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}
|
@ -1,4 +1,6 @@
|
|||||||
import type { Router, Request } from "express"
|
import type { Router, Request } from 'express'
|
||||||
|
import type { User } from '../../entities'
|
||||||
|
import type { Expand } from '../../universal/types'
|
||||||
|
|
||||||
export type ProviderConfig = {
|
export type ProviderConfig = {
|
||||||
// Unique provider identifier, used as part of URL paths
|
// Unique provider identifier, used as part of URL paths
|
||||||
@ -17,3 +19,23 @@ export type InitData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type RequestWithWasp = Request & { wasp?: { [key: string]: any } }
|
export type RequestWithWasp = Request & { wasp?: { [key: string]: any } }
|
||||||
|
|
||||||
|
export function createDefineAdditionalSignupFieldsFn<
|
||||||
|
// Wasp already includes these fields in the signup process
|
||||||
|
ExistingFields extends keyof User,
|
||||||
|
PossibleAdditionalFields = Expand<
|
||||||
|
Partial<Omit<User, ExistingFields>>
|
||||||
|
>
|
||||||
|
>() {
|
||||||
|
return function defineFields(config: {
|
||||||
|
[key in keyof PossibleAdditionalFields]: FieldGetter<
|
||||||
|
PossibleAdditionalFields[key]
|
||||||
|
>
|
||||||
|
}) {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldGetter<T> = (
|
||||||
|
data: { [key: string]: unknown }
|
||||||
|
) => Promise<T | undefined> | T | undefined
|
||||||
|
@ -7,6 +7,11 @@ import { type User } from '../entities/index.js'
|
|||||||
import waspServerConfig from '../config.js';
|
import waspServerConfig from '../config.js';
|
||||||
import { type Prisma } from '@prisma/client';
|
import { type Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
import { createDefineAdditionalSignupFieldsFn } from './providers/types.js'
|
||||||
|
const _waspAdditionalSignupFieldsConfig = {} as ReturnType<
|
||||||
|
ReturnType<typeof createDefineAdditionalSignupFieldsFn<never>>
|
||||||
|
>
|
||||||
|
|
||||||
type UserId = User['id']
|
type UserId = User['id']
|
||||||
|
|
||||||
export const contextWithUserEntity = {
|
export const contextWithUserEntity = {
|
||||||
@ -73,4 +78,23 @@ function rethrowPossiblePrismaError(e: unknown): void {
|
|||||||
|
|
||||||
function throwValidationError(message: string): void {
|
function throwValidationError(message: string): void {
|
||||||
throw new HttpError(422, 'Validation failed', { message })
|
throw new HttpError(422, 'Validation failed', { message })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function validateAndGetAdditionalFields(data: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
password: _password,
|
||||||
|
...sanitizedData
|
||||||
|
} = data;
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
for (const [field, getFieldValue] of Object.entries(_waspAdditionalSignupFieldsConfig)) {
|
||||||
|
try {
|
||||||
|
const value = await getFieldValue(sanitizedData)
|
||||||
|
result[field] = value
|
||||||
|
} catch (e) {
|
||||||
|
throwValidationError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"mitt": "3.0.0",
|
"mitt": "3.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hook-form": "^7.45.4",
|
||||||
"react-redux": "^7.1.3",
|
"react-redux": "^7.1.3",
|
||||||
"react-router-dom": "^5.3.3",
|
"react-router-dom": "^5.3.3",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
type State,
|
type State,
|
||||||
type CustomizationOptions,
|
type CustomizationOptions,
|
||||||
type ErrorMessage,
|
type ErrorMessage,
|
||||||
|
type AdditionalSignupFields,
|
||||||
} from './types'
|
} from './types'
|
||||||
import { LoginSignupForm } from './internal/common/LoginSignupForm'
|
import { LoginSignupForm } from './internal/common/LoginSignupForm'
|
||||||
import { MessageError, MessageSuccess } from './internal/Message'
|
import { MessageError, MessageSuccess } from './internal/Message'
|
||||||
@ -33,9 +34,11 @@ export const AuthContext = createContext({
|
|||||||
setSuccessMessage: (successMessage: string | null) => {},
|
setSuccessMessage: (successMessage: string | null) => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
function Auth ({ state, appearance, logo, socialLayout = 'horizontal' }: {
|
function Auth ({ state, appearance, logo, socialLayout = 'horizontal', additionalSignupFields }: {
|
||||||
state: State;
|
state: State;
|
||||||
} & CustomizationOptions) {
|
} & CustomizationOptions & {
|
||||||
|
additionalSignupFields?: AdditionalSignupFields;
|
||||||
|
}) {
|
||||||
const [errorMessage, setErrorMessage] = useState<ErrorMessage | null>(null);
|
const [errorMessage, setErrorMessage] = useState<ErrorMessage | null>(null);
|
||||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@ -71,6 +74,7 @@ function Auth ({ state, appearance, logo, socialLayout = 'horizontal' }: {
|
|||||||
<LoginSignupForm
|
<LoginSignupForm
|
||||||
state={state}
|
state={state}
|
||||||
socialButtonsDirection={socialButtonsDirection}
|
socialButtonsDirection={socialButtonsDirection}
|
||||||
|
additionalSignupFields={additionalSignupFields}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
import Auth from './Auth'
|
import Auth from './Auth'
|
||||||
import { type CustomizationOptions, State } from './types'
|
import {
|
||||||
|
type CustomizationOptions,
|
||||||
|
type AdditionalSignupFields,
|
||||||
|
State,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
export function SignupForm({
|
export function SignupForm({
|
||||||
appearance,
|
appearance,
|
||||||
logo,
|
logo,
|
||||||
socialLayout,
|
socialLayout,
|
||||||
}: CustomizationOptions) {
|
additionalFields,
|
||||||
|
}: CustomizationOptions & { additionalFields?: AdditionalSignupFields; }) {
|
||||||
return (
|
return (
|
||||||
<Auth
|
<Auth
|
||||||
appearance={appearance}
|
appearance={appearance}
|
||||||
logo={logo}
|
logo={logo}
|
||||||
socialLayout={socialLayout}
|
socialLayout={socialLayout}
|
||||||
state={State.Signup}
|
state={State.Signup}
|
||||||
|
additionalSignupFields={additionalFields}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,10 @@ export const FormLabel = styled('label', {
|
|||||||
display: 'block',
|
display: 'block',
|
||||||
fontSize: '$sm',
|
fontSize: '$sm',
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
|
marginBottom: '0.5rem',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const FormInput = styled('input', {
|
const commonInputStyles = {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
lineHeight: '1.5rem',
|
lineHeight: '1.5rem',
|
||||||
fontSize: '$sm',
|
fontSize: '$sm',
|
||||||
@ -44,7 +45,18 @@ export const FormInput = styled('input', {
|
|||||||
paddingBottom: '0.375rem',
|
paddingBottom: '0.375rem',
|
||||||
paddingLeft: '0.75rem',
|
paddingLeft: '0.75rem',
|
||||||
paddingRight: '0.75rem',
|
paddingRight: '0.75rem',
|
||||||
|
margin: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormInput = styled('input', commonInputStyles)
|
||||||
|
|
||||||
|
export const FormTextarea = styled('textarea', commonInputStyles)
|
||||||
|
|
||||||
|
export const FormError = styled('div', {
|
||||||
|
display: 'block',
|
||||||
|
fontSize: '$sm',
|
||||||
|
fontWeight: '500',
|
||||||
|
color: '$formErrorText',
|
||||||
marginTop: '0.5rem',
|
marginTop: '0.5rem',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,9 +1,22 @@
|
|||||||
import { useContext, type FormEvent } from 'react'
|
import { useContext } from 'react'
|
||||||
import { styled } from '../../../../stitches.config'
|
import { useForm, UseFormReturn } from 'react-hook-form'
|
||||||
import config from '../../../../config.js'
|
|
||||||
|
|
||||||
import { AuthContext } from '../../Auth'
|
import { AuthContext } from '../../Auth'
|
||||||
import { Form, FormInput, FormItemGroup, FormLabel, SubmitButton } from '../Form'
|
import {
|
||||||
|
Form,
|
||||||
|
FormInput,
|
||||||
|
FormItemGroup,
|
||||||
|
FormLabel,
|
||||||
|
FormError,
|
||||||
|
FormTextarea,
|
||||||
|
SubmitButton,
|
||||||
|
} from '../Form'
|
||||||
|
import type {
|
||||||
|
AdditionalSignupFields,
|
||||||
|
AdditionalSignupField,
|
||||||
|
AdditionalSignupFieldRenderFn,
|
||||||
|
FormState,
|
||||||
|
} from '../../types'
|
||||||
import * as SocialIcons from '../social/SocialIcons'
|
import * as SocialIcons from '../social/SocialIcons'
|
||||||
import { SocialButton } from '../social/SocialButton'
|
import { SocialButton } from '../social/SocialButton'
|
||||||
|
|
||||||
@ -46,12 +59,18 @@ const SocialAuthButtons = styled('div', {
|
|||||||
})
|
})
|
||||||
const googleSignInUrl = `${config.apiUrl}/auth/google/login`
|
const googleSignInUrl = `${config.apiUrl}/auth/google/login`
|
||||||
|
|
||||||
|
export type LoginSignupFormFields = {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const LoginSignupForm = ({
|
export const LoginSignupForm = ({
|
||||||
state,
|
state,
|
||||||
socialButtonsDirection = 'horizontal',
|
socialButtonsDirection = 'horizontal',
|
||||||
|
additionalSignupFields,
|
||||||
}: {
|
}: {
|
||||||
state: 'login' | 'signup',
|
state: 'login' | 'signup'
|
||||||
socialButtonsDirection?: 'horizontal' | 'vertical';
|
socialButtonsDirection?: 'horizontal' | 'vertical'
|
||||||
|
additionalSignupFields?: AdditionalSignupFields
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
isLoading,
|
isLoading,
|
||||||
@ -59,7 +78,10 @@ export const LoginSignupForm = ({
|
|||||||
setSuccessMessage,
|
setSuccessMessage,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
} = useContext(AuthContext)
|
} = useContext(AuthContext)
|
||||||
const cta = state === 'login' ? 'Log in' : 'Sign up';
|
const isLogin = state === 'login'
|
||||||
|
const cta = isLogin ? 'Log in' : 'Sign up';
|
||||||
|
const hookForm = useForm<LoginSignupFormFields>()
|
||||||
|
const { register, formState: { errors }, handleSubmit: hookFormHandleSubmit } = hookForm
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<SocialAuth>
|
<SocialAuth>
|
||||||
@ -71,3 +93,76 @@ export const LoginSignupForm = ({
|
|||||||
</SocialAuth>
|
</SocialAuth>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AdditionalFormFields({
|
||||||
|
hookForm,
|
||||||
|
formState: { isLoading },
|
||||||
|
additionalSignupFields,
|
||||||
|
}: {
|
||||||
|
hookForm: UseFormReturn<LoginSignupFormFields>;
|
||||||
|
formState: FormState;
|
||||||
|
additionalSignupFields: AdditionalSignupFields;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
} = hookForm;
|
||||||
|
|
||||||
|
function renderField<ComponentType extends React.JSXElementConstructor<any>>(
|
||||||
|
field: AdditionalSignupField,
|
||||||
|
// Ideally we would use ComponentType here, but it doesn't work with react-hook-form
|
||||||
|
Component: any,
|
||||||
|
props?: React.ComponentProps<ComponentType>
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<FormItemGroup key={field.name}>
|
||||||
|
<FormLabel>{field.label}</FormLabel>
|
||||||
|
<Component
|
||||||
|
{...register(field.name, field.validations)}
|
||||||
|
{...props}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
{errors[field.name] && (
|
||||||
|
<FormError>{errors[field.name].message}</FormError>
|
||||||
|
)}
|
||||||
|
</FormItemGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areAdditionalFieldsRenderFn(additionalSignupFields)) {
|
||||||
|
return additionalSignupFields(hookForm, { isLoading })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
additionalSignupFields &&
|
||||||
|
additionalSignupFields.map((field) => {
|
||||||
|
if (isFieldRenderFn(field)) {
|
||||||
|
return field(hookForm, { isLoading })
|
||||||
|
}
|
||||||
|
switch (field.type) {
|
||||||
|
case 'input':
|
||||||
|
return renderField<typeof FormInput>(field, FormInput, {
|
||||||
|
type: 'text',
|
||||||
|
})
|
||||||
|
case 'textarea':
|
||||||
|
return renderField<typeof FormTextarea>(field, FormTextarea)
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported additional signup field type: ${field.type}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFieldRenderFn(
|
||||||
|
additionalSignupField: AdditionalSignupField | AdditionalSignupFieldRenderFn
|
||||||
|
): additionalSignupField is AdditionalSignupFieldRenderFn {
|
||||||
|
return typeof additionalSignupField === 'function'
|
||||||
|
}
|
||||||
|
|
||||||
|
function areAdditionalFieldsRenderFn(
|
||||||
|
additionalSignupFields: AdditionalSignupFields
|
||||||
|
): additionalSignupFields is AdditionalSignupFieldRenderFn {
|
||||||
|
return typeof additionalSignupFields === 'function'
|
||||||
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { createTheme } from '@stitches/react'
|
import { createTheme } from '@stitches/react'
|
||||||
|
import { UseFormReturn, RegisterOptions } from 'react-hook-form'
|
||||||
|
import type { LoginSignupFormFields } from './internal/common/LoginSignupForm'
|
||||||
|
|
||||||
export enum State {
|
export enum State {
|
||||||
Login = 'login',
|
Login = 'login',
|
||||||
@ -15,3 +17,23 @@ export type ErrorMessage = {
|
|||||||
title: string
|
title: string
|
||||||
description?: string
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FormState = {
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdditionalSignupFieldRenderFn = (
|
||||||
|
hookForm: UseFormReturn<LoginSignupFormFields>,
|
||||||
|
formState: FormState
|
||||||
|
) => React.ReactNode
|
||||||
|
|
||||||
|
export type AdditionalSignupField = {
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
type: 'input' | 'textarea'
|
||||||
|
validations?: RegisterOptions<LoginSignupFormFields>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdditionalSignupFields =
|
||||||
|
| (AdditionalSignupField | AdditionalSignupFieldRenderFn)[]
|
||||||
|
| AdditionalSignupFieldRenderFn
|
||||||
|
@ -12,6 +12,7 @@ export const {
|
|||||||
gray500: 'gainsboro',
|
gray500: 'gainsboro',
|
||||||
gray400: '#f0f0f0',
|
gray400: '#f0f0f0',
|
||||||
red: '#FED7D7',
|
red: '#FED7D7',
|
||||||
|
darkRed: '#fa3838',
|
||||||
green: '#C6F6D5',
|
green: '#C6F6D5',
|
||||||
|
|
||||||
brand: '$waspYellow',
|
brand: '$waspYellow',
|
||||||
@ -23,6 +24,7 @@ export const {
|
|||||||
|
|
||||||
submitButtonText: 'black',
|
submitButtonText: 'black',
|
||||||
|
|
||||||
|
formErrorText: '$darkRed',
|
||||||
},
|
},
|
||||||
fontSizes: {
|
fontSizes: {
|
||||||
sm: '0.875rem'
|
sm: '0.875rem'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
app waspComplexTest {
|
app waspComplexTest {
|
||||||
db: { system: PostgreSQL },
|
db: { system: PostgreSQL },
|
||||||
wasp: {
|
wasp: {
|
||||||
version: "^0.11.3"
|
version: "^0.11.4"
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
userEntity: User,
|
userEntity: User,
|
||||||
|
@ -326,7 +326,7 @@
|
|||||||
"file",
|
"file",
|
||||||
"web-app/package.json"
|
"web-app/package.json"
|
||||||
],
|
],
|
||||||
"57da965d01f5d39d74c6ca91f00d84ba5b6c78660ee837266b2622e410e4fd8e"
|
"adcc3a24462553bd66d4c14f43cd351512d28c249d96d3cd3e1e6dad834770fd"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
|
@ -1 +1 @@
|
|||||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.12.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"rate-limiter-flexible","version":"^2.4.1"},{"name":"superjson","version":"^1.12.2"},{"name":"pg-boss","version":"^8.4.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.12.0"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"},{"name":"@types/uuid","version":"^9.0.0"},{"name":"@types/cors","version":"^2.8.5"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"^4.29.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.12.0"},{"name":"superjson","version":"^1.12.2"},{"name":"mitt","version":"3.0.0"}],"devDependencies":[{"name":"vite","version":"^4.3.9"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^2.0.0"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.0.0"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}
|
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.12.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"rate-limiter-flexible","version":"^2.4.1"},{"name":"superjson","version":"^1.12.2"},{"name":"pg-boss","version":"^8.4.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.12.0"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"},{"name":"@types/uuid","version":"^9.0.0"},{"name":"@types/cors","version":"^2.8.5"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"^4.29.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.12.0"},{"name":"superjson","version":"^1.12.2"},{"name":"mitt","version":"3.0.0"},{"name":"react-hook-form","version":"^7.45.4"}],"devDependencies":[{"name":"vite","version":"^4.3.9"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^2.0.0"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.0.0"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}
|
@ -18,6 +18,7 @@
|
|||||||
"mitt": "3.0.0",
|
"mitt": "3.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hook-form": "^7.45.4",
|
||||||
"react-router-dom": "^5.3.3",
|
"react-router-dom": "^5.3.3",
|
||||||
"superjson": "^1.12.2"
|
"superjson": "^1.12.2"
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
app waspJob {
|
app waspJob {
|
||||||
db: { system: PostgreSQL },
|
db: { system: PostgreSQL },
|
||||||
wasp: {
|
wasp: {
|
||||||
version: "^0.11.3"
|
version: "^0.11.4"
|
||||||
},
|
},
|
||||||
title: "waspJob"
|
title: "waspJob"
|
||||||
}
|
}
|
||||||
|
@ -284,7 +284,7 @@
|
|||||||
"file",
|
"file",
|
||||||
"web-app/package.json"
|
"web-app/package.json"
|
||||||
],
|
],
|
||||||
"7e237189e89ac549b485ffef329aaa6699c874fd16b8e890f37f412ad7715219"
|
"fd7ca891ab5d232690015d34110d34a180906e1bb7c37e2432ebad2356b860a3"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
|
@ -1 +1 @@
|
|||||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.12.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"rate-limiter-flexible","version":"^2.4.1"},{"name":"superjson","version":"^1.12.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.12.0"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"},{"name":"@types/uuid","version":"^9.0.0"},{"name":"@types/cors","version":"^2.8.5"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"^4.29.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.12.0"},{"name":"superjson","version":"^1.12.2"},{"name":"mitt","version":"3.0.0"}],"devDependencies":[{"name":"vite","version":"^4.3.9"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^2.0.0"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.0.0"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}
|
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.12.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"rate-limiter-flexible","version":"^2.4.1"},{"name":"superjson","version":"^1.12.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.12.0"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"},{"name":"@types/uuid","version":"^9.0.0"},{"name":"@types/cors","version":"^2.8.5"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"^4.29.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.12.0"},{"name":"superjson","version":"^1.12.2"},{"name":"mitt","version":"3.0.0"},{"name":"react-hook-form","version":"^7.45.4"}],"devDependencies":[{"name":"vite","version":"^4.3.9"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^2.0.0"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.0.0"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}
|
@ -18,6 +18,7 @@
|
|||||||
"mitt": "3.0.0",
|
"mitt": "3.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hook-form": "^7.45.4",
|
||||||
"react-router-dom": "^5.3.3",
|
"react-router-dom": "^5.3.3",
|
||||||
"superjson": "^1.12.2"
|
"superjson": "^1.12.2"
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
app waspMigrate {
|
app waspMigrate {
|
||||||
wasp: {
|
wasp: {
|
||||||
version: "^0.11.3"
|
version: "^0.11.4"
|
||||||
},
|
},
|
||||||
title: "waspMigrate"
|
title: "waspMigrate"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
app waspNew {
|
app waspNew {
|
||||||
wasp: {
|
wasp: {
|
||||||
version: "^0.11.3"
|
version: "^0.11.4"
|
||||||
},
|
},
|
||||||
title: "waspNew"
|
title: "waspNew"
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,13 @@ app crudTesting {
|
|||||||
usernameAndPassword: {},
|
usernameAndPassword: {},
|
||||||
},
|
},
|
||||||
onAuthFailedRedirectTo: "/login",
|
onAuthFailedRedirectTo: "/login",
|
||||||
|
signup: {
|
||||||
|
additionalFields: import { fields } from "@server/auth.js",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
dependencies: [
|
||||||
|
("zod", "^3.22.2")
|
||||||
|
],
|
||||||
db: {
|
db: {
|
||||||
system: PostgreSQL
|
system: PostgreSQL
|
||||||
}
|
}
|
||||||
@ -44,6 +50,7 @@ entity User {=psl
|
|||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
username String @unique
|
username String @unique
|
||||||
password String
|
password String
|
||||||
|
address String?
|
||||||
tasks Task[]
|
tasks Task[]
|
||||||
psl=}
|
psl=}
|
||||||
|
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "address" TEXT;
|
5
waspc/examples/crud-testing/prettier.config.js
Normal file
5
waspc/examples/crud-testing/prettier.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
trailingComma: 'es5',
|
||||||
|
semi: false,
|
||||||
|
singleQuote: true,
|
||||||
|
}
|
@ -2,6 +2,7 @@ import "./Main.css";
|
|||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Link, routes } from "@wasp/router";
|
import { Link, routes } from "@wasp/router";
|
||||||
|
import logout from "@wasp/auth/logout";
|
||||||
|
|
||||||
import { tasks as tasksCrud } from "@wasp/crud/tasks";
|
import { tasks as tasksCrud } from "@wasp/crud/tasks";
|
||||||
import { User } from "@wasp/entities";
|
import { User } from "@wasp/entities";
|
||||||
@ -45,7 +46,7 @@ const MainPage = ({ user }: { user: User }) => {
|
|||||||
setEditTaskTitle("");
|
setEditTaskTitle("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStartEditing(task: Task) {
|
function handleStartEditing(task: { id: number; title: string }) {
|
||||||
setIsEditing(task.id);
|
setIsEditing(task.id);
|
||||||
setEditTaskTitle(task.title);
|
setEditTaskTitle(task.title);
|
||||||
}
|
}
|
||||||
@ -122,6 +123,7 @@ const MainPage = ({ user }: { user: User }) => {
|
|||||||
Create task
|
Create task
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
<button onClick={logout}>Logout</button>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,24 @@
|
|||||||
import { SignupForm } from "@wasp/auth/forms/Signup";
|
import { SignupForm } from '@wasp/auth/forms/Signup'
|
||||||
|
import {
|
||||||
|
FormError,
|
||||||
|
FormInput,
|
||||||
|
FormItemGroup,
|
||||||
|
FormLabel,
|
||||||
|
} from '@wasp/auth/forms/internal/Form'
|
||||||
|
|
||||||
export const SignupPage = () => {
|
export const SignupPage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<SignupForm
|
||||||
<main>
|
additionalFields={[
|
||||||
<h1>Signup</h1>
|
{
|
||||||
<SignupForm />
|
name: 'address',
|
||||||
</main>
|
label: 'Address',
|
||||||
</div>
|
type: 'input',
|
||||||
);
|
validations: {
|
||||||
};
|
required: 'Address is required'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
19
waspc/examples/crud-testing/src/server/auth.ts
Normal file
19
waspc/examples/crud-testing/src/server/auth.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { defineAdditionalSignupFields } from '@wasp/auth/index.js'
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
export const fields = defineAdditionalSignupFields({
|
||||||
|
address: (data) => {
|
||||||
|
console.log('Received data:', data)
|
||||||
|
const AddressSchema = z
|
||||||
|
.string({
|
||||||
|
required_error: 'Address is required',
|
||||||
|
invalid_type_error: 'Address must be a string',
|
||||||
|
})
|
||||||
|
.min(10, 'Address must be at least 10 characters long')
|
||||||
|
const result = AddressSchema.safeParse(data.address)
|
||||||
|
if (result.success === false) {
|
||||||
|
throw new Error(result.error.issues[0].message)
|
||||||
|
}
|
||||||
|
return result.data
|
||||||
|
},
|
||||||
|
})
|
5
waspc/examples/crud-testing/src/server/auth_simple.js
Normal file
5
waspc/examples/crud-testing/src/server/auth_simple.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { defineAdditionalSignupFields } from '@wasp/auth/index.js'
|
||||||
|
|
||||||
|
export const fields = defineAdditionalSignupFields({
|
||||||
|
address: (data) => data.address,
|
||||||
|
})
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "address" TEXT;
|
@ -7,6 +7,7 @@ import getDate from '@wasp/queries/getDate'
|
|||||||
import Todo, { areThereAnyTasks } from './Todo'
|
import Todo, { areThereAnyTasks } from './Todo'
|
||||||
import { App } from './App'
|
import { App } from './App'
|
||||||
import { getMe } from '@wasp/auth/useAuth'
|
import { getMe } from '@wasp/auth/useAuth'
|
||||||
|
import type { User } from '@wasp/auth/types'
|
||||||
|
|
||||||
test('areThereAnyTasks', () => {
|
test('areThereAnyTasks', () => {
|
||||||
expect(areThereAnyTasks([])).toBe(false)
|
expect(areThereAnyTasks([])).toBe(false)
|
||||||
@ -38,8 +39,9 @@ const mockUser = {
|
|||||||
email: 'elon@tesla.com',
|
email: 'elon@tesla.com',
|
||||||
isEmailVerified: false,
|
isEmailVerified: false,
|
||||||
emailVerificationSentAt: null,
|
emailVerificationSentAt: null,
|
||||||
passwordResetSentAt: null
|
passwordResetSentAt: null,
|
||||||
}
|
address: null,
|
||||||
|
} satisfies User
|
||||||
|
|
||||||
test('handles multiple mock data sources', async () => {
|
test('handles multiple mock data sources', async () => {
|
||||||
mockQuery(getMe, mockUser)
|
mockQuery(getMe, mockUser)
|
||||||
|
@ -7,6 +7,7 @@ import { getTotalTaskCountMessage } from './helpers'
|
|||||||
|
|
||||||
import appearance from './appearance'
|
import appearance from './appearance'
|
||||||
import todoLogo from '../../todoLogo.png'
|
import todoLogo from '../../todoLogo.png'
|
||||||
|
import { FormItemGroup } from '@wasp/auth/forms/internal/Form'
|
||||||
|
|
||||||
const Signup = () => {
|
const Signup = () => {
|
||||||
const { data: numTasks } = useQuery(getNumTasks)
|
const { data: numTasks } = useQuery(getNumTasks)
|
||||||
@ -20,6 +21,22 @@ const Signup = () => {
|
|||||||
appearance={appearance}
|
appearance={appearance}
|
||||||
logo={todoLogo}
|
logo={todoLogo}
|
||||||
socialLayout="horizontal"
|
socialLayout="horizontal"
|
||||||
|
additionalFields={[
|
||||||
|
{
|
||||||
|
name: 'address',
|
||||||
|
type: 'input',
|
||||||
|
label: 'Address',
|
||||||
|
validations: {
|
||||||
|
required: 'Address is required',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
() => (
|
||||||
|
<FormItemGroup className="text-sm text-gray-500">
|
||||||
|
👉 Don't forget to press the button below to submit the
|
||||||
|
form.
|
||||||
|
</FormItemGroup>
|
||||||
|
),
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<span className="text-sm font-medium text-gray-900">
|
<span className="text-sm font-medium text-gray-900">
|
||||||
|
13
waspc/examples/todoApp/src/server/auth/signup.ts
Normal file
13
waspc/examples/todoApp/src/server/auth/signup.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { defineAdditionalSignupFields } from '@wasp/auth/index.js'
|
||||||
|
|
||||||
|
export const fields = defineAdditionalSignupFields({
|
||||||
|
address: (data) => {
|
||||||
|
if (typeof data.address !== 'string') {
|
||||||
|
throw new Error('Address is required.')
|
||||||
|
}
|
||||||
|
if (data.address.length < 10) {
|
||||||
|
throw new Error('Address must be at least 10 characters long.')
|
||||||
|
}
|
||||||
|
return data.address
|
||||||
|
},
|
||||||
|
})
|
@ -41,6 +41,9 @@ app todoApp {
|
|||||||
allowUnverifiedLogin: false,
|
allowUnverifiedLogin: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
signup: {
|
||||||
|
additionalFields: import { fields } from "@server/auth/signup.js",
|
||||||
|
},
|
||||||
onAuthFailedRedirectTo: "/login",
|
onAuthFailedRedirectTo: "/login",
|
||||||
onAuthSucceededRedirectTo: "/profile"
|
onAuthSucceededRedirectTo: "/profile"
|
||||||
},
|
},
|
||||||
@ -79,6 +82,7 @@ entity User {=psl
|
|||||||
externalAuthAssociations SocialLogin[]
|
externalAuthAssociations SocialLogin[]
|
||||||
// Business logic
|
// Business logic
|
||||||
tasks Task[]
|
tasks Task[]
|
||||||
|
address String?
|
||||||
psl=}
|
psl=}
|
||||||
|
|
||||||
entity SocialLogin {=psl
|
entity SocialLogin {=psl
|
||||||
|
@ -6,6 +6,7 @@ module Wasp.AppSpec.App.Auth
|
|||||||
AuthMethods (..),
|
AuthMethods (..),
|
||||||
ExternalAuthConfig (..),
|
ExternalAuthConfig (..),
|
||||||
EmailAuthConfig (..),
|
EmailAuthConfig (..),
|
||||||
|
SignupOptions (..),
|
||||||
usernameAndPasswordConfig,
|
usernameAndPasswordConfig,
|
||||||
isUsernameAndPasswordAuthEnabled,
|
isUsernameAndPasswordAuthEnabled,
|
||||||
isExternalAuthEnabled,
|
isExternalAuthEnabled,
|
||||||
@ -29,6 +30,7 @@ data Auth = Auth
|
|||||||
{ userEntity :: Ref Entity,
|
{ userEntity :: Ref Entity,
|
||||||
externalAuthEntity :: Maybe (Ref Entity),
|
externalAuthEntity :: Maybe (Ref Entity),
|
||||||
methods :: AuthMethods,
|
methods :: AuthMethods,
|
||||||
|
signup :: Maybe SignupOptions,
|
||||||
onAuthFailedRedirectTo :: String,
|
onAuthFailedRedirectTo :: String,
|
||||||
onAuthSucceededRedirectTo :: Maybe String
|
onAuthSucceededRedirectTo :: Maybe String
|
||||||
}
|
}
|
||||||
@ -62,6 +64,11 @@ data EmailAuthConfig = EmailAuthConfig
|
|||||||
}
|
}
|
||||||
deriving (Show, Eq, Data)
|
deriving (Show, Eq, Data)
|
||||||
|
|
||||||
|
data SignupOptions = SignupOptions
|
||||||
|
{ additionalFields :: Maybe ExtImport
|
||||||
|
}
|
||||||
|
deriving (Show, Eq, Data)
|
||||||
|
|
||||||
usernameAndPasswordConfig :: UsernameAndPasswordConfig
|
usernameAndPasswordConfig :: UsernameAndPasswordConfig
|
||||||
usernameAndPasswordConfig = UsernameAndPasswordConfig Nothing
|
usernameAndPasswordConfig = UsernameAndPasswordConfig Nothing
|
||||||
|
|
||||||
|
@ -390,7 +390,11 @@ genExportedTypesDir spec =
|
|||||||
[ C.mkTmplFdWithData [relfile|src/types/index.ts|] (Just tmplData)
|
[ C.mkTmplFdWithData [relfile|src/types/index.ts|] (Just tmplData)
|
||||||
]
|
]
|
||||||
where
|
where
|
||||||
tmplData = object ["isExternalAuthEnabled" .= isExternalAuthEnabled, "isEmailAuthEnabled" .= isEmailAuthEnabled]
|
tmplData =
|
||||||
|
object
|
||||||
|
[ "isExternalAuthEnabled" .= isExternalAuthEnabled,
|
||||||
|
"isEmailAuthEnabled" .= isEmailAuthEnabled
|
||||||
|
]
|
||||||
isExternalAuthEnabled = AS.App.Auth.isExternalAuthEnabled <$> maybeAuth
|
isExternalAuthEnabled = AS.App.Auth.isExternalAuthEnabled <$> maybeAuth
|
||||||
isEmailAuthEnabled = AS.App.Auth.isEmailAuthEnabled <$> maybeAuth
|
isEmailAuthEnabled = AS.App.Auth.isEmailAuthEnabled <$> maybeAuth
|
||||||
maybeAuth = AS.App.auth $ snd $ getApp spec
|
maybeAuth = AS.App.auth $ snd $ getApp spec
|
||||||
|
@ -28,9 +28,12 @@ genLocalAuth auth
|
|||||||
sequence
|
sequence
|
||||||
[ genLoginRoute auth,
|
[ genLoginRoute auth,
|
||||||
genSignupRoute auth,
|
genSignupRoute auth,
|
||||||
genLocalAuthConfig
|
genLocalAuthConfig,
|
||||||
|
genFileCopy [relfile|auth/providers/local/types.ts|]
|
||||||
]
|
]
|
||||||
| otherwise = return []
|
| otherwise = return []
|
||||||
|
where
|
||||||
|
genFileCopy = return . C.mkSrcTmplFd
|
||||||
|
|
||||||
genLocalAuthConfig :: Generator FileDraft
|
genLocalAuthConfig :: Generator FileDraft
|
||||||
genLocalAuthConfig = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
|
genLocalAuthConfig = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
|
||||||
|
@ -10,6 +10,7 @@ import StrongPath
|
|||||||
Path',
|
Path',
|
||||||
Rel,
|
Rel,
|
||||||
reldir,
|
reldir,
|
||||||
|
reldirP,
|
||||||
relfile,
|
relfile,
|
||||||
(</>),
|
(</>),
|
||||||
)
|
)
|
||||||
@ -29,6 +30,7 @@ import Wasp.Generator.ServerGenerator.Auth.EmailAuthG (genEmailAuth)
|
|||||||
import Wasp.Generator.ServerGenerator.Auth.LocalAuthG (genLocalAuth)
|
import Wasp.Generator.ServerGenerator.Auth.LocalAuthG (genLocalAuth)
|
||||||
import Wasp.Generator.ServerGenerator.Auth.OAuthAuthG (genOAuthAuth)
|
import Wasp.Generator.ServerGenerator.Auth.OAuthAuthG (genOAuthAuth)
|
||||||
import qualified Wasp.Generator.ServerGenerator.Common as C
|
import qualified Wasp.Generator.ServerGenerator.Common as C
|
||||||
|
import Wasp.Generator.ServerGenerator.JsImport (extImportToImportJson)
|
||||||
import Wasp.Util ((<++>))
|
import Wasp.Util ((<++>))
|
||||||
import qualified Wasp.Util as Util
|
import qualified Wasp.Util as Util
|
||||||
|
|
||||||
@ -45,6 +47,7 @@ genAuth spec = case maybeAuth of
|
|||||||
genProvidersIndex auth,
|
genProvidersIndex auth,
|
||||||
genFileCopy [relfile|auth/providers/types.ts|]
|
genFileCopy [relfile|auth/providers/types.ts|]
|
||||||
]
|
]
|
||||||
|
<++> genIndexTs auth
|
||||||
<++> genLocalAuth auth
|
<++> genLocalAuth auth
|
||||||
<++> genOAuthAuth spec auth
|
<++> genOAuthAuth spec auth
|
||||||
<++> genEmailAuth spec auth
|
<++> genEmailAuth spec auth
|
||||||
@ -125,12 +128,30 @@ genUtils auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplDat
|
|||||||
"userEntityLower" .= (Util.toLowerFirst userEntityName :: String),
|
"userEntityLower" .= (Util.toLowerFirst userEntityName :: String),
|
||||||
"failureRedirectPath" .= AS.Auth.onAuthFailedRedirectTo auth,
|
"failureRedirectPath" .= AS.Auth.onAuthFailedRedirectTo auth,
|
||||||
"successRedirectPath" .= getOnAuthSucceededRedirectToOrDefault auth,
|
"successRedirectPath" .= getOnAuthSucceededRedirectToOrDefault auth,
|
||||||
"isEmailAuthEnabled" .= AS.Auth.isEmailAuthEnabled auth
|
"isEmailAuthEnabled" .= AS.Auth.isEmailAuthEnabled auth,
|
||||||
|
"additionalSignupFields" .= extImportToImportJson [reldirP|../|] additionalSignupFields
|
||||||
]
|
]
|
||||||
|
|
||||||
utilsFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'
|
utilsFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'
|
||||||
utilsFileInSrcDir = [relfile|auth/utils.ts|]
|
utilsFileInSrcDir = [relfile|auth/utils.ts|]
|
||||||
|
|
||||||
|
additionalSignupFields = AS.Auth.signup auth >>= AS.Auth.additionalFields
|
||||||
|
|
||||||
|
genIndexTs :: AS.Auth.Auth -> Generator [FileDraft]
|
||||||
|
genIndexTs auth =
|
||||||
|
return $
|
||||||
|
if isEmailAuthEnabled || isLocalAuthEnabled
|
||||||
|
then [C.mkTmplFdWithData [relfile|src/auth/index.ts|] (Just tmplData)]
|
||||||
|
else []
|
||||||
|
where
|
||||||
|
tmplData =
|
||||||
|
object
|
||||||
|
[ "isEmailAuthEnabled" .= isEmailAuthEnabled,
|
||||||
|
"isLocalAuthEnabled" .= isLocalAuthEnabled
|
||||||
|
]
|
||||||
|
isEmailAuthEnabled = AS.Auth.isEmailAuthEnabled auth
|
||||||
|
isLocalAuthEnabled = AS.Auth.isUsernameAndPasswordAuthEnabled auth
|
||||||
|
|
||||||
getOnAuthSucceededRedirectToOrDefault :: AS.Auth.Auth -> String
|
getOnAuthSucceededRedirectToOrDefault :: AS.Auth.Auth -> String
|
||||||
getOnAuthSucceededRedirectToOrDefault auth = fromMaybe "/" (AS.Auth.onAuthSucceededRedirectTo auth)
|
getOnAuthSucceededRedirectToOrDefault auth = fromMaybe "/" (AS.Auth.onAuthSucceededRedirectTo auth)
|
||||||
|
|
||||||
|
@ -136,7 +136,9 @@ npmDepsForWasp spec =
|
|||||||
-- https://github.com/wasp-lang/wasp/pull/962/ for details).
|
-- https://github.com/wasp-lang/wasp/pull/962/ for details).
|
||||||
("@prisma/client", show prismaVersion),
|
("@prisma/client", show prismaVersion),
|
||||||
("superjson", "^1.12.2"),
|
("superjson", "^1.12.2"),
|
||||||
("mitt", "3.0.0")
|
("mitt", "3.0.0"),
|
||||||
|
-- Used for Auth UI
|
||||||
|
("react-hook-form", "^7.45.4")
|
||||||
]
|
]
|
||||||
++ depsRequiredForAuth spec
|
++ depsRequiredForAuth spec
|
||||||
++ depsRequiredByTailwind spec
|
++ depsRequiredByTailwind spec
|
||||||
|
@ -50,6 +50,9 @@ spec_Analyzer = do
|
|||||||
" userEntity: User,",
|
" userEntity: User,",
|
||||||
" methods: { usernameAndPassword: {} },",
|
" methods: { usernameAndPassword: {} },",
|
||||||
" onAuthFailedRedirectTo: \"/\",",
|
" onAuthFailedRedirectTo: \"/\",",
|
||||||
|
" signup: {",
|
||||||
|
" additionalFields: import { fields } from \"@server/auth/signup.js\",",
|
||||||
|
" },",
|
||||||
" },",
|
" },",
|
||||||
" dependencies: [",
|
" dependencies: [",
|
||||||
" (\"redux\", \"^4.0.5\")",
|
" (\"redux\", \"^4.0.5\")",
|
||||||
@ -135,6 +138,12 @@ spec_Analyzer = do
|
|||||||
Auth.Auth
|
Auth.Auth
|
||||||
{ Auth.userEntity = Ref "User" :: Ref Entity,
|
{ Auth.userEntity = Ref "User" :: Ref Entity,
|
||||||
Auth.externalAuthEntity = Nothing,
|
Auth.externalAuthEntity = Nothing,
|
||||||
|
Auth.signup =
|
||||||
|
Just $
|
||||||
|
Auth.SignupOptions
|
||||||
|
{ Auth.additionalFields =
|
||||||
|
Just $ ExtImport (ExtImportField "fields") (fromJust $ SP.parseRelFileP "auth/signup.js")
|
||||||
|
},
|
||||||
Auth.methods =
|
Auth.methods =
|
||||||
Auth.AuthMethods
|
Auth.AuthMethods
|
||||||
{ Auth.usernameAndPassword = Just Auth.usernameAndPasswordConfig,
|
{ Auth.usernameAndPassword = Just Auth.usernameAndPasswordConfig,
|
||||||
|
@ -6,7 +6,7 @@ cabal-version: 2.4
|
|||||||
-- Consider using hpack, or maybe even hpack-dhall.
|
-- Consider using hpack, or maybe even hpack-dhall.
|
||||||
|
|
||||||
name: waspc
|
name: waspc
|
||||||
version: 0.11.3
|
version: 0.11.4
|
||||||
description: Please see the README on GitHub at <https://github.com/wasp-lang/wasp/waspc#readme>
|
description: Please see the README on GitHub at <https://github.com/wasp-lang/wasp/waspc#readme>
|
||||||
homepage: https://github.com/wasp-lang/wasp/waspc#readme
|
homepage: https://github.com/wasp-lang/wasp/waspc#readme
|
||||||
bug-reports: https://github.com/wasp-lang/wasp/issues
|
bug-reports: https://github.com/wasp-lang/wasp/issues
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -218,6 +218,8 @@ export function SignupPage() {
|
|||||||
|
|
||||||
It will automatically show the correct authentication providers based on your `main.wasp` file.
|
It will automatically show the correct authentication providers based on your `main.wasp` file.
|
||||||
|
|
||||||
|
Read more about customizing the signup process like adding additional fields or extra UI in the [Using Auth](/docs/auth/overview#customizing-the-signup-process) section.
|
||||||
|
|
||||||
### Forgot Password Form
|
### Forgot Password Form
|
||||||
|
|
||||||
Used with <EmailPill /> authentication.
|
Used with <EmailPill /> authentication.
|
||||||
|
6
web/prettier.config.js
Normal file
6
web/prettier.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Used to format the code in the docs
|
||||||
|
module.exports = {
|
||||||
|
trailingComma: 'es5',
|
||||||
|
semi: false,
|
||||||
|
singleQuote: true,
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user