diff --git a/docs/docs/auth/dbauth.md b/docs/docs/auth/dbauth.md index 2bc99d715d98..15060496de3c 100644 --- a/docs/docs/auth/dbauth.md +++ b/docs/docs/auth/dbauth.md @@ -107,6 +107,16 @@ If you'd rather create your own, you might want to start from the generated page Almost all config for dbAuth lives in `api/src/functions/auth.js` in the object you give to the `DbAuthHandler` initialization. The comments above each key will explain what goes where. Here's an overview of the more important options: +### login.enabled + +Allow users to call login. Defaults to true. Needs to be explicitly set to false to disable the flow. + +```jsx +login: { + enabled: false +} +``` + ### login.handler() If you want to do something other than immediately let a user log in if their username/password is correct, you can add additional logic in `login.handler()`. For example, if a user's credentials are correct, but they haven't verified their email address yet, you can throw an error in this function with the appropriate message and then display it to the user. If the login should proceed, simply return the user that was passed as the only argument to the function: @@ -123,6 +133,16 @@ login: { } ``` +### signup.enabled + +Allow users to sign up. Defaults to true. Needs to be explicitly set to false to disable the flow. + +```jsx +signup: { + enabled: false +} +``` + ### signup.handler() This function should contain the code needed to actually create a user in your database. You will receive a single argument which is an object with all of the fields necessary to create the user (`username`, `hashedPassword` and `salt`) as well as any additional fields you included in your signup form in an object called `userAttributes`: @@ -167,6 +187,16 @@ const onSubmit = async (data) => { } ``` +### forgotPassword.enabled + +Allow users to request a new password via a call to `forgotPassword`. Defaults to true. Needs to be explicitly set to false to disable the flow. +When disabling this flow you probably want to disable `resetPassword` as well. + +```jsx +forgotPassword: { + enabled: false +} +``` ### forgotPassword.handler() This handler is invoked if a user is found with the username/email that they submitted on the Forgot Password page, and that user will be passed as an argument. Inside this function is where you'll send the user a link to reset their password—via an email is most common. The link will, by default, look like: @@ -177,6 +207,17 @@ If you changed the path to the Reset Password page in your routes you'll need to https://example.com/reset-password?resetKey=${user.resetKey} +### resetPassword.enabled + +Allow users to reset their password via a code from a call to `forgotPassword`. Defaults to true. Needs to be explicitly set to false to disable the flow. +When disabling this flow you probably want to disable `forgotPassword` as well. + +```jsx +resetPassword: { + enabled: false +} +``` + ### resetPassword.handler() This handler is invoked after the password has been successfully changed in the database. Returning something truthy (like `return user`) will automatically log the user in after their password is changed. If you'd like to return them to the login page and make them log in manually, `return false` and redirect the user in the Reset Password page. diff --git a/packages/api/src/functions/dbAuth/DbAuthHandler.ts b/packages/api/src/functions/dbAuth/DbAuthHandler.ts index 8221c88191f5..f0c8b84452e5 100644 --- a/packages/api/src/functions/dbAuth/DbAuthHandler.ts +++ b/packages/api/src/functions/dbAuth/DbAuthHandler.ts @@ -32,6 +32,109 @@ import { type SetCookieHeader = { 'Set-Cookie': string } type CsrfTokenHeader = { 'csrf-token': string } +interface SignupFlowOptions { + /** + * Allow users to sign up. Defaults to true. + * Needs to be explicitly set to false to disable the flow + */ + enabled?: boolean + /** + * Whatever you want to happen to your data on new user signup. Redwood will + * check for duplicate usernames before calling this handler. At a minimum + * you need to save the `username`, `hashedPassword` and `salt` to your + * user table. `userAttributes` contains any additional object members that + * were included in the object given to the `signUp()` function you got + * from `useAuth()` + */ + handler: (signupHandlerOptions: SignupHandlerOptions) => Promise + /** + * Object containing error strings + */ + errors?: { + fieldMissing?: string + usernameTaken?: string + flowNotEnabled?: string + } +} + +interface ForgotPasswordFlowOptions { + /** + * Allow users to request a new password via a call to forgotPassword. Defaults to true. + * Needs to be explicitly set to false to disable the flow + */ + enabled?: boolean + handler: (user: Record) => Promise + errors?: { + usernameNotFound?: string + usernameRequired?: string + flowNotEnabled?: string + } + expires: number +} + +interface LoginFlowOptions { + /** + * Allow users to login. Defaults to true. + * Needs to be explicitly set to false to disable the flow + */ + enabled?: boolean + /** + * Anything you want to happen before logging the user in. This can include + * throwing an error to prevent login. If you do want to allow login, this + * function must return an object representing the user you want to be logged + * in, containing at least an `id` field (whatever named field was provided + * for `authFields.id`). For example: `return { id: user.id }` + */ + handler: (user: Record) => Promise + /** + * Object containing error strings + */ + errors?: { + usernameOrPasswordMissing?: string + usernameNotFound?: string + incorrectPassword?: string + flowNotEnabled?: string + } + /** + * How long a user will remain logged in, in seconds + */ + expires: number +} + +interface ResetPasswordFlowOptions { + /** + * Allow users to reset their password via a code from a call to forgotPassword. Defaults to true. + * Needs to be explicitly set to false to disable the flow + */ + enabled?: boolean + handler: (user: Record) => Promise + allowReusedPassword: boolean + errors?: { + resetTokenExpired?: string + resetTokenInvalid?: string + resetTokenRequired?: string + reusedPassword?: string + flowNotEnabled?: string + } +} + +interface WebAuthnFlowOptions { + enabled: boolean + expires: number + name: string + domain: string + origin: string + timeout?: number + type: 'any' | 'platform' | 'cross-platform' + credentialFields: { + id: string + userId: string + publicKey: string + transports: string + counter: string + } +} + interface DbAuthHandlerOptions { /** * Provide prisma db client @@ -74,93 +177,24 @@ interface DbAuthHandlerOptions { /** * Object containing forgot password options */ - forgotPassword: { - handler: (user: Record) => Promise - errors?: { - usernameNotFound?: string - usernameRequired?: string - } - expires: number - } + forgotPassword: ForgotPasswordFlowOptions | { enabled: false } /** * Object containing login options */ - login: { - /** - * Anything you want to happen before logging the user in. This can include - * throwing an error to prevent login. If you do want to allow login, this - * function must return an object representing the user you want to be logged - * in, containing at least an `id` field (whatever named field was provided - * for `authFields.id`). For example: `return { id: user.id }` - */ - handler: (user: Record) => Promise - /** - * Object containing error strings - */ - errors?: { - usernameOrPasswordMissing?: string - usernameNotFound?: string - incorrectPassword?: string - } - /** - * How long a user will remain logged in, in seconds - */ - expires: number - } + login: LoginFlowOptions | { enabled: false } /** * Object containing reset password options */ - resetPassword: { - handler: (user: Record) => Promise - allowReusedPassword: boolean - errors?: { - resetTokenExpired?: string - resetTokenInvalid?: string - resetTokenRequired?: string - reusedPassword?: string - } - } + resetPassword: ResetPasswordFlowOptions | { enabled: false } /** * Object containing login options */ - signup: { - /** - * Whatever you want to happen to your data on new user signup. Redwood will - * check for duplicate usernames before calling this handler. At a minimum - * you need to save the `username`, `hashedPassword` and `salt` to your - * user table. `userAttributes` contains any additional object members that - * were included in the object given to the `signUp()` function you got - * from `useAuth()` - */ - handler: (signupHandlerOptions: SignupHandlerOptions) => Promise - /** - * Object containing error strings - */ - errors?: { - fieldMissing?: string - usernameTaken?: string - } - } + signup: SignupFlowOptions | { enabled: false } /** * Object containing WebAuthn options */ - webAuthn?: { - enabled: boolean - expires: number - name: string - domain: string - origin: string - timeout?: number - type: 'any' | 'platform' | 'cross-platform' - credentialFields: { - id: string - userId: string - publicKey: string - transports: string - counter: string - } - } + webAuthn?: WebAuthnFlowOptions | { enabled: false } /** * CORS settings, same as in createGraphqlHandler @@ -297,13 +331,15 @@ export class DbAuthHandler { const sessionExpiresAt = new Date() sessionExpiresAt.setSeconds( - sessionExpiresAt.getSeconds() + this.options.login.expires + sessionExpiresAt.getSeconds() + + (this.options.login as LoginFlowOptions).expires ) this.sessionExpiresDate = sessionExpiresAt.toUTCString() const webAuthnExpiresAt = new Date() webAuthnExpiresAt.setSeconds( - webAuthnExpiresAt.getSeconds() + (this.options?.webAuthn?.expires || 0) + webAuthnExpiresAt.getSeconds() + + ((this.options?.webAuthn as WebAuthnFlowOptions)?.expires || 0) ) this.webAuthnExpiresDate = webAuthnExpiresAt.toUTCString() @@ -389,13 +425,20 @@ export class DbAuthHandler { } async forgotPassword() { + const { enabled = true } = this.options.forgotPassword + if (!enabled) { + throw new DbAuthError.FlowNotEnabledError( + (this.options.forgotPassword as ForgotPasswordFlowOptions)?.errors + ?.flowNotEnabled || `Forgot password flow is not enabled` + ) + } const { username } = this.params // was the username sent in at all? if (!username || username.trim() === '') { throw new DbAuthError.UsernameRequiredError( - this.options.forgotPassword?.errors?.usernameRequired || - `Username is required` + (this.options.forgotPassword as ForgotPasswordFlowOptions)?.errors + ?.usernameRequired || `Username is required` ) } let user @@ -411,7 +454,8 @@ export class DbAuthHandler { if (user) { const tokenExpires = new Date() tokenExpires.setSeconds( - tokenExpires.getSeconds() + this.options.forgotPassword.expires + tokenExpires.getSeconds() + + (this.options.forgotPassword as ForgotPasswordFlowOptions).expires ) // generate a token @@ -435,9 +479,9 @@ export class DbAuthHandler { } // call user-defined handler in their functions/auth.js - const response = await this.options.forgotPassword.handler( - this._sanitizeUser(user) - ) + const response = await ( + this.options.forgotPassword as ForgotPasswordFlowOptions + ).handler(this._sanitizeUser(user)) return [ response ? JSON.stringify(response) : '', @@ -447,8 +491,8 @@ export class DbAuthHandler { ] } else { throw new DbAuthError.UsernameNotFoundError( - this.options.forgotPassword?.errors?.usernameNotFound || - `Username '${username} not found` + (this.options.forgotPassword as ForgotPasswordFlowOptions)?.errors + ?.usernameNotFound || `Username '${username} not found` ) } } @@ -471,9 +515,18 @@ export class DbAuthHandler { } async login() { + const { enabled = true } = this.options.login + if (!enabled) { + throw new DbAuthError.FlowNotEnabledError( + (this.options.login as LoginFlowOptions)?.errors?.flowNotEnabled || + `Login flow is not enabled` + ) + } const { username, password } = this.params const dbUser = await this._verifyUser(username, password) - const handlerUser = await this.options.login.handler(dbUser) + const handlerUser = await (this.options.login as LoginFlowOptions).handler( + dbUser + ) if ( handlerUser == null || @@ -490,12 +543,21 @@ export class DbAuthHandler { } async resetPassword() { + const { enabled = true } = this.options.resetPassword + if (!enabled) { + throw new DbAuthError.FlowNotEnabledError( + (this.options.resetPassword as ResetPasswordFlowOptions)?.errors + ?.flowNotEnabled || `Reset password flow is not enabled` + ) + } const { password, resetToken } = this.params // is the resetToken present? if (resetToken == null || String(resetToken).trim() === '') { throw new DbAuthError.ResetTokenRequiredError( - this.options.resetPassword?.errors?.resetTokenRequired + ( + this.options.resetPassword as ResetPasswordFlowOptions + )?.errors?.resetTokenRequired ) } @@ -508,11 +570,14 @@ export class DbAuthHandler { const [hashedPassword] = this._hashPassword(password, user.salt) if ( - !this.options.resetPassword.allowReusedPassword && + !(this.options.resetPassword as ResetPasswordFlowOptions) + .allowReusedPassword && user.hashedPassword === hashedPassword ) { throw new DbAuthError.ReusedPasswordError( - this.options.resetPassword?.errors?.reusedPassword + ( + this.options.resetPassword as ResetPasswordFlowOptions + )?.errors?.reusedPassword ) } @@ -533,9 +598,9 @@ export class DbAuthHandler { } // call the user-defined handler so they can decide what to do with this user - const response = await this.options.resetPassword.handler( - this._sanitizeUser(user) - ) + const response = await ( + this.options.resetPassword as ResetPasswordFlowOptions + ).handler(this._sanitizeUser(user)) // returning the user from the handler means to log them in automatically if (response) { @@ -546,6 +611,13 @@ export class DbAuthHandler { } async signup() { + const { enabled = true } = this.options.signup + if (!enabled) { + throw new DbAuthError.FlowNotEnabledError( + (this.options.signup as SignupFlowOptions)?.errors?.flowNotEnabled || + `Signup flow is not enabled` + ) + } const userOrMessage = await this._createUser() // at this point `user` is either an actual user, in which case log the @@ -567,7 +639,9 @@ export class DbAuthHandler { String(this.params.resetToken).trim() === '' ) { throw new DbAuthError.ResetTokenRequiredError( - this.options.resetPassword?.errors?.resetTokenRequired + ( + this.options.resetPassword as ResetPasswordFlowOptions + )?.errors?.resetTokenRequired ) } @@ -852,27 +926,42 @@ export class DbAuthHandler { } // must have an expiration time set for the session cookie - if (!this.options?.login?.expires) { + if ( + this.options?.login?.enabled !== false && + !this.options?.login?.expires + ) { throw new DbAuthError.NoSessionExpirationError() } // must have a login handler to actually log a user in - if (!this.options?.login?.handler) { + if ( + this.options?.login?.enabled !== false && + !this.options?.login?.handler + ) { throw new DbAuthError.NoLoginHandlerError() } // must have a signup handler to define how to create a new user - if (!this.options?.signup?.handler) { + if ( + this.options?.signup?.enabled !== false && + !this.options?.signup?.handler + ) { throw new DbAuthError.NoSignupHandlerError() } // must have a forgot password handler to define how to notify user of reset token - if (!this.options?.forgotPassword?.handler) { + if ( + this.options?.forgotPassword?.enabled !== false && + !this.options?.forgotPassword?.handler + ) { throw new DbAuthError.NoForgotPasswordHandlerError() } // must have a reset password handler to define what to do with user once password changed - if (!this.options?.resetPassword?.handler) { + if ( + this.options?.resetPassword?.enabled !== false && + !this.options?.resetPassword?.handler + ) { throw new DbAuthError.NoResetPasswordHandlerError() } @@ -1012,7 +1101,8 @@ export class DbAuthHandler { async _findUserByToken(token: string) { const tokenExpires = new Date() tokenExpires.setSeconds( - tokenExpires.getSeconds() - this.options.forgotPassword.expires + tokenExpires.getSeconds() - + (this.options.forgotPassword as ForgotPasswordFlowOptions).expires ) const user = await this.dbAccessor.findFirst({ @@ -1024,7 +1114,9 @@ export class DbAuthHandler { // user not found with the given token if (!user) { throw new DbAuthError.ResetTokenInvalidError( - this.options.resetPassword?.errors?.resetTokenInvalid + ( + this.options.resetPassword as ResetPasswordFlowOptions + )?.errors?.resetTokenInvalid ) } @@ -1032,7 +1124,9 @@ export class DbAuthHandler { if (user[this.options.authFields.resetTokenExpiresAt] < tokenExpires) { await this._clearResetToken(user) throw new DbAuthError.ResetTokenExpiredError( - this.options.resetPassword?.errors?.resetTokenExpired + ( + this.options.resetPassword as ResetPasswordFlowOptions + )?.errors?.resetTokenExpired ) } @@ -1069,7 +1163,9 @@ export class DbAuthHandler { password.toString().trim() === '' ) { throw new DbAuthError.UsernameAndPasswordRequiredError( - this.options.login?.errors?.usernameOrPasswordMissing + ( + this.options.login as LoginFlowOptions + )?.errors?.usernameOrPasswordMissing ) } @@ -1086,7 +1182,7 @@ export class DbAuthHandler { if (!user) { throw new DbAuthError.UserNotFoundError( username, - this.options.login?.errors?.usernameNotFound + (this.options.login as LoginFlowOptions)?.errors?.usernameNotFound ) } @@ -1100,7 +1196,7 @@ export class DbAuthHandler { } else { throw new DbAuthError.IncorrectPasswordError( username, - this.options.login?.errors?.incorrectPassword + (this.options.login as LoginFlowOptions)?.errors?.incorrectPassword ) } } @@ -1152,14 +1248,14 @@ export class DbAuthHandler { if (user) { throw new DbAuthError.DuplicateUsernameError( username, - this.options.signup?.errors?.usernameTaken + (this.options.signup as SignupFlowOptions)?.errors?.usernameTaken ) } // if we get here everything is good, call the app's signup handler and let // them worry about scrubbing data and saving to the DB const [hashedPassword, salt] = this._hashPassword(password) - const newUser = await this.options.signup.handler({ + const newUser = await (this.options.signup as SignupFlowOptions).handler({ username, hashedPassword, salt, @@ -1205,7 +1301,7 @@ export class DbAuthHandler { if (!value || value.trim() === '') { throw new DbAuthError.FieldRequiredError( name, - this.options.signup?.errors?.fieldMissing + (this.options.signup as SignupFlowOptions)?.errors?.fieldMissing ) } else { return true diff --git a/packages/api/src/functions/dbAuth/__tests__/DbAuthHandler.test.js b/packages/api/src/functions/dbAuth/__tests__/DbAuthHandler.test.js index 9b15abd46441..ba9ad1505329 100644 --- a/packages/api/src/functions/dbAuth/__tests__/DbAuthHandler.test.js +++ b/packages/api/src/functions/dbAuth/__tests__/DbAuthHandler.test.js @@ -325,6 +325,28 @@ describe('dbAuth', () => { ).toThrow(dbAuthError.NoForgotPasswordHandler) }) + it('does not throw an error if no forgotPassword.handler option but forgotPassword.enabled set to false', () => { + expect( + () => + new DbAuthHandler(event, context, { + db: db, + login: { + handler: () => {}, + expires: 1, + }, + resetPassword: { + handler: () => {}, + }, + signup: { + handler: () => {}, + }, + forgotPassword: { + enabled: false, + }, + }) + ).not.toThrow(dbAuthError.NoForgotPasswordHandler) + }) + it('throws an error if login expiration time is not defined', () => { // login object doesn't exist at all expect( @@ -381,6 +403,26 @@ describe('dbAuth', () => { ).toThrow(dbAuthError.NoLoginHandlerError) }) + it('does not throw an error if no login.handler option but login.enabled set to false', () => { + expect( + () => + new DbAuthHandler(event, context, { + login: { + enabled: false, + }, + resetPassword: { + handler: () => {}, + }, + signup: { + handler: () => {}, + }, + forgotPassword: { + handler: () => {}, + }, + }) + ).not.toThrow(dbAuthError.NoLoginHandlerError) + }) + it('throws an error if no signup.handler option', () => { expect( () => @@ -416,6 +458,28 @@ describe('dbAuth', () => { ).toThrow(dbAuthError.NoSignupHandler) }) + it('does not throw an error if no signup.handler option but signup.enabled set to false', () => { + expect( + () => + new DbAuthHandler(event, context, { + db: db, + login: { + handler: () => {}, + expires: 1, + }, + resetPassword: { + handler: () => {}, + }, + signup: { + enabled: false, + }, + forgotPassword: { + handler: () => {}, + }, + }) + ).not.toThrow(dbAuthError.NoSignupHandler) + }) + it('parses params from a plain text body', () => { event = { headers: {}, body: `{"foo":"bar", "baz":123}` } const dbAuth = new DbAuthHandler(event, context, options) @@ -604,6 +668,44 @@ describe('dbAuth', () => { }) describe('forgotPassword', () => { + it('throws default error when not enabled', async () => { + event.body = JSON.stringify({ + username: 'rob@redwoodjs.com', + password: 'password', + name: 'Rob', + }) + options.forgotPassword.enabled = false + const dbAuth = new DbAuthHandler(event, context, options) + + try { + await dbAuth.forgotPassword() + } catch (e) { + expect(e.message).toEqual('Forgot password flow is not enabled') + } + expect.assertions(1) + }) + + it('throws custom error when not enabled and message provided', async () => { + event.body = JSON.stringify({ + username: 'rob@redwoodjs.com', + password: 'password', + name: 'Rob', + }) + options.forgotPassword.enabled = false + options.forgotPassword.errors = { + ...options.forgotPassword.errors, + flowNotEnabled: 'Custom flow not enabled error', + } + const dbAuth = new DbAuthHandler(event, context, options) + + try { + await dbAuth.forgotPassword() + } catch (e) { + expect(e.message).toEqual('Custom flow not enabled error') + } + expect.assertions(1) + }) + it('throws an error if username is blank', async () => { // missing completely event.body = JSON.stringify({}) @@ -708,6 +810,43 @@ describe('dbAuth', () => { }) describe('login', () => { + it('throws default error when not enabled', async () => { + event.body = JSON.stringify({ + username: 'rob@redwoodjs.com', + password: 'password', + name: 'Rob', + }) + options.login.enabled = false + const dbAuth = new DbAuthHandler(event, context, options) + + try { + await dbAuth.login() + } catch (e) { + expect(e.message).toEqual('Login flow is not enabled') + } + expect.assertions(1) + }) + + it('throws custom error when not enabled and message provided', async () => { + event.body = JSON.stringify({ + username: 'rob@redwoodjs.com', + password: 'password', + name: 'Rob', + }) + options.login.enabled = false + options.login.errors = { + ...options.signup.errors, + flowNotEnabled: 'Custom flow not enabled error', + } + const dbAuth = new DbAuthHandler(event, context, options) + + try { + await dbAuth.login() + } catch (e) { + expect(e.message).toEqual('Custom flow not enabled error') + } + expect.assertions(1) + }) it('throws an error if username is not found', async () => { await createDbUser() event.body = JSON.stringify({ @@ -863,6 +1002,43 @@ describe('dbAuth', () => { }) describe('resetPassword', () => { + it('throws default error when not enabled', async () => { + event.body = JSON.stringify({ + username: 'rob@redwoodjs.com', + password: 'password', + name: 'Rob', + }) + options.resetPassword.enabled = false + const dbAuth = new DbAuthHandler(event, context, options) + + try { + await dbAuth.resetPassword() + } catch (e) { + expect(e.message).toEqual('Reset password flow is not enabled') + } + expect.assertions(1) + }) + + it('throws custom error when not enabled and message provided', async () => { + event.body = JSON.stringify({ + username: 'rob@redwoodjs.com', + password: 'password', + name: 'Rob', + }) + options.resetPassword.enabled = false + options.resetPassword.errors = { + ...options.signup.errors, + flowNotEnabled: 'Custom flow not enabled error', + } + const dbAuth = new DbAuthHandler(event, context, options) + + try { + await dbAuth.resetPassword() + } catch (e) { + expect(e.message).toEqual('Custom flow not enabled error') + } + expect.assertions(1) + }) it('throws an error if resetToken is blank', async () => { // missing completely event.body = JSON.stringify({}) @@ -1144,6 +1320,44 @@ describe('dbAuth', () => { await expect(dbAuth.signup()).rejects.toThrow('Cannot signup') }) + it('throws default error when not enabled', async () => { + event.body = JSON.stringify({ + username: 'rob@redwoodjs.com', + password: 'password', + name: 'Rob', + }) + options.signup.enabled = false + const dbAuth = new DbAuthHandler(event, context, options) + + try { + await dbAuth.signup() + } catch (e) { + expect(e.message).toEqual('Signup flow is not enabled') + } + expect.assertions(1) + }) + + it('throws custom error when not enabled and message provided', async () => { + event.body = JSON.stringify({ + username: 'rob@redwoodjs.com', + password: 'password', + name: 'Rob', + }) + options.signup.enabled = false + options.signup.errors = { + ...options.signup.errors, + flowNotEnabled: 'Custom flow not enabled error', + } + const dbAuth = new DbAuthHandler(event, context, options) + + try { + await dbAuth.signup() + } catch (e) { + expect(e.message).toEqual('Custom flow not enabled error') + } + expect.assertions(1) + }) + it('creates a new user and logs them in', async () => { event.body = JSON.stringify({ username: 'rob@redwoodjs.com', diff --git a/packages/api/src/functions/dbAuth/errors.ts b/packages/api/src/functions/dbAuth/errors.ts index b45f302cca17..fe2a292916bc 100644 --- a/packages/api/src/functions/dbAuth/errors.ts +++ b/packages/api/src/functions/dbAuth/errors.ts @@ -158,6 +158,13 @@ export class SessionDecryptionError extends Error { } } +export class FlowNotEnabledError extends Error { + constructor(message = 'Flow is not enabled') { + super(message) + this.name = 'FlowNotEnabledError' + } +} + export class UsernameRequiredError extends Error { constructor(message = 'Username is required') { super(message) diff --git a/packages/cli/src/commands/setup/auth/templates/dbAuth.function.ts.template b/packages/cli/src/commands/setup/auth/templates/dbAuth.function.ts.template index b5ef7d5ac11c..6b4e8d44c17a 100644 --- a/packages/cli/src/commands/setup/auth/templates/dbAuth.function.ts.template +++ b/packages/cli/src/commands/setup/auth/templates/dbAuth.function.ts.template @@ -4,6 +4,7 @@ import { DbAuthHandler } from '@redwoodjs/api' export const handler = async (event, context) => { const forgotPasswordOptions = { + enabled: true, // handler() is invoked after verifying that a user was found with the given // username. This is where you can send the user an email with a link to // reset their password. With the default dbAuth routes and field names, the @@ -34,6 +35,7 @@ export const handler = async (event, context) => { } const loginOptions = { + enabled: true, // handler() is called after finding the user that matches the // username/password provided at login, but before actually considering them // logged in. The `user` argument will be the user in the database that @@ -63,6 +65,7 @@ export const handler = async (event, context) => { } const resetPasswordOptions = { + enabled: true, // handler() is invoked after the password has been successfully updated in // the database. Returning anything truthy will automatically log the user // in. Return `false` otherwise, and in the Reset Password page redirect the @@ -87,6 +90,7 @@ export const handler = async (event, context) => { } const signupOptions = { + enabled: true, // Whatever you want to happen to your data on new user signup. Redwood will // check for duplicate usernames before calling this handler. At a minimum // you need to save the `username`, `hashedPassword` and `salt` to your