feat: ensure signup verification [UPM-14] (#13858)
Previously, signing up gave immediate access to the app. Now, unconfirmed users are redirected to a verification page where they can resend the confirmation email. - After signup, the user is routed to `/auth/verify-email` instead of the dashboard - After login, unconfirmed users are redirected to the verification page - The dashboard route guard catches unconfirmed users and redirects them - `active_for_authentication?` is removed from the sessions controller so unconfirmed users can authenticate — the frontend gates access instead - If the user visits the verification page after already confirming, they're automatically redirected to the dashboard - No session is issued until the user is verified <details><summary>Demo</summary> <p> #### Fresh Signup https://github.com/user-attachments/assets/abb735e5-7c8e-44a2-801c-96d9e4823e51 #### Google Fresh Signup https://github.com/user-attachments/assets/ab9e389a-a604-4a9d-b492-219e6d94ee3f #### Create new account from Dashboard https://github.com/user-attachments/assets/c456690d-1946-4e0b-834b-ad8efcea8369 </p> </details> --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -45,6 +45,13 @@
|
||||
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
|
||||
},
|
||||
"SUBMIT": "Create account",
|
||||
"HAVE_AN_ACCOUNT": "Already have an account?"
|
||||
"HAVE_AN_ACCOUNT": "Already have an account?",
|
||||
"VERIFY_EMAIL": {
|
||||
"TITLE": "Check your inbox",
|
||||
"DESCRIPTION": "We sent a verification link to {email}. Click the link to verify your email and get started.",
|
||||
"RESEND": "Resend verification email",
|
||||
"RESEND_SUCCESS": "Verification email sent. Please check your inbox.",
|
||||
"RESEND_ERROR": "Could not send verification email. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ export const register = async creds => {
|
||||
password: creds.password,
|
||||
h_captcha_client_response: creds.hCaptchaClientResponse,
|
||||
});
|
||||
setAuthCredentials(response);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
@@ -65,6 +64,13 @@ export const register = async creds => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const resendConfirmation = async ({ email, hCaptchaClientResponse }) => {
|
||||
return wootAPI.post('resend_confirmation', {
|
||||
email,
|
||||
h_captcha_client_response: hCaptchaClientResponse,
|
||||
});
|
||||
};
|
||||
|
||||
export const verifyPasswordToken = async ({ confirmationToken }) => {
|
||||
try {
|
||||
const response = await wootAPI.post('auth/confirmation', {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength, email } from '@vuelidate/validators';
|
||||
import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { DEFAULT_REDIRECT_URL } from 'dashboard/constants/globals';
|
||||
import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
|
||||
import FormInput from '../../../../../components/Form/Input.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
@@ -19,6 +19,7 @@ const MIN_PASSWORD_LENGTH = 6;
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const hCaptcha = ref(null);
|
||||
const isPasswordFocused = ref(false);
|
||||
@@ -76,7 +77,10 @@ const performRegistration = async () => {
|
||||
isSignupInProgress.value = true;
|
||||
try {
|
||||
await register(credentials);
|
||||
window.location = DEFAULT_REDIRECT_URL;
|
||||
router.push({
|
||||
name: 'auth_verify_email',
|
||||
state: { email: credentials.email },
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error?.message || t('REGISTER.API.ERROR_MESSAGE');
|
||||
if (globalConfig.value.hCaptchaSiteKey) {
|
||||
|
||||
110
app/javascript/v3/views/auth/verify-email/Index.vue
Normal file
110
app/javascript/v3/views/auth/verify-email/Index.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import { resendConfirmation } from '../../../api/auth';
|
||||
|
||||
const props = defineProps({
|
||||
email: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
if (!props.email) {
|
||||
router.push({ name: 'login' });
|
||||
}
|
||||
|
||||
const globalConfig = computed(() => store.getters['globalConfig/get']);
|
||||
const isResendingEmail = ref(false);
|
||||
const hCaptcha = ref(null);
|
||||
let captchaToken = '';
|
||||
|
||||
const performResend = async () => {
|
||||
isResendingEmail.value = true;
|
||||
try {
|
||||
await resendConfirmation({
|
||||
email: props.email,
|
||||
hCaptchaClientResponse: captchaToken,
|
||||
});
|
||||
useAlert(t('REGISTER.VERIFY_EMAIL.RESEND_SUCCESS'));
|
||||
} catch {
|
||||
useAlert(t('REGISTER.VERIFY_EMAIL.RESEND_ERROR'));
|
||||
} finally {
|
||||
isResendingEmail.value = false;
|
||||
captchaToken = '';
|
||||
if (globalConfig.value.hCaptchaSiteKey) {
|
||||
hCaptcha.value.reset();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleResendEmail = () => {
|
||||
if (isResendingEmail.value) return;
|
||||
if (globalConfig.value.hCaptchaSiteKey) {
|
||||
hCaptcha.value.execute();
|
||||
} else {
|
||||
performResend();
|
||||
}
|
||||
};
|
||||
|
||||
const onCaptchaVerified = token => {
|
||||
captchaToken = token;
|
||||
performResend();
|
||||
};
|
||||
|
||||
const onCaptchaError = () => {
|
||||
isResendingEmail.value = false;
|
||||
captchaToken = '';
|
||||
hCaptcha.value.reset();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main
|
||||
class="flex flex-col w-full min-h-screen py-20 bg-n-brand/5 dark:bg-n-background sm:px-6 lg:px-8"
|
||||
>
|
||||
<section
|
||||
class="bg-white shadow sm:mx-auto mt-11 sm:w-full sm:max-w-lg dark:bg-n-solid-2 p-11 sm:shadow-lg sm:rounded-lg"
|
||||
>
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-semibold text-n-slate-12">
|
||||
{{ $t('REGISTER.VERIFY_EMAIL.TITLE') }}
|
||||
</h2>
|
||||
<p class="mt-2 text-sm text-n-slate-11">
|
||||
{{ $t('REGISTER.VERIFY_EMAIL.DESCRIPTION', { email }) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<VueHcaptcha
|
||||
v-if="globalConfig.hCaptchaSiteKey"
|
||||
ref="hCaptcha"
|
||||
size="invisible"
|
||||
:sitekey="globalConfig.hCaptchaSiteKey"
|
||||
@verify="onCaptchaVerified"
|
||||
@error="onCaptchaError"
|
||||
@expired="onCaptchaError"
|
||||
@challenge-expired="onCaptchaError"
|
||||
@closed="onCaptchaError"
|
||||
/>
|
||||
<NextButton
|
||||
lg
|
||||
type="button"
|
||||
data-testid="resend_email_button"
|
||||
class="w-full"
|
||||
:label="$t('REGISTER.VERIFY_EMAIL.RESEND')"
|
||||
:is-loading="isResendingEmail"
|
||||
@click="handleResendEmail"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
@@ -5,6 +5,7 @@ import SamlLogin from './login/Saml.vue';
|
||||
import Signup from './auth/signup/Index.vue';
|
||||
import ResetPassword from './auth/reset/password/Index.vue';
|
||||
import Confirmation from './auth/confirmation/Index.vue';
|
||||
import VerifyEmail from './auth/verify-email/Index.vue';
|
||||
import PasswordEdit from './auth/password/Edit.vue';
|
||||
|
||||
export default [
|
||||
@@ -48,6 +49,15 @@ export default [
|
||||
redirectUrl: route.query.route_url,
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: frontendURL('auth/verify-email'),
|
||||
name: 'auth_verify_email',
|
||||
component: VerifyEmail,
|
||||
meta: { ignoreSession: true },
|
||||
props: () => ({
|
||||
email: window.history.state?.email || '',
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: frontendURL('auth/password/edit'),
|
||||
name: 'auth_password_edit',
|
||||
|
||||
Reference in New Issue
Block a user