Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
}
loading.value = true;
try {
await axios.post('/auth/verify-email', {
await axios.post('/auth/verify-email-code', {
email: props.email,
code: code.value,
});
Expand Down
13 changes: 13 additions & 0 deletions locales/en-us.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,19 @@
"instructor_guide": "You can now use LibreOne to complete your instructor verification. This will allow LibreTexts to give you advanced access to services reserved for educators only.",
"continue_verification": "Continue to Instructor Verification"
},
"email_verification": {
"header": "Email Verification",
"loading": "Checking your verification token...",
"verify_thanks": "Thanks for helping to keep our community safe!",
"success_header": "Email Verified!",
"success_tagline": "Your email has been successfully verified. You can now sign in to LibreOne with your email and password.",
"continue_to_signin": "Continue to Sign In",
"error_header": "Verification Failed",
"error_invalid": "Oops, it looks like your verification token is invalid. If you already entered your six-digit verification code during registration, you can safely ignore this message and sign in to LibreOne.",
"error_expired": "Oops, it looks like your verification token has expired. Please request a new verification email below.",
"resend_verification": "Resend Verification Email",
"resend_success": "Success! A new verification email has been sent to your email address. You can safely close this page now."
},
"password": {
"strength": "Strength",
"short": "Too short",
Expand Down
10 changes: 5 additions & 5 deletions pages/register/+Page.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ import {
import AuthForm from "@components/registration/AuthForm.vue";
import { usePageContext } from "@renderer/usePageContext";
import { usePageProps } from "@renderer/usePageProps";
const VerifyEmail = defineAsyncComponent(
() => import("@components/registration/VerifyEmail.vue")
const VerifyEmailForm = defineAsyncComponent(
() => import("@components/registration/VerifyEmailForm.vue")
);

const props = usePageProps<{
Expand All @@ -66,7 +66,7 @@ onMounted(() => {

const componentProps = computed(() => {
switch (stage.value) {
case VerifyEmail: {
case VerifyEmailForm: {
return { email: email.value };
}
default: {
Expand All @@ -76,7 +76,7 @@ const componentProps = computed(() => {
});
const componentEvents = computed(() => {
switch (stage.value) {
case VerifyEmail: {
case VerifyEmailForm: {
return {};
}
default: {
Expand All @@ -92,6 +92,6 @@ const componentEvents = computed(() => {
*/
function handleInitialRegistrationComplete(resEmail: string) {
email.value = resEmail;
stage.value = VerifyEmail;
stage.value = VerifyEmailForm;
}
</script>
163 changes: 163 additions & 0 deletions pages/verify-email/+Page.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<template>
<div
class="bg-zinc-100 grid grid-flow-col justify-items-center items-center min-h-screen py-10"
>
<div class="w-11/12 md:w-3/4">
<img
src="@renderer/libretexts_logo.png"
alt="LibreTexts"
class="max-w-xs my-0 mx-auto"
/>
<div
class="bg-white p-6 mt-6 shadow-md shadow-gray-400 rounded-md overflow-hidden"
>
<div aria-live="polite" :aria-busy="loading">
<!-- Loading State -->
<template v-if="loading">
<h1 class="text-center text-3xl font-medium">
{{ $t("email_verification.header") }}
</h1>
<div class="flex items-center justify-center mt-6">
<LoadingIndicator />
<span class="ml-2">{{ $t("email_verification.loading") }}</span>
</div>
<p class="text-xs text-center text-gray-500 mt-4">
{{ $t("email_verification.verify_thanks") }}
</p>
</template>

<!-- Success State -->
<template v-else-if="success">
<h1 class="text-center text-3xl font-medium">
{{ $t("email_verification.success_header") }}
</h1>
<p class="text-center text-gray-700 mt-4">
{{ $t("email_verification.success_tagline") }}
</p>
<a
href="/signin"
class="inline-flex items-center justify-center h-10 bg-primary p-2 mt-6 rounded-md text-white w-full font-medium hover:bg-sky-700 hover:shadow"
>
<span>{{ $t("email_verification.continue_to_signin") }}</span>
<FontAwesomeIcon
icon="fa-solid fa-circle-arrow-right"
class="ml-2"
/>
</a>
</template>

<!-- Invalid/Expired Token State -->
<template v-else>
<h1 class="text-center text-3xl font-medium">
{{ $t("email_verification.error_header") }}
</h1>
<p class="text-center text-gray-700 mt-4" v-if="showResendOption">
{{ $t("email_verification.error_expired") }}
</p>
<p class="text-center text-gray-700 mt-4" v-else>
{{ $t("email_verification.error_invalid") }}
</p>
<p class="text-center text-gray-700 mt-4" v-if="didResend">
{{ $t("email_verification.resend_success") }}
</p>
<ThemedButton
v-if="showResendOption && !didResend"
icon="IconCircleArrowRight"
@click="resendVerificationEmail"
class="mt-6 w-full justify-center"
>
{{ $t("email_verification.resend_verification") }}
</ThemedButton>
</template>

<!-- Error Message -->
<p
class="text-center text-red-600 text-sm mt-4 font-bold"
v-if="error"
>
{{ error }}
</p>
</div>
</div>
</div>
</div>
</template>

<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { useAxios } from "@renderer/useAxios";
import { usePageProps } from "@renderer/usePageProps";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import LoadingIndicator from "@components/LoadingIndicator.vue";
import ThemedButton from "../../components/ThemedButton.vue";

const props = usePageProps<{
token: string;
}>();
const axios = useAxios();

const success = ref(false);
const loading = ref(true);
const error = ref<string | null>(null);
const resendUUID = ref<string | null>(null);
const showResendOption = ref(false);
const didResend = ref(false);

async function submitToken() {
try {
loading.value = true;

const result = await axios.post("/auth/verify-email-token", {
token: props.token,
});

success.value = result.data.success === true;
resendUUID.value = result.data.data?.uuid || null;

// Can only resend if the token expired so we got the UUID back
// If it was an invalid token, we won't get a UUID
if (!success.value && resendUUID.value) {
showResendOption.value = true;
}
} catch (e: any) {
console.error(e);
success.value = false;
if (e?.code !== "ERR_BAD_REQUEST") {
error.value = "An error occurred while verifying your email.";
}
} finally {
loading.value = false;
}
}

async function resendVerificationEmail() {
try {
if (!resendUUID.value) return;

loading.value = true;

await axios.post("/auth/resend-verification-email", {
uuid: resendUUID.value,
});

didResend.value = true;
} catch (e) {
console.error("Failed to resend verification email:", e);
error.value =
"An error occurred while resending the verification email. Please contact our Support Center.";
} finally {
loading.value = false;
}
}

// Automatically submit token when component mounts
onMounted(() => {
if (props.token) {
submitToken();
} else {
// No token provided, show error state
loading.value = false;
success.value = false;
}
});
</script>
23 changes: 23 additions & 0 deletions pages/verify-email/+onBeforeRender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { PageContextServer } from 'vike/types';

/**
* Reads search parameters provided in the URL and transforms them to component props.
*
* @param pageContext - The current server-side page rendering context.
* @returns New pageContext object with parsed props.
*/
export default async function onBeforeRender(pageContext: PageContextServer) {
const searchParams = pageContext.urlParsed.search;
let token: string | null = null;
if (searchParams.token) {
token = searchParams.token;
}

return {
pageContext: {
pageProps: {
...(token && { token }),
},
},
};
}
Loading
Loading