mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-26 10:35:04 +03:00
Remove user-facing unverified email flow (#1638)
This commit is contained in:
parent
e3c825fae1
commit
b360af3065
@ -166,12 +166,6 @@ export const LoginSignupForm = ({
|
||||
onLoginSuccess() {
|
||||
history.push('{= onAuthSucceededRedirectTo =}')
|
||||
},
|
||||
{=# isEmailVerificationRequired =}
|
||||
isEmailVerificationRequired: true,
|
||||
{=/ isEmailVerificationRequired =}
|
||||
{=^ isEmailVerificationRequired =}
|
||||
isEmailVerificationRequired: false,
|
||||
{=/ isEmailVerificationRequired =}
|
||||
});
|
||||
{=/ isEmailAuthEnabled =}
|
||||
{=# isAnyPasswordBasedAuthEnabled =}
|
||||
|
@ -4,7 +4,6 @@ import { login } from '../../../email/actions/login'
|
||||
export function useEmail({
|
||||
onError,
|
||||
showEmailVerificationPending,
|
||||
isEmailVerificationRequired,
|
||||
onLoginSuccess,
|
||||
isLogin,
|
||||
}: {
|
||||
@ -12,7 +11,6 @@ export function useEmail({
|
||||
showEmailVerificationPending: () => void
|
||||
onLoginSuccess: () => void
|
||||
isLogin: boolean
|
||||
isEmailVerificationRequired: boolean
|
||||
}) {
|
||||
async function handleSubmit(data) {
|
||||
try {
|
||||
@ -21,12 +19,7 @@ export function useEmail({
|
||||
onLoginSuccess()
|
||||
} else {
|
||||
await signup(data)
|
||||
if (isEmailVerificationRequired) {
|
||||
showEmailVerificationPending()
|
||||
} else {
|
||||
await login(data)
|
||||
onLoginSuccess()
|
||||
}
|
||||
showEmailVerificationPending()
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
onError(err as Error)
|
||||
|
@ -53,16 +53,19 @@ const config: ProviderConfig = {
|
||||
createRouter() {
|
||||
const router = Router();
|
||||
|
||||
const loginRoute = handleRejection(getLoginRoute({
|
||||
allowUnverifiedLogin: {=# allowUnverifiedLogin =}true{=/ allowUnverifiedLogin =}{=^ allowUnverifiedLogin =}false{=/ allowUnverifiedLogin =},
|
||||
}));
|
||||
const loginRoute = handleRejection(getLoginRoute());
|
||||
router.post('/login', loginRoute);
|
||||
|
||||
const signupRoute = handleRejection(getSignupRoute({
|
||||
fromField,
|
||||
clientRoute: '{= emailVerificationClientRoute =}',
|
||||
getVerificationEmailContent: _waspGetVerificationEmailContent,
|
||||
allowUnverifiedLogin: {=# allowUnverifiedLogin =}true{=/ allowUnverifiedLogin =}{=^ allowUnverifiedLogin =}false{=/ allowUnverifiedLogin =},
|
||||
{=# isDevelopment =}
|
||||
isEmailAutoVerified: process.env.SKIP_EMAIL_VERIFICATION_IN_DEV === 'true',
|
||||
{=/ isDevelopment =}
|
||||
{=^ isDevelopment =}
|
||||
isEmailAutoVerified: false,
|
||||
{=/ isDevelopment =}
|
||||
}));
|
||||
router.post('/signup', signupRoute);
|
||||
|
||||
|
@ -10,11 +10,7 @@ import {
|
||||
import { createSession } from '../../session.js'
|
||||
import { ensureValidEmail, ensurePasswordIsPresent } from '../../validation.js'
|
||||
|
||||
export function getLoginRoute({
|
||||
allowUnverifiedLogin,
|
||||
}: {
|
||||
allowUnverifiedLogin: boolean
|
||||
}) {
|
||||
export function getLoginRoute() {
|
||||
return async function login(
|
||||
req: Request<{ email: string; password: string; }>,
|
||||
res: Response,
|
||||
@ -29,7 +25,7 @@ export function getLoginRoute({
|
||||
throwInvalidCredentialsError()
|
||||
}
|
||||
const providerData = deserializeAndSanitizeProviderData<'email'>(authIdentity.providerData)
|
||||
if (!providerData.isEmailVerified && !allowUnverifiedLogin) {
|
||||
if (!providerData.isEmailVerified) {
|
||||
throwInvalidCredentialsError()
|
||||
}
|
||||
try {
|
||||
|
@ -24,12 +24,12 @@ export function getSignupRoute({
|
||||
fromField,
|
||||
clientRoute,
|
||||
getVerificationEmailContent,
|
||||
allowUnverifiedLogin,
|
||||
isEmailAutoVerified,
|
||||
}: {
|
||||
fromField: EmailFromField;
|
||||
clientRoute: string;
|
||||
getVerificationEmailContent: GetVerificationEmailContentFn;
|
||||
allowUnverifiedLogin: boolean;
|
||||
isEmailAutoVerified: boolean;
|
||||
}) {
|
||||
return async function signup(
|
||||
req: Request<{ email: string; password: string; }>,
|
||||
@ -65,20 +65,7 @@ export function getSignupRoute({
|
||||
* else's email address and therefore permanently making that email
|
||||
* address unavailable for later account creation (by real owner).
|
||||
*/
|
||||
if (existingAuthIdentity) {
|
||||
if (allowUnverifiedLogin) {
|
||||
/**
|
||||
* This is the case where we allow unverified login.
|
||||
*
|
||||
* If we pretended that the user was created successfully that would bring
|
||||
* us little value: the attacker would not be able to login and figure out
|
||||
* if the user exists or not, anyway.
|
||||
*
|
||||
* So, we throw an error that says that the user already exists.
|
||||
*/
|
||||
throw new HttpError(422, "User with that email already exists.")
|
||||
}
|
||||
|
||||
if (existingAuthIdentity) {
|
||||
const providerData = deserializeAndSanitizeProviderData<'email'>(existingAuthIdentity.providerData);
|
||||
|
||||
// TOOD: faking work makes sense if the time spent on faking the work matches the time
|
||||
@ -107,7 +94,7 @@ export function getSignupRoute({
|
||||
|
||||
const newUserProviderData = await sanitizeAndSerializeProviderData<'email'>({
|
||||
hashedPassword: fields.password,
|
||||
isEmailVerified: false,
|
||||
isEmailVerified: isEmailAutoVerified ? true : false,
|
||||
emailVerificationSentAt: null,
|
||||
passwordResetSentAt: null,
|
||||
});
|
||||
@ -124,6 +111,12 @@ export function getSignupRoute({
|
||||
rethrowPossibleAuthError(e);
|
||||
}
|
||||
|
||||
// Wasp allows for auto-verification of emails in development mode to
|
||||
// make writing e2e tests easier.
|
||||
if (isEmailAutoVerified) {
|
||||
return res.json({ success: true });
|
||||
}
|
||||
|
||||
const verificationLink = await createEmailVerificationLink(fields.email, clientRoute);
|
||||
try {
|
||||
await sendEmailVerificationEmail(
|
||||
@ -137,7 +130,7 @@ export function getSignupRoute({
|
||||
} catch (e: unknown) {
|
||||
console.error("Failed to send email verification email:", e);
|
||||
throw new HttpError(500, "Failed to send email verification email.");
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({ success: true });
|
||||
};
|
||||
|
@ -37,7 +37,6 @@ app todoApp {
|
||||
getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
|
||||
clientRoute: PasswordResetRoute
|
||||
},
|
||||
allowUnverifiedLogin: false,
|
||||
},
|
||||
},
|
||||
signup: {
|
||||
|
@ -1,2 +1,3 @@
|
||||
GOOGLE_CLIENT_ID="mock-client-id"
|
||||
GOOGLE_CLIENT_SECRET="mock-client-secret"
|
||||
GOOGLE_CLIENT_SECRET="mock-client-secret"
|
||||
SKIP_EMAIL_VERIFICATION_IN_DEV=true
|
@ -22,8 +22,7 @@ app todoApp {
|
||||
passwordReset: {
|
||||
getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
|
||||
clientRoute: PasswordResetRoute
|
||||
},
|
||||
allowUnverifiedLogin: true,
|
||||
}
|
||||
},
|
||||
google: {}
|
||||
},
|
||||
|
@ -17,7 +17,9 @@ test.describe("signup and login", () => {
|
||||
|
||||
await page.waitForSelector("text=Create a new account");
|
||||
|
||||
await expect(page.locator("a[href='http://localhost:3001/auth/google/login']")).toBeVisible();
|
||||
await expect(
|
||||
page.locator("a[href='http://localhost:3001/auth/google/login']")
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("can sign up", async ({ page }) => {
|
||||
@ -29,7 +31,9 @@ test.describe("signup and login", () => {
|
||||
await page.locator("input[type='password']").fill(password);
|
||||
await page.locator("button").click();
|
||||
|
||||
await expect(page).toHaveURL("/profile");
|
||||
await expect(page.locator("body")).toContainText(
|
||||
`You've signed up successfully! Check your email for the confirmation link.`
|
||||
);
|
||||
});
|
||||
|
||||
test("can log in and create a task", async ({ page }) => {
|
||||
|
@ -13,7 +13,6 @@ module Wasp.AppSpec.App.Auth
|
||||
isGoogleAuthEnabled,
|
||||
isGitHubAuthEnabled,
|
||||
isEmailAuthEnabled,
|
||||
isEmailVerificationRequired,
|
||||
)
|
||||
where
|
||||
|
||||
@ -59,8 +58,7 @@ data ExternalAuthConfig = ExternalAuthConfig
|
||||
data EmailAuthConfig = EmailAuthConfig
|
||||
{ fromField :: EmailFromField,
|
||||
emailVerification :: EmailVerificationConfig,
|
||||
passwordReset :: PasswordResetConfig,
|
||||
allowUnverifiedLogin :: Maybe Bool
|
||||
passwordReset :: PasswordResetConfig
|
||||
}
|
||||
deriving (Show, Eq, Data)
|
||||
|
||||
@ -86,8 +84,3 @@ isGitHubAuthEnabled = isJust . gitHub . methods
|
||||
|
||||
isEmailAuthEnabled :: Auth -> Bool
|
||||
isEmailAuthEnabled = isJust . email . methods
|
||||
|
||||
isEmailVerificationRequired :: Auth -> Bool
|
||||
isEmailVerificationRequired auth = case email . methods $ auth of
|
||||
Nothing -> False
|
||||
Just emailAuthConfig -> allowUnverifiedLogin emailAuthConfig /= Just True
|
||||
|
@ -61,7 +61,7 @@ genEmailAuthConfig spec emailAuthConfig = return $ C.mkTmplFdWithDstAndData tmpl
|
||||
"passwordResetClientRoute" .= passwordResetClientRoute,
|
||||
"getPasswordResetEmailContent" .= getPasswordResetEmailContent,
|
||||
"getVerificationEmailContent" .= getVerificationEmailContent,
|
||||
"allowUnverifiedLogin" .= fromMaybe False (AS.Auth.allowUnverifiedLogin emailAuthConfig)
|
||||
"isDevelopment" .= isDevelopment
|
||||
]
|
||||
|
||||
fromFieldJson =
|
||||
@ -74,6 +74,8 @@ genEmailAuthConfig spec emailAuthConfig = return $ C.mkTmplFdWithDstAndData tmpl
|
||||
maybeName = AS.EmailSender.name fromField
|
||||
email = AS.EmailSender.email fromField
|
||||
|
||||
isDevelopment = not $ AS.isBuild spec
|
||||
|
||||
emailVerificationClientRoute = getRoutePathFromRef spec $ AS.Auth.EmailVerification.clientRoute emailVerification
|
||||
passwordResetClientRoute = getRoutePathFromRef spec $ AS.Auth.PasswordReset.clientRoute passwordReset
|
||||
getPasswordResetEmailContent = extImportToImportJson relPathToServerSrcDir $ AS.Auth.PasswordReset.getEmailContentFn passwordReset
|
||||
|
@ -118,8 +118,7 @@ genLoginSignupForm auth =
|
||||
-- Username and password
|
||||
"isUsernameAndPasswordAuthEnabled" .= AS.Auth.isUsernameAndPasswordAuthEnabled auth,
|
||||
-- Email
|
||||
"isEmailAuthEnabled" .= AS.Auth.isEmailAuthEnabled auth,
|
||||
"isEmailVerificationRequired" .= AS.Auth.isEmailVerificationRequired auth
|
||||
"isEmailAuthEnabled" .= AS.Auth.isEmailAuthEnabled auth
|
||||
]
|
||||
areBothSocialAndPasswordBasedAuthEnabled = AS.Auth.isExternalAuthEnabled auth && isAnyPasswordBasedAuthEnabled
|
||||
isAnyPasswordBasedAuthEnabled = AS.Auth.isUsernameAndPasswordAuthEnabled auth || AS.Auth.isEmailAuthEnabled auth
|
||||
|
@ -180,8 +180,7 @@ spec_AppSpecValid = do
|
||||
AS.Auth.PasswordReset.PasswordResetConfig
|
||||
{ AS.Auth.PasswordReset.clientRoute = AS.Core.Ref.Ref basicRouteName,
|
||||
AS.Auth.PasswordReset.getEmailContentFn = Nothing
|
||||
},
|
||||
AS.Auth.allowUnverifiedLogin = Nothing
|
||||
}
|
||||
}
|
||||
|
||||
it "returns no error if app.auth is not set" $ do
|
||||
@ -238,8 +237,7 @@ spec_AppSpecValid = do
|
||||
AS.Auth.PasswordReset.PasswordResetConfig
|
||||
{ AS.Auth.PasswordReset.clientRoute = AS.Core.Ref.Ref basicRouteName,
|
||||
AS.Auth.PasswordReset.getEmailContentFn = Nothing
|
||||
},
|
||||
AS.Auth.allowUnverifiedLogin = Nothing
|
||||
}
|
||||
}
|
||||
|
||||
let makeSpec emailSender isBuild =
|
||||
|
@ -70,7 +70,6 @@ app myApp {
|
||||
passwordReset: {
|
||||
clientRoute: PasswordResetRoute,
|
||||
},
|
||||
allowUnverifiedLogin: false,
|
||||
},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login",
|
||||
@ -105,7 +104,6 @@ app myApp {
|
||||
passwordReset: {
|
||||
clientRoute: PasswordResetRoute,
|
||||
},
|
||||
allowUnverifiedLogin: false,
|
||||
},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login",
|
||||
@ -457,10 +455,6 @@ Running `wasp db migrate-dev` and then `wasp start` should give you a working ap
|
||||
|
||||
![Auth UI](/img/authui/login.png)
|
||||
|
||||
If logging in with an unverified email is _allowed_, the user will be able to login with an unverified email address. If logging in with an unverified email is _not allowed_, the user will be shown an error message.
|
||||
|
||||
Read more about the `allowUnverifiedLogin` option [here](#allowunverifiedlogin-bool-specifies-whether-the-user-can-login-without-verifying-their-e-mail-address).
|
||||
|
||||
### Signup
|
||||
|
||||
![Auth UI](/img/authui/signup.png)
|
||||
@ -484,6 +478,17 @@ Some of the behavior you get out of the box:
|
||||
|
||||
## Email Verification Flow
|
||||
|
||||
:::info Automatic email verification in development
|
||||
|
||||
In development mode, you can skip the email verification step by setting the `SKIP_EMAIL_VERIFICATION_IN_DEV` environment variable to `true` in your `.env.server` file:
|
||||
|
||||
```env title=".env.server"
|
||||
SKIP_EMAIL_VERIFICATION_IN_DEV=true
|
||||
```
|
||||
|
||||
This is useful when you are developing your app and don't want to go through the email verification flow every time you sign up. It can be also useful when you are writing automated tests for your app.
|
||||
:::
|
||||
|
||||
By default, Wasp requires the e-mail to be verified before allowing the user to log in. This is done by sending a verification email to the user's email address and requiring the user to click on a link in the email to verify their email address.
|
||||
|
||||
Our setup looks like this:
|
||||
@ -902,7 +907,6 @@ app myApp {
|
||||
clientRoute: PasswordResetRoute,
|
||||
getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
|
||||
},
|
||||
allowUnverifiedLogin: false,
|
||||
},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/someRoute"
|
||||
@ -934,7 +938,6 @@ app myApp {
|
||||
clientRoute: PasswordResetRoute,
|
||||
getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
|
||||
},
|
||||
allowUnverifiedLogin: false,
|
||||
},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/someRoute"
|
||||
@ -1104,11 +1107,3 @@ It has the following fields:
|
||||
</Tabs>
|
||||
|
||||
<small>This is the default content of the e-mail, you can customize it to your liking.</small>
|
||||
|
||||
#### `allowUnverifiedLogin: bool`: specifies whether the user can login without verifying their e-mail address
|
||||
|
||||
It defaults to `false`. If `allowUnverifiedLogin` is set to `true`, the user can login without verifying their e-mail address, otherwise users will receive a `401` error when trying to login without verifying their e-mail address.
|
||||
|
||||
Sometimes you want to allow unverified users to login to provide them a different onboarding experience. Some of the pages can be viewed without verifying the e-mail address, but some of them can't. You can use the `isEmailVerified` field on the user entity to check if the user has verified their e-mail address.
|
||||
|
||||
If you have any questions, feel free to ask them on [our Discord server](https://discord.gg/rzdnErX).
|
Loading…
Reference in New Issue
Block a user