Signup Customisation [RFC] (#1395)

This commit is contained in:
Mihovil Ilakovac 2023-09-12 13:19:47 +02:00 committed by GitHub
parent cb88dfc618
commit 99c9021f82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 1469 additions and 196 deletions

View File

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

View 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 =}

View File

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

View File

@ -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',
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
{{={= =}=}}
{=# isEmailAuthEnabled =}
export { defineAdditionalSignupFields } from './providers/email/types.js';
{=/ isEmailAuthEnabled =}
{=# isLocalAuthEnabled =}
export { defineAdditionalSignupFields } from './providers/local/types.js';
{=/ isLocalAuthEnabled =}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
import { createDefineAdditionalSignupFieldsFn } from '../types.js'
export const defineAdditionalSignupFields = createDefineAdditionalSignupFieldsFn<"username" | "password">()

View File

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

View File

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

View File

@ -270,7 +270,7 @@
"file",
"web-app/package.json"
],
"ee9766b7c88b3d4ac36b1dd4b3237ea750e4c26ad27bcd18006225707d042e18"
"8bacfb3d4e24886405c2a8fa94be0be7d3ec4b882063e5f22667893811cd4371"
],
[
[

View File

@ -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"}]}}

View File

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

View File

@ -1,7 +1,7 @@
app waspBuild {
db: { system: PostgreSQL },
wasp: {
version: "^0.11.3"
version: "^0.11.4"
},
title: "waspBuild"
}

View File

@ -284,7 +284,7 @@
"file",
"web-app/package.json"
],
"8a2249588d7cf9ac7c6c8cb979727c264446581f60c7062e5852aef4c1d8a675"
"c2b7000a7380cce059cdafe67fa755a8e5f4d1de5e13eb11e545a3ee4d32db0d"
],
[
[

View File

@ -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"}]}}

View File

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

View File

@ -1,6 +1,6 @@
app waspCompile {
wasp: {
version: "^0.11.3"
version: "^0.11.4"
},
title: "waspCompile"
}

View File

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

View File

@ -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"}]}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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',
})

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
app waspComplexTest {
db: { system: PostgreSQL },
wasp: {
version: "^0.11.3"
version: "^0.11.4"
},
auth: {
userEntity: User,

View File

@ -326,7 +326,7 @@
"file",
"web-app/package.json"
],
"57da965d01f5d39d74c6ca91f00d84ba5b6c78660ee837266b2622e410e4fd8e"
"adcc3a24462553bd66d4c14f43cd351512d28c249d96d3cd3e1e6dad834770fd"
],
[
[

View File

@ -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"}]}}

View File

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

View File

@ -1,7 +1,7 @@
app waspJob {
db: { system: PostgreSQL },
wasp: {
version: "^0.11.3"
version: "^0.11.4"
},
title: "waspJob"
}

View File

@ -284,7 +284,7 @@
"file",
"web-app/package.json"
],
"7e237189e89ac549b485ffef329aaa6699c874fd16b8e890f37f412ad7715219"
"fd7ca891ab5d232690015d34110d34a180906e1bb7c37e2432ebad2356b860a3"
],
[
[

View File

@ -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"}]}}

View File

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

View File

@ -1,6 +1,6 @@
app waspMigrate {
wasp: {
version: "^0.11.3"
version: "^0.11.4"
},
title: "waspMigrate"
}

View File

@ -1,6 +1,6 @@
app waspNew {
wasp: {
version: "^0.11.3"
version: "^0.11.4"
},
title: "waspNew"
}

View File

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

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "address" TEXT;

View File

@ -0,0 +1,5 @@
module.exports = {
trailingComma: 'es5',
semi: false,
singleQuote: true,
}

View File

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

View File

@ -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'
}
}
]}
/>
)
}

View 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
},
})

View File

@ -0,0 +1,5 @@
import { defineAdditionalSignupFields } from '@wasp/auth/index.js'
export const fields = defineAdditionalSignupFields({
address: (data) => data.address,
})

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "address" TEXT;

View File

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

View File

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

View 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
},
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,6 @@
// Used to format the code in the docs
module.exports = {
trailingComma: 'es5',
semi: false,
singleQuote: true,
}