Files
chore/frontend/vue-app/src/components/auth/VerifySignup.vue
Ryan Kegel a0a059472b
Some checks failed
Gitea Actions Demo / build-and-push (push) Failing after 6s
Moved things around
2026-01-21 17:18:58 -05:00

317 lines
8.6 KiB
Vue

<template>
<div class="layout">
<div class="edit-view">
<div class="verify-container">
<h2 v-if="verifyingLoading">Verifying</h2>
<div v-if="verified" class="success-message" aria-live="polite">
Your account has been verified.
<div class="meta">
Redirecting to sign in in <strong>{{ countdown }}</strong> second<span
v-if="countdown !== 1"
>s</span
>.
</div>
<div style="margin-top: 0.6rem">
<button
type="button"
class="btn-link"
@click="goToLogin"
style="
background: none;
border: none;
color: var(--btn-primary);
font-weight: 600;
cursor: pointer;
padding: 0;
margin-left: 6px;
"
>
Sign in
</button>
</div>
</div>
<div v-else>
<!-- Error or success message at the top -->
<div
v-if="verifyError"
class="error-message"
aria-live="polite"
style="margin-bottom: 1rem"
>
{{ verifyError }}
</div>
<div
v-if="resendSuccess"
class="success-message"
aria-live="polite"
style="margin-bottom: 1rem"
>
Verification email sent. Check your inbox.
</div>
<!-- Email form and resend button -->
<form @submit.prevent="handleResend" v-if="!sendingDialog">
<label
for="resend-email"
style="display: block; font-weight: 600; margin-bottom: 0.25rem"
>Email address</label
>
<input
id="resend-email"
v-model.trim="resendEmail"
autofocus
type="email"
placeholder="you@example.com"
:class="{ 'input-error': resendAttempted && !isResendEmailValid }"
/>
<small v-if="resendAttempted && !resendEmail" class="error-message" aria-live="polite"
>Email is required.</small
>
<small
v-else-if="resendAttempted && !isResendEmailValid"
class="error-message"
aria-live="polite"
>Please enter a valid email address.</small
>
<div style="margin-top: 0.6rem">
<button
type="submit"
class="form-btn"
:disabled="!isResendEmailValid || resendLoading"
>
Resend verification email
</button>
</div>
</form>
<!-- Sending dialog -->
<div v-if="sendingDialog" class="sending-dialog">
<div
class="modal-backdrop"
style="
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.35);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
"
>
<div
class="modal-dialog"
style="
background: #fff;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
max-width: 340px;
text-align: center;
"
>
<h3 style="margin-bottom: 1rem">Sending Verification Email</h3>
</div>
</div>
</div>
<div style="margin-top: 0.8rem">
<button
type="button"
class="btn-link"
@click="goToLogin"
style="
background: none;
border: none;
color: var(--btn-primary);
font-weight: 600;
cursor: pointer;
padding: 0;
margin-left: 6px;
"
>
Sign in
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import {
MISSING_TOKEN,
TOKEN_TIMESTAMP_MISSING,
TOKEN_EXPIRED,
INVALID_TOKEN,
MISSING_EMAIL,
USER_NOT_FOUND,
ALREADY_VERIFIED,
} from '@/common/errorCodes'
import '@/assets/actions-shared.css'
import '@/assets/button-shared.css'
import { parseErrorResponse } from '@/common/api'
const router = useRouter()
const route = useRoute()
const verifyingLoading = ref(true)
const verified = ref(false)
const verifyError = ref('')
const resendSuccess = ref(false)
const countdown = ref(10)
let countdownTimer: number | null = null
// Resend state
const resendEmail = ref<string>((route.query.email as string) ?? '')
const resendAttempted = ref(false)
const resendLoading = ref(false)
const sendingDialog = ref(false)
const isResendEmailValid = computed(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(resendEmail.value))
async function verifyToken() {
const raw = route.query.token ?? ''
const token = Array.isArray(raw) ? raw[0] : String(raw || '')
if (!token) {
router.push({ name: 'Login' }).catch(() => (window.location.href = '/auth/login'))
return
}
verifyingLoading.value = true
try {
const url = `/api/verify?token=${encodeURIComponent(token)}`
const res = await fetch(url, { method: 'GET' })
if (!res.ok) {
const { msg, code } = await parseErrorResponse(res)
switch (code) {
case INVALID_TOKEN:
case MISSING_TOKEN:
case TOKEN_TIMESTAMP_MISSING:
verifyError.value =
"Your account isn't verified. Please request a new verification email."
break
case TOKEN_EXPIRED:
verifyError.value =
'Your verification link has expired. Please request a new verification email.'
break
default:
verifyError.value = msg || `Verification failed with status ${res.status}.`
}
return
}
// success
verified.value = true
startRedirectCountdown()
} catch {
verifyError.value = 'Network error. Please try again.'
} finally {
verifyingLoading.value = false
}
}
function startRedirectCountdown() {
countdown.value = 10
countdownTimer = window.setInterval(() => {
countdown.value -= 1
if (countdown.value <= 0) {
clearCountdown()
router.push({ name: 'Login' }).catch(() => (window.location.href = '/auth/login'))
}
}, 1000)
}
function clearCountdown() {
if (countdownTimer !== null) {
clearInterval(countdownTimer)
countdownTimer = null
}
}
onBeforeUnmount(() => {
clearCountdown()
})
onMounted(() => {
verifyToken()
})
async function handleResend() {
resendAttempted.value = true
resendSuccess.value = false
verifyError.value = ''
if (!isResendEmailValid.value) return
sendingDialog.value = true
resendLoading.value = true
try {
const res = await fetch('/api/resend-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: resendEmail.value.trim() }),
})
resendEmail.value = ''
sendingDialog.value = false
resendLoading.value = false
if (!res.ok) {
const { msg, code } = await parseErrorResponse(res)
switch (code) {
case MISSING_EMAIL:
verifyError.value = 'An email address is required.'
break
case USER_NOT_FOUND:
verifyError.value = 'This email is not registered.'
break
case ALREADY_VERIFIED:
verifyError.value = 'Your account is already verified. Please log in.'
break
default:
verifyError.value = msg || `Resend failed with status ${res.status}.`
}
return
}
resendSuccess.value = true
verifyError.value = ''
} catch {
verifyError.value = 'Network error. Please try again.'
} finally {
sendingDialog.value = false
resendLoading.value = false
resendAttempted.value = false
}
}
function goToLogin() {
router.push({ name: 'Login' }).catch(() => (window.location.href = '/auth/login'))
}
</script>
<style scoped>
:deep(.edit-view) {
width: 400px;
}
.verify-container {
max-width: 520px;
margin: 0 auto;
padding: 0.6rem;
}
.meta {
margin-top: 0.5rem;
color: var(--sub-message-color, #6b7280);
}
</style>