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
|
||||
|
||||
### 🎉 [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
|
||||
|
||||
Wasp now supports PostgreSQL extensions! You can enable them in your `main.wasp` file:
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
type State,
|
||||
type CustomizationOptions,
|
||||
type ErrorMessage,
|
||||
type AdditionalSignupFields,
|
||||
} from './types'
|
||||
import { LoginSignupForm } from './internal/common/LoginSignupForm'
|
||||
import { MessageError, MessageSuccess } from './internal/Message'
|
||||
@ -39,9 +40,11 @@ export const AuthContext = createContext({
|
||||
setSuccessMessage: (successMessage: string | null) => {},
|
||||
})
|
||||
|
||||
function Auth ({ state, appearance, logo, socialLayout = 'horizontal' }: {
|
||||
function Auth ({ state, appearance, logo, socialLayout = 'horizontal', additionalSignupFields }: {
|
||||
state: State;
|
||||
} & CustomizationOptions) {
|
||||
} & CustomizationOptions & {
|
||||
additionalSignupFields?: AdditionalSignupFields;
|
||||
}) {
|
||||
const [errorMessage, setErrorMessage] = useState<ErrorMessage | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -82,6 +85,7 @@ function Auth ({ state, appearance, logo, socialLayout = 'horizontal' }: {
|
||||
<LoginSignupForm
|
||||
state={state}
|
||||
socialButtonsDirection={socialButtonsDirection}
|
||||
additionalSignupFields={additionalSignupFields}
|
||||
/>
|
||||
)}
|
||||
{=# isEmailAuthEnabled =}
|
||||
|
@ -1,17 +1,23 @@
|
||||
import Auth from './Auth'
|
||||
import { type CustomizationOptions, State } from './types'
|
||||
import {
|
||||
type CustomizationOptions,
|
||||
type AdditionalSignupFields,
|
||||
State,
|
||||
} from './types'
|
||||
|
||||
export function SignupForm({
|
||||
appearance,
|
||||
logo,
|
||||
socialLayout,
|
||||
}: CustomizationOptions) {
|
||||
additionalFields,
|
||||
}: CustomizationOptions & { additionalFields?: AdditionalSignupFields; }) {
|
||||
return (
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
state={State.Signup}
|
||||
additionalSignupFields={additionalFields}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -14,9 +14,10 @@ export const FormLabel = styled('label', {
|
||||
display: 'block',
|
||||
fontSize: '$sm',
|
||||
fontWeight: '500',
|
||||
marginBottom: '0.5rem',
|
||||
})
|
||||
|
||||
export const FormInput = styled('input', {
|
||||
const commonInputStyles = {
|
||||
display: 'block',
|
||||
lineHeight: '1.5rem',
|
||||
fontSize: '$sm',
|
||||
@ -44,7 +45,18 @@ export const FormInput = styled('input', {
|
||||
paddingBottom: '0.375rem',
|
||||
paddingLeft: '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',
|
||||
})
|
||||
|
||||
|
@ -1,10 +1,23 @@
|
||||
{{={= =}=}}
|
||||
import { useContext, type FormEvent } from 'react'
|
||||
import { styled } from '../../../../stitches.config'
|
||||
import config from '../../../../config.js'
|
||||
import { useContext } from 'react'
|
||||
import { useForm, UseFormReturn } from 'react-hook-form'
|
||||
|
||||
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 =}
|
||||
import * as SocialIcons from '../social/SocialIcons'
|
||||
import { SocialButton } from '../social/SocialButton'
|
||||
@ -97,12 +110,23 @@ const googleSignInUrl = `${config.apiUrl}{= googleSignInPath =}`
|
||||
const gitHubSignInUrl = `${config.apiUrl}{= gitHubSignInPath =}`
|
||||
{=/ 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 = ({
|
||||
state,
|
||||
socialButtonsDirection = 'horizontal',
|
||||
additionalSignupFields,
|
||||
}: {
|
||||
state: 'login' | 'signup',
|
||||
socialButtonsDirection?: 'horizontal' | 'vertical';
|
||||
state: 'login' | 'signup'
|
||||
socialButtonsDirection?: 'horizontal' | 'vertical'
|
||||
additionalSignupFields?: AdditionalSignupFields
|
||||
}) => {
|
||||
const {
|
||||
isLoading,
|
||||
@ -110,16 +134,19 @@ export const LoginSignupForm = ({
|
||||
setSuccessMessage,
|
||||
setIsLoading,
|
||||
} = useContext(AuthContext)
|
||||
const cta = state === 'login' ? 'Log in' : 'Sign up';
|
||||
const isLogin = state === 'login'
|
||||
const cta = isLogin ? 'Log in' : 'Sign up';
|
||||
{=# isAnyPasswordBasedAuthEnabled =}
|
||||
const history = useHistory();
|
||||
const onErrorHandler = (error) => {
|
||||
setErrorMessage({ title: error.message, description: error.data?.data?.message })
|
||||
};
|
||||
{=/ isAnyPasswordBasedAuthEnabled =}
|
||||
const hookForm = useForm<LoginSignupFormFields>()
|
||||
const { register, formState: { errors }, handleSubmit: hookFormHandleSubmit } = hookForm
|
||||
{=# isUsernameAndPasswordAuthEnabled =}
|
||||
const { handleSubmit, usernameFieldVal, passwordFieldVal, setUsernameFieldVal, setPasswordFieldVal } = useUsernameAndPassword({
|
||||
isLogin: state === 'login',
|
||||
const { handleSubmit } = useUsernameAndPassword({
|
||||
isLogin,
|
||||
onError: onErrorHandler,
|
||||
onSuccess() {
|
||||
history.push('{= onAuthSucceededRedirectTo =}')
|
||||
@ -127,10 +154,11 @@ export const LoginSignupForm = ({
|
||||
});
|
||||
{=/ isUsernameAndPasswordAuthEnabled =}
|
||||
{=# isEmailAuthEnabled =}
|
||||
const { handleSubmit, emailFieldVal, passwordFieldVal, setEmailFieldVal, setPasswordFieldVal } = useEmail({
|
||||
isLogin: state === 'login',
|
||||
const { handleSubmit } = useEmail({
|
||||
isLogin,
|
||||
onError: onErrorHandler,
|
||||
showEmailVerificationPending() {
|
||||
hookForm.reset()
|
||||
setSuccessMessage(`You've signed up successfully! Check your email for the confirmation link.`)
|
||||
},
|
||||
onLoginSuccess() {
|
||||
@ -145,13 +173,12 @@ export const LoginSignupForm = ({
|
||||
});
|
||||
{=/ isEmailAuthEnabled =}
|
||||
{=# isAnyPasswordBasedAuthEnabled =}
|
||||
async function onSubmit (event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
async function onSubmit (data) {
|
||||
setIsLoading(true);
|
||||
setErrorMessage(null);
|
||||
setSuccessMessage(null);
|
||||
try {
|
||||
await handleSubmit();
|
||||
await handleSubmit(data);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@ -184,41 +211,49 @@ export const LoginSignupForm = ({
|
||||
</OrContinueWith>
|
||||
{=/ areBothSocialAndPasswordBasedAuthEnabled =}
|
||||
{=# isAnyPasswordBasedAuthEnabled =}
|
||||
<Form onSubmit={onSubmit}>
|
||||
<Form onSubmit={hookFormHandleSubmit(onSubmit)}>
|
||||
{=# isUsernameAndPasswordAuthEnabled =}
|
||||
<FormItemGroup>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormInput
|
||||
{...register('username', {
|
||||
required: 'Username is required',
|
||||
})}
|
||||
type="text"
|
||||
required
|
||||
value={usernameFieldVal}
|
||||
onChange={e => setUsernameFieldVal(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||
</FormItemGroup>
|
||||
{=/ isUsernameAndPasswordAuthEnabled =}
|
||||
{=# isEmailAuthEnabled =}
|
||||
<FormItemGroup>
|
||||
<FormLabel>E-mail</FormLabel>
|
||||
<FormInput
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
})}
|
||||
type="email"
|
||||
required
|
||||
value={emailFieldVal}
|
||||
onChange={e => setEmailFieldVal(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.email && <FormError>{errors.email.message}</FormError>}
|
||||
</FormItemGroup>
|
||||
{=/ isEmailAuthEnabled =}
|
||||
<FormItemGroup>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormInput
|
||||
{...register('password', {
|
||||
required: 'Password is required',
|
||||
})}
|
||||
type="password"
|
||||
required
|
||||
value={passwordFieldVal}
|
||||
onChange={e => setPasswordFieldVal(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.password && <FormError>{errors.password.message}</FormError>}
|
||||
</FormItemGroup>
|
||||
<AdditionalFormFields
|
||||
hookForm={hookForm}
|
||||
formState={{ isLoading }}
|
||||
additionalSignupFields={additionalSignupFields}
|
||||
/>
|
||||
<FormItemGroup>
|
||||
<SubmitButton type="submit" disabled={isLoading}>{cta}</SubmitButton>
|
||||
</FormItemGroup>
|
||||
@ -226,3 +261,76 @@ export const LoginSignupForm = ({
|
||||
{=/ 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 { useState, useContext, FormEvent } from 'react'
|
||||
import { Form, FormItemGroup, FormLabel, FormInput, SubmitButton } from '../Form'
|
||||
import { Form, FormItemGroup, FormLabel, FormInput, SubmitButton, FormError } from '../Form'
|
||||
import { AuthContext } from '../../Auth'
|
||||
|
||||
export const ForgotPasswordForm = () => {
|
||||
const { register, handleSubmit, reset, formState: { errors } } = useForm<{ email: string }>()
|
||||
const { isLoading, setErrorMessage, setSuccessMessage, setIsLoading } = useContext(AuthContext)
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
const onSubmit = async (data) => {
|
||||
setIsLoading(true)
|
||||
setErrorMessage(null)
|
||||
setSuccessMessage(null)
|
||||
try {
|
||||
await requestPasswordReset({ email })
|
||||
await requestPasswordReset(data)
|
||||
reset()
|
||||
setSuccessMessage('Check your email for a password reset link.')
|
||||
setEmail('')
|
||||
} catch (error) {
|
||||
setErrorMessage({
|
||||
title: error.message,
|
||||
@ -28,16 +28,17 @@ export const ForgotPasswordForm = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form onSubmit={onSubmit}>
|
||||
<Form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormItemGroup>
|
||||
<FormLabel>E-mail</FormLabel>
|
||||
<FormInput
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
})}
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.email && <FormError>{errors.email.message}</FormError>}
|
||||
</FormItemGroup>
|
||||
<FormItemGroup>
|
||||
<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 { useState, useContext, FormEvent } from 'react'
|
||||
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'
|
||||
|
||||
export const ResetPasswordForm = () => {
|
||||
const { register, handleSubmit, reset, formState: { errors } } = useForm<{ password: string; passwordConfirmation: string }>()
|
||||
const { isLoading, setErrorMessage, setSuccessMessage, setIsLoading } = useContext(AuthContext)
|
||||
const location = useLocation()
|
||||
const token = new URLSearchParams(location.search).get('token')
|
||||
const [password, setPassword] = useState('')
|
||||
const [passwordConfirmation, setPasswordConfirmation] = useState('')
|
||||
|
||||
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
if (!token) {
|
||||
setErrorMessage({
|
||||
title:
|
||||
@ -22,7 +19,7 @@ export const ResetPasswordForm = () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (!password || password !== passwordConfirmation) {
|
||||
if (!data.password || data.password !== data.passwordConfirmation) {
|
||||
setErrorMessage({ title: `Passwords don't match!` })
|
||||
return
|
||||
}
|
||||
@ -31,10 +28,9 @@ export const ResetPasswordForm = () => {
|
||||
setErrorMessage(null)
|
||||
setSuccessMessage(null)
|
||||
try {
|
||||
await resetPassword({ password, token })
|
||||
await resetPassword({ password: data.password, token })
|
||||
reset()
|
||||
setSuccessMessage('Your password has been reset.')
|
||||
setPassword('')
|
||||
setPasswordConfirmation('')
|
||||
} catch (error) {
|
||||
setErrorMessage({
|
||||
title: error.message,
|
||||
@ -47,26 +43,32 @@ export const ResetPasswordForm = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form onSubmit={onSubmit}>
|
||||
<Form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormItemGroup>
|
||||
<FormLabel>New password</FormLabel>
|
||||
<FormInput
|
||||
{...register('password', {
|
||||
required: 'Password is required',
|
||||
})}
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.passwordConfirmation && (
|
||||
<FormError>{errors.passwordConfirmation.message}</FormError>
|
||||
)}
|
||||
</FormItemGroup>
|
||||
<FormItemGroup>
|
||||
<FormLabel>Confirm new password</FormLabel>
|
||||
<FormInput
|
||||
{...register('passwordConfirmation', {
|
||||
required: 'Password confirmation is required',
|
||||
})}
|
||||
type="password"
|
||||
required
|
||||
value={passwordConfirmation}
|
||||
onChange={(e) => setPasswordConfirmation(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.passwordConfirmation && (
|
||||
<FormError>{errors.passwordConfirmation.message}</FormError>
|
||||
)}
|
||||
</FormItemGroup>
|
||||
<FormItemGroup>
|
||||
<SubmitButton type="submit" disabled={isLoading}>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useState } from 'react'
|
||||
import { signup } from '../../../email/actions/signup'
|
||||
import { login } from '../../../email/actions/login'
|
||||
|
||||
@ -15,25 +14,20 @@ export function useEmail({
|
||||
isLogin: boolean
|
||||
isEmailVerificationRequired: boolean
|
||||
}) {
|
||||
const [emailFieldVal, setEmailFieldVal] = useState('')
|
||||
const [passwordFieldVal, setPasswordFieldVal] = useState('')
|
||||
|
||||
async function handleSubmit() {
|
||||
async function handleSubmit(data) {
|
||||
try {
|
||||
if (isLogin) {
|
||||
await login({ email: emailFieldVal, password: passwordFieldVal })
|
||||
await login(data)
|
||||
onLoginSuccess()
|
||||
} else {
|
||||
await signup({ email: emailFieldVal, password: passwordFieldVal })
|
||||
await signup(data)
|
||||
if (isEmailVerificationRequired) {
|
||||
showEmailVerificationPending()
|
||||
} else {
|
||||
await login({ email: emailFieldVal, password: passwordFieldVal })
|
||||
await login(data)
|
||||
onLoginSuccess()
|
||||
}
|
||||
}
|
||||
setEmailFieldVal('')
|
||||
setPasswordFieldVal('')
|
||||
} catch (err: unknown) {
|
||||
onError(err as Error)
|
||||
}
|
||||
@ -41,9 +35,5 @@ export function useEmail({
|
||||
|
||||
return {
|
||||
handleSubmit,
|
||||
emailFieldVal,
|
||||
passwordFieldVal,
|
||||
setEmailFieldVal,
|
||||
setPasswordFieldVal,
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useState } from 'react'
|
||||
import signup from '../../../signup'
|
||||
import login from '../../../login'
|
||||
|
||||
@ -11,21 +10,13 @@ export function useUsernameAndPassword({
|
||||
onSuccess: () => void
|
||||
isLogin: boolean
|
||||
}) {
|
||||
const [usernameFieldVal, setUsernameFieldVal] = useState('')
|
||||
const [passwordFieldVal, setPasswordFieldVal] = useState('')
|
||||
|
||||
async function handleSubmit() {
|
||||
async function handleSubmit(data) {
|
||||
try {
|
||||
if (!isLogin) {
|
||||
await signup({
|
||||
username: usernameFieldVal,
|
||||
password: passwordFieldVal,
|
||||
})
|
||||
await signup(data)
|
||||
}
|
||||
await login(usernameFieldVal, passwordFieldVal)
|
||||
await login(data.username, data.password)
|
||||
|
||||
setUsernameFieldVal('')
|
||||
setPasswordFieldVal('')
|
||||
onSuccess()
|
||||
} catch (err: unknown) {
|
||||
onError(err as Error)
|
||||
@ -34,9 +25,5 @@ export function useUsernameAndPassword({
|
||||
|
||||
return {
|
||||
handleSubmit,
|
||||
usernameFieldVal,
|
||||
passwordFieldVal,
|
||||
setUsernameFieldVal,
|
||||
setPasswordFieldVal,
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
{{={= =}=}}
|
||||
import { createTheme } from '@stitches/react'
|
||||
import { UseFormReturn, RegisterOptions } from 'react-hook-form'
|
||||
import type { LoginSignupFormFields } from './internal/common/LoginSignupForm'
|
||||
|
||||
export enum State {
|
||||
Login = 'login',
|
||||
@ -21,3 +23,23 @@ export type ErrorMessage = {
|
||||
title: 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',
|
||||
gray400: '#f0f0f0',
|
||||
red: '#FED7D7',
|
||||
darkRed: '#fa3838',
|
||||
green: '#C6F6D5',
|
||||
|
||||
brand: '$waspYellow',
|
||||
@ -23,6 +24,7 @@ export const {
|
||||
|
||||
submitButtonText: 'black',
|
||||
|
||||
formErrorText: '$darkRed',
|
||||
},
|
||||
fontSizes: {
|
||||
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,
|
||||
} from "../../utils.js";
|
||||
import { GetVerificationEmailContentFn } from './types.js';
|
||||
import { validateAndGetAdditionalFields } from '../../utils.js'
|
||||
|
||||
export function getSignupRoute({
|
||||
fromField,
|
||||
@ -41,8 +42,11 @@ export function getSignupRoute({
|
||||
}
|
||||
await deleteUser(existingUser);
|
||||
}
|
||||
|
||||
const additionalFields = await validateAndGetAdditionalFields(userFields);
|
||||
|
||||
const user = await createUser({
|
||||
...additionalFields,
|
||||
email: userFields.email,
|
||||
password: userFields.password,
|
||||
});
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { createDefineAdditionalSignupFieldsFn } from '../types.js'
|
||||
|
||||
export type GetVerificationEmailContentFn = (params: { verificationLink: string }) => EmailContent;
|
||||
|
||||
export type GetPasswordResetEmailContentFn = (params: { passwordResetLink: string }) => EmailContent;
|
||||
@ -11,3 +13,5 @@ type EmailContent = {
|
||||
export const tokenVerificationErrors = {
|
||||
TokenExpiredError: 'TokenExpiredError',
|
||||
};
|
||||
|
||||
export const defineAdditionalSignupFields = createDefineAdditionalSignupFieldsFn<"email" | "password">()
|
||||
|
@ -1,11 +1,15 @@
|
||||
{{={= =}=}}
|
||||
import { handleRejection } from '../../../utils.js'
|
||||
import { createUser } from '../../utils.js'
|
||||
import { validateAndGetAdditionalFields } from '../../utils.js'
|
||||
|
||||
export default handleRejection(async (req, res) => {
|
||||
const userFields = req.body || {}
|
||||
|
||||
const additionalFields = await validateAndGetAdditionalFields(userFields)
|
||||
|
||||
await createUser({
|
||||
...additionalFields,
|
||||
username: userFields.username,
|
||||
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 = {
|
||||
// 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 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 { Email } from '../email/core/types.js';
|
||||
{=/ 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']
|
||||
|
||||
@ -218,4 +231,23 @@ function rethrowPossiblePrismaError(e: unknown): void {
|
||||
|
||||
function throwValidationError(message: string): void {
|
||||
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",
|
||||
"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",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"superjson": "^1.12.2"
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
app waspBuild {
|
||||
db: { system: PostgreSQL },
|
||||
wasp: {
|
||||
version: "^0.11.3"
|
||||
version: "^0.11.4"
|
||||
},
|
||||
title: "waspBuild"
|
||||
}
|
||||
|
@ -284,7 +284,7 @@
|
||||
"file",
|
||||
"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",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"superjson": "^1.12.2"
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
app waspCompile {
|
||||
wasp: {
|
||||
version: "^0.11.3"
|
||||
version: "^0.11.4"
|
||||
},
|
||||
title: "waspCompile"
|
||||
}
|
||||
|
@ -179,14 +179,14 @@
|
||||
"file",
|
||||
"server/src/auth/providers/types.ts"
|
||||
],
|
||||
"9859bf91e0abe0aaadf7b8c74c573607f6085a5414071836cef6ba84b2bebb69"
|
||||
"323555d76755fe32b21084f063caf931faabcb5937c279cc706bbecad3361d43"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/auth/utils.ts"
|
||||
],
|
||||
"b611de9a6b546f6f1cec4497a4cb525150399131dfba32b53ba34afb04e62e96"
|
||||
"1cfb0c8095ba0ed7686229b22e8a9edd971526d50660f88d2cbb988af9b5af30"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -578,7 +578,7 @@
|
||||
"file",
|
||||
"web-app/package.json"
|
||||
],
|
||||
"fc5d4d8e4a3ed36972eac6891122dfd3d8edb4074a8df041162dcb38bb6f6d2d"
|
||||
"7857f3a9fc85cd39f16dbd9362bccc5e5662b5b216da45b843ebca6bba4142b5"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -655,7 +655,7 @@
|
||||
"file",
|
||||
"web-app/src/auth/forms/Auth.tsx"
|
||||
],
|
||||
"d40cf940a499fdd4b137dcf9f3cd4fbe0bbab4b7c44eb7819b41daeaa861050b"
|
||||
"fc6c204f73999f556eab441772e66dd4dbd433bd69aa2c9e64e713fb2921a886"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -669,14 +669,14 @@
|
||||
"file",
|
||||
"web-app/src/auth/forms/Signup.tsx"
|
||||
],
|
||||
"cacd5348e84d42bc142f6c6e00051a8e86f6b17eb037b2772772f073925bf570"
|
||||
"a38124a9a250a603ef6d04dbcb46c04084ace676e9de5fc31f229405d87f47d4"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/forms/internal/Form.tsx"
|
||||
],
|
||||
"ce6b409fda73d88e762b27aef7038618961e61e6ef8e5f66912325ab88a8223e"
|
||||
"b9b21954b919f173b751c0078aed8303cc15d88df9e9874228efcae7976f26cd"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -690,7 +690,7 @@
|
||||
"file",
|
||||
"web-app/src/auth/forms/internal/common/LoginSignupForm.tsx"
|
||||
],
|
||||
"c92bab325f51159c3d1bb285c7e807acbb85069c5b16afc3a35fbd0121c91b6a"
|
||||
"f211f57dca3f10f08a3e618d27bf8006d26f6832bb7e40f8a22ae44f2d42531e"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -711,7 +711,7 @@
|
||||
"file",
|
||||
"web-app/src/auth/forms/types.ts"
|
||||
],
|
||||
"c4066fbd39ec20a3d43be9f9d5762d555dbc006057579ac168a67b2678918a13"
|
||||
"992ca4b2c8e30536636143c556e6bdcc5d5d0d86c1eb2e119171e25d5c33b4e3"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -935,7 +935,7 @@
|
||||
"file",
|
||||
"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 = {
|
||||
// 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 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 { type Prisma } from '@prisma/client';
|
||||
|
||||
import { createDefineAdditionalSignupFieldsFn } from './providers/types.js'
|
||||
const _waspAdditionalSignupFieldsConfig = {} as ReturnType<
|
||||
ReturnType<typeof createDefineAdditionalSignupFieldsFn<never>>
|
||||
>
|
||||
|
||||
type UserId = User['id']
|
||||
|
||||
export const contextWithUserEntity = {
|
||||
@ -73,4 +78,23 @@ function rethrowPossiblePrismaError(e: unknown): void {
|
||||
|
||||
function throwValidationError(message: string): void {
|
||||
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",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"redux": "^4.0.5",
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
type State,
|
||||
type CustomizationOptions,
|
||||
type ErrorMessage,
|
||||
type AdditionalSignupFields,
|
||||
} from './types'
|
||||
import { LoginSignupForm } from './internal/common/LoginSignupForm'
|
||||
import { MessageError, MessageSuccess } from './internal/Message'
|
||||
@ -33,9 +34,11 @@ export const AuthContext = createContext({
|
||||
setSuccessMessage: (successMessage: string | null) => {},
|
||||
})
|
||||
|
||||
function Auth ({ state, appearance, logo, socialLayout = 'horizontal' }: {
|
||||
function Auth ({ state, appearance, logo, socialLayout = 'horizontal', additionalSignupFields }: {
|
||||
state: State;
|
||||
} & CustomizationOptions) {
|
||||
} & CustomizationOptions & {
|
||||
additionalSignupFields?: AdditionalSignupFields;
|
||||
}) {
|
||||
const [errorMessage, setErrorMessage] = useState<ErrorMessage | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -71,6 +74,7 @@ function Auth ({ state, appearance, logo, socialLayout = 'horizontal' }: {
|
||||
<LoginSignupForm
|
||||
state={state}
|
||||
socialButtonsDirection={socialButtonsDirection}
|
||||
additionalSignupFields={additionalSignupFields}
|
||||
/>
|
||||
)}
|
||||
</AuthContext.Provider>
|
||||
|
@ -1,17 +1,23 @@
|
||||
import Auth from './Auth'
|
||||
import { type CustomizationOptions, State } from './types'
|
||||
import {
|
||||
type CustomizationOptions,
|
||||
type AdditionalSignupFields,
|
||||
State,
|
||||
} from './types'
|
||||
|
||||
export function SignupForm({
|
||||
appearance,
|
||||
logo,
|
||||
socialLayout,
|
||||
}: CustomizationOptions) {
|
||||
additionalFields,
|
||||
}: CustomizationOptions & { additionalFields?: AdditionalSignupFields; }) {
|
||||
return (
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
state={State.Signup}
|
||||
additionalSignupFields={additionalFields}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -14,9 +14,10 @@ export const FormLabel = styled('label', {
|
||||
display: 'block',
|
||||
fontSize: '$sm',
|
||||
fontWeight: '500',
|
||||
marginBottom: '0.5rem',
|
||||
})
|
||||
|
||||
export const FormInput = styled('input', {
|
||||
const commonInputStyles = {
|
||||
display: 'block',
|
||||
lineHeight: '1.5rem',
|
||||
fontSize: '$sm',
|
||||
@ -44,7 +45,18 @@ export const FormInput = styled('input', {
|
||||
paddingBottom: '0.375rem',
|
||||
paddingLeft: '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',
|
||||
})
|
||||
|
||||
|
@ -1,9 +1,22 @@
|
||||
import { useContext, type FormEvent } from 'react'
|
||||
import { styled } from '../../../../stitches.config'
|
||||
import config from '../../../../config.js'
|
||||
import { useContext } from 'react'
|
||||
import { useForm, UseFormReturn } from 'react-hook-form'
|
||||
|
||||
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 { SocialButton } from '../social/SocialButton'
|
||||
|
||||
@ -46,12 +59,18 @@ const SocialAuthButtons = styled('div', {
|
||||
})
|
||||
const googleSignInUrl = `${config.apiUrl}/auth/google/login`
|
||||
|
||||
export type LoginSignupFormFields = {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export const LoginSignupForm = ({
|
||||
state,
|
||||
socialButtonsDirection = 'horizontal',
|
||||
additionalSignupFields,
|
||||
}: {
|
||||
state: 'login' | 'signup',
|
||||
socialButtonsDirection?: 'horizontal' | 'vertical';
|
||||
state: 'login' | 'signup'
|
||||
socialButtonsDirection?: 'horizontal' | 'vertical'
|
||||
additionalSignupFields?: AdditionalSignupFields
|
||||
}) => {
|
||||
const {
|
||||
isLoading,
|
||||
@ -59,7 +78,10 @@ export const LoginSignupForm = ({
|
||||
setSuccessMessage,
|
||||
setIsLoading,
|
||||
} = 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 (<>
|
||||
<SocialAuth>
|
||||
@ -71,3 +93,76 @@ export const LoginSignupForm = ({
|
||||
</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 { UseFormReturn, RegisterOptions } from 'react-hook-form'
|
||||
import type { LoginSignupFormFields } from './internal/common/LoginSignupForm'
|
||||
|
||||
export enum State {
|
||||
Login = 'login',
|
||||
@ -15,3 +17,23 @@ export type ErrorMessage = {
|
||||
title: 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',
|
||||
gray400: '#f0f0f0',
|
||||
red: '#FED7D7',
|
||||
darkRed: '#fa3838',
|
||||
green: '#C6F6D5',
|
||||
|
||||
brand: '$waspYellow',
|
||||
@ -23,6 +24,7 @@ export const {
|
||||
|
||||
submitButtonText: 'black',
|
||||
|
||||
formErrorText: '$darkRed',
|
||||
},
|
||||
fontSizes: {
|
||||
sm: '0.875rem'
|
||||
|
@ -1,7 +1,7 @@
|
||||
app waspComplexTest {
|
||||
db: { system: PostgreSQL },
|
||||
wasp: {
|
||||
version: "^0.11.3"
|
||||
version: "^0.11.4"
|
||||
},
|
||||
auth: {
|
||||
userEntity: User,
|
||||
|
@ -326,7 +326,7 @@
|
||||
"file",
|
||||
"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",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"superjson": "^1.12.2"
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
app waspJob {
|
||||
db: { system: PostgreSQL },
|
||||
wasp: {
|
||||
version: "^0.11.3"
|
||||
version: "^0.11.4"
|
||||
},
|
||||
title: "waspJob"
|
||||
}
|
||||
|
@ -284,7 +284,7 @@
|
||||
"file",
|
||||
"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",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"superjson": "^1.12.2"
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
app waspMigrate {
|
||||
wasp: {
|
||||
version: "^0.11.3"
|
||||
version: "^0.11.4"
|
||||
},
|
||||
title: "waspMigrate"
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
app waspNew {
|
||||
wasp: {
|
||||
version: "^0.11.3"
|
||||
version: "^0.11.4"
|
||||
},
|
||||
title: "waspNew"
|
||||
}
|
||||
|
@ -12,7 +12,13 @@ app crudTesting {
|
||||
usernameAndPassword: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login",
|
||||
signup: {
|
||||
additionalFields: import { fields } from "@server/auth.js",
|
||||
},
|
||||
},
|
||||
dependencies: [
|
||||
("zod", "^3.22.2")
|
||||
],
|
||||
db: {
|
||||
system: PostgreSQL
|
||||
}
|
||||
@ -44,6 +50,7 @@ entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
address String?
|
||||
tasks Task[]
|
||||
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 { Link, routes } from "@wasp/router";
|
||||
import logout from "@wasp/auth/logout";
|
||||
|
||||
import { tasks as tasksCrud } from "@wasp/crud/tasks";
|
||||
import { User } from "@wasp/entities";
|
||||
@ -45,7 +46,7 @@ const MainPage = ({ user }: { user: User }) => {
|
||||
setEditTaskTitle("");
|
||||
}
|
||||
|
||||
function handleStartEditing(task: Task) {
|
||||
function handleStartEditing(task: { id: number; title: string }) {
|
||||
setIsEditing(task.id);
|
||||
setEditTaskTitle(task.title);
|
||||
}
|
||||
@ -122,6 +123,7 @@ const MainPage = ({ user }: { user: User }) => {
|
||||
Create task
|
||||
</button>
|
||||
</form>
|
||||
<button onClick={logout}>Logout</button>
|
||||
</main>
|
||||
</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 = () => {
|
||||
return (
|
||||
<div className="container">
|
||||
<main>
|
||||
<h1>Signup</h1>
|
||||
<SignupForm />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
<SignupForm
|
||||
additionalFields={[
|
||||
{
|
||||
name: 'address',
|
||||
label: 'Address',
|
||||
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 { App } from './App'
|
||||
import { getMe } from '@wasp/auth/useAuth'
|
||||
import type { User } from '@wasp/auth/types'
|
||||
|
||||
test('areThereAnyTasks', () => {
|
||||
expect(areThereAnyTasks([])).toBe(false)
|
||||
@ -38,8 +39,9 @@ const mockUser = {
|
||||
email: 'elon@tesla.com',
|
||||
isEmailVerified: false,
|
||||
emailVerificationSentAt: null,
|
||||
passwordResetSentAt: null
|
||||
}
|
||||
passwordResetSentAt: null,
|
||||
address: null,
|
||||
} satisfies User
|
||||
|
||||
test('handles multiple mock data sources', async () => {
|
||||
mockQuery(getMe, mockUser)
|
||||
|
@ -7,6 +7,7 @@ import { getTotalTaskCountMessage } from './helpers'
|
||||
|
||||
import appearance from './appearance'
|
||||
import todoLogo from '../../todoLogo.png'
|
||||
import { FormItemGroup } from '@wasp/auth/forms/internal/Form'
|
||||
|
||||
const Signup = () => {
|
||||
const { data: numTasks } = useQuery(getNumTasks)
|
||||
@ -20,6 +21,22 @@ const Signup = () => {
|
||||
appearance={appearance}
|
||||
logo={todoLogo}
|
||||
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 />
|
||||
<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,
|
||||
},
|
||||
},
|
||||
signup: {
|
||||
additionalFields: import { fields } from "@server/auth/signup.js",
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login",
|
||||
onAuthSucceededRedirectTo: "/profile"
|
||||
},
|
||||
@ -79,6 +82,7 @@ entity User {=psl
|
||||
externalAuthAssociations SocialLogin[]
|
||||
// Business logic
|
||||
tasks Task[]
|
||||
address String?
|
||||
psl=}
|
||||
|
||||
entity SocialLogin {=psl
|
||||
|
@ -6,6 +6,7 @@ module Wasp.AppSpec.App.Auth
|
||||
AuthMethods (..),
|
||||
ExternalAuthConfig (..),
|
||||
EmailAuthConfig (..),
|
||||
SignupOptions (..),
|
||||
usernameAndPasswordConfig,
|
||||
isUsernameAndPasswordAuthEnabled,
|
||||
isExternalAuthEnabled,
|
||||
@ -29,6 +30,7 @@ data Auth = Auth
|
||||
{ userEntity :: Ref Entity,
|
||||
externalAuthEntity :: Maybe (Ref Entity),
|
||||
methods :: AuthMethods,
|
||||
signup :: Maybe SignupOptions,
|
||||
onAuthFailedRedirectTo :: String,
|
||||
onAuthSucceededRedirectTo :: Maybe String
|
||||
}
|
||||
@ -62,6 +64,11 @@ data EmailAuthConfig = EmailAuthConfig
|
||||
}
|
||||
deriving (Show, Eq, Data)
|
||||
|
||||
data SignupOptions = SignupOptions
|
||||
{ additionalFields :: Maybe ExtImport
|
||||
}
|
||||
deriving (Show, Eq, Data)
|
||||
|
||||
usernameAndPasswordConfig :: UsernameAndPasswordConfig
|
||||
usernameAndPasswordConfig = UsernameAndPasswordConfig Nothing
|
||||
|
||||
|
@ -390,7 +390,11 @@ genExportedTypesDir spec =
|
||||
[ C.mkTmplFdWithData [relfile|src/types/index.ts|] (Just tmplData)
|
||||
]
|
||||
where
|
||||
tmplData = object ["isExternalAuthEnabled" .= isExternalAuthEnabled, "isEmailAuthEnabled" .= isEmailAuthEnabled]
|
||||
tmplData =
|
||||
object
|
||||
[ "isExternalAuthEnabled" .= isExternalAuthEnabled,
|
||||
"isEmailAuthEnabled" .= isEmailAuthEnabled
|
||||
]
|
||||
isExternalAuthEnabled = AS.App.Auth.isExternalAuthEnabled <$> maybeAuth
|
||||
isEmailAuthEnabled = AS.App.Auth.isEmailAuthEnabled <$> maybeAuth
|
||||
maybeAuth = AS.App.auth $ snd $ getApp spec
|
||||
|
@ -28,9 +28,12 @@ genLocalAuth auth
|
||||
sequence
|
||||
[ genLoginRoute auth,
|
||||
genSignupRoute auth,
|
||||
genLocalAuthConfig
|
||||
genLocalAuthConfig,
|
||||
genFileCopy [relfile|auth/providers/local/types.ts|]
|
||||
]
|
||||
| otherwise = return []
|
||||
where
|
||||
genFileCopy = return . C.mkSrcTmplFd
|
||||
|
||||
genLocalAuthConfig :: Generator FileDraft
|
||||
genLocalAuthConfig = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
|
||||
|
@ -10,6 +10,7 @@ import StrongPath
|
||||
Path',
|
||||
Rel,
|
||||
reldir,
|
||||
reldirP,
|
||||
relfile,
|
||||
(</>),
|
||||
)
|
||||
@ -29,6 +30,7 @@ import Wasp.Generator.ServerGenerator.Auth.EmailAuthG (genEmailAuth)
|
||||
import Wasp.Generator.ServerGenerator.Auth.LocalAuthG (genLocalAuth)
|
||||
import Wasp.Generator.ServerGenerator.Auth.OAuthAuthG (genOAuthAuth)
|
||||
import qualified Wasp.Generator.ServerGenerator.Common as C
|
||||
import Wasp.Generator.ServerGenerator.JsImport (extImportToImportJson)
|
||||
import Wasp.Util ((<++>))
|
||||
import qualified Wasp.Util as Util
|
||||
|
||||
@ -45,6 +47,7 @@ genAuth spec = case maybeAuth of
|
||||
genProvidersIndex auth,
|
||||
genFileCopy [relfile|auth/providers/types.ts|]
|
||||
]
|
||||
<++> genIndexTs auth
|
||||
<++> genLocalAuth auth
|
||||
<++> genOAuthAuth spec auth
|
||||
<++> genEmailAuth spec auth
|
||||
@ -125,12 +128,30 @@ genUtils auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplDat
|
||||
"userEntityLower" .= (Util.toLowerFirst userEntityName :: String),
|
||||
"failureRedirectPath" .= AS.Auth.onAuthFailedRedirectTo 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 = [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 auth = fromMaybe "/" (AS.Auth.onAuthSucceededRedirectTo auth)
|
||||
|
||||
|
@ -136,7 +136,9 @@ npmDepsForWasp spec =
|
||||
-- https://github.com/wasp-lang/wasp/pull/962/ for details).
|
||||
("@prisma/client", show prismaVersion),
|
||||
("superjson", "^1.12.2"),
|
||||
("mitt", "3.0.0")
|
||||
("mitt", "3.0.0"),
|
||||
-- Used for Auth UI
|
||||
("react-hook-form", "^7.45.4")
|
||||
]
|
||||
++ depsRequiredForAuth spec
|
||||
++ depsRequiredByTailwind spec
|
||||
|
@ -50,6 +50,9 @@ spec_Analyzer = do
|
||||
" userEntity: User,",
|
||||
" methods: { usernameAndPassword: {} },",
|
||||
" onAuthFailedRedirectTo: \"/\",",
|
||||
" signup: {",
|
||||
" additionalFields: import { fields } from \"@server/auth/signup.js\",",
|
||||
" },",
|
||||
" },",
|
||||
" dependencies: [",
|
||||
" (\"redux\", \"^4.0.5\")",
|
||||
@ -135,6 +138,12 @@ spec_Analyzer = do
|
||||
Auth.Auth
|
||||
{ Auth.userEntity = Ref "User" :: Ref Entity,
|
||||
Auth.externalAuthEntity = Nothing,
|
||||
Auth.signup =
|
||||
Just $
|
||||
Auth.SignupOptions
|
||||
{ Auth.additionalFields =
|
||||
Just $ ExtImport (ExtImportField "fields") (fromJust $ SP.parseRelFileP "auth/signup.js")
|
||||
},
|
||||
Auth.methods =
|
||||
Auth.AuthMethods
|
||||
{ Auth.usernameAndPassword = Just Auth.usernameAndPasswordConfig,
|
||||
|
@ -6,7 +6,7 @@ cabal-version: 2.4
|
||||
-- Consider using hpack, or maybe even hpack-dhall.
|
||||
|
||||
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>
|
||||
homepage: https://github.com/wasp-lang/wasp/waspc#readme
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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