Some checks failed
Gitea Actions Demo / build-and-push (push) Failing after 6s
317 lines
8.6 KiB
Vue
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>
|