feat: implement user authentication, profile management, and email verification with i18n support
This commit is contained in:
parent
06db182c46
commit
b2365a4c6a
6 changed files with 271 additions and 17 deletions
|
|
@ -8,7 +8,7 @@ useHead({
|
|||
title: 'ตั้งค่าบัญชี - e-Learning'
|
||||
})
|
||||
|
||||
const { currentUser, updateUserProfile, changePassword, uploadAvatar, sendVerifyEmail } = useAuth()
|
||||
const { currentUser, updateUserProfile, changePassword, uploadAvatar, sendVerifyEmail, fetchUserProfile } = useAuth()
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
|
|
@ -40,9 +40,25 @@ const userData = ref({
|
|||
phone: currentUser.value?.phone || '',
|
||||
createdAt: formatDate(currentUser.value?.createdAt),
|
||||
photoURL: currentUser.value?.photoURL || '',
|
||||
prefix: currentUser.value?.prefix?.th || ''
|
||||
prefix: currentUser.value?.prefix?.th || '',
|
||||
emailVerifiedAt: currentUser.value?.emailVerifiedAt
|
||||
})
|
||||
|
||||
// ...
|
||||
|
||||
// Watch for changes in global user state (e.g. after avatar upload)
|
||||
watch(() => currentUser.value, (newUser) => {
|
||||
if (newUser) {
|
||||
userData.value.photoURL = newUser.photoURL || ''
|
||||
userData.value.firstName = newUser.firstName
|
||||
userData.value.lastName = newUser.lastName
|
||||
userData.value.prefix = newUser.prefix?.th || ''
|
||||
userData.value.email = newUser.email
|
||||
userData.value.phone = newUser.phone || ''
|
||||
userData.value.emailVerifiedAt = newUser.emailVerifiedAt
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
const passwordForm = reactive({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
|
|
@ -184,7 +200,8 @@ watch(() => currentUser.value, (newUser) => {
|
|||
}
|
||||
}, { deep: true })
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
await fetchUserProfile(true)
|
||||
isHydrated.value = true
|
||||
})
|
||||
</script>
|
||||
|
|
@ -269,8 +286,10 @@ onMounted(() => {
|
|||
<ProfileEditForm
|
||||
v-model="userData"
|
||||
:loading="isProfileSaving"
|
||||
:verifying="isSendingVerify"
|
||||
@submit="handleUpdateProfile"
|
||||
@upload="handleFileUpload"
|
||||
@verify="handleSendVerifyEmail"
|
||||
/>
|
||||
|
||||
|
||||
|
|
|
|||
151
Frontend-Learner/pages/verify-email.vue
Normal file
151
Frontend-Learner/pages/verify-email.vue
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
<script setup lang="ts">
|
||||
/**
|
||||
* @file verify-email.vue
|
||||
* @description Page for handling email verification process.
|
||||
* Displays loading state while processing token, then shows success or error message.
|
||||
*/
|
||||
|
||||
definePageMeta({
|
||||
layout: 'default'
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const { verifyEmail } = useAuth()
|
||||
|
||||
const isLoading = ref(true)
|
||||
const isSuccess = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
const token = route.query.token as string
|
||||
|
||||
if (!token) {
|
||||
isLoading.value = false
|
||||
isSuccess.value = false
|
||||
errorMessage.value = t('auth.invalidToken') || 'Token ไม่ถูกต้อง'
|
||||
return
|
||||
}
|
||||
|
||||
// Call verify API
|
||||
const result = await verifyEmail(token)
|
||||
|
||||
isLoading.value = false
|
||||
|
||||
if (result.success) {
|
||||
isSuccess.value = true
|
||||
} else {
|
||||
isSuccess.value = false
|
||||
if (result.code === 400) {
|
||||
errorMessage.value = t('profile.emailAlreadyVerified') || 'อีเมลได้รับการยืนยันแล้ว'
|
||||
// Treat as success visually or show specific message?
|
||||
// Requirement says "check mark" for done.
|
||||
// If already verified, maybe show success-like state with "Already Verified" message.
|
||||
isSuccess.value = true // Let's show checkmark but with specific message
|
||||
} else if (result.code === 401) {
|
||||
errorMessage.value = t('auth.tokenExpired') || 'Token หมดอายุหรือล้มเหลว'
|
||||
} else {
|
||||
errorMessage.value = result.error || 'ยืนยันอีเมลไม่สำเร็จ'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const navigateToHome = () => {
|
||||
router.push('/dashboard/profile')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 bg-slate-50 dark:bg-[#0f172a]">
|
||||
<div class="auth-card max-w-md w-full space-y-8 p-8 rounded-2xl text-center">
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="flex flex-col items-center justify-center py-8">
|
||||
<q-spinner-dots size="4rem" color="primary" />
|
||||
<h2 class="mt-6 text-xl font-bold text-slate-900 dark:text-white animate-pulse">
|
||||
{{ $t('auth.verifyingEmail') || 'กำลังยืนยันอีเมล...' }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Success State -->
|
||||
<div v-else-if="isSuccess" class="flex flex-col items-center animate-bounce-in">
|
||||
<div class="w-24 h-24 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center mb-6">
|
||||
<q-icon name="check_circle" class="text-6xl text-green-500" />
|
||||
</div>
|
||||
|
||||
<h2 class="text-3xl font-black text-slate-900 dark:text-white mb-2">
|
||||
{{ errorMessage && errorMessage !== '' ? (errorMessage) : ($t('auth.emailVerified') || 'ยืนยันอีเมลสำเร็จ!') }}
|
||||
</h2>
|
||||
<p class="text-slate-500 dark:text-slate-400 mb-8">
|
||||
{{ $t('auth.emailVerifiedDesc') || 'บัญชีของคุณได้รับการยืนยันเรียบร้อยแล้ว' }}
|
||||
</p>
|
||||
|
||||
<q-btn
|
||||
unelevated
|
||||
rounded
|
||||
color="primary"
|
||||
class="w-full py-3 font-bold text-lg shadow-lg shadow-blue-500/30"
|
||||
:label="$t('common.backToHome') || 'กลับสู่หน้าหลัก'"
|
||||
@click="navigateToHome"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else class="flex flex-col items-center animate-shake">
|
||||
<div class="w-24 h-24 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center mb-6">
|
||||
<q-icon name="error" class="text-6xl text-red-500" />
|
||||
</div>
|
||||
|
||||
<h2 class="text-2xl font-black text-slate-900 dark:text-white mb-2">
|
||||
{{ $t('common.error') }}
|
||||
</h2>
|
||||
<p class="text-red-500 font-medium mb-8">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
|
||||
<q-btn
|
||||
unelevated
|
||||
rounded
|
||||
color="slate-700"
|
||||
class="w-full py-3 font-bold text-lg"
|
||||
label="ลองใหม่อีกครั้ง"
|
||||
@click="router.push('/')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.animate-bounce-in {
|
||||
animation: bounceIn 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.animate-shake {
|
||||
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
@apply bg-white border-slate-100 shadow-xl;
|
||||
border-width: 1px;
|
||||
}
|
||||
.dark .auth-card {
|
||||
@apply bg-[#1e293b] border-white/5 shadow-none;
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
0% { transform: scale(0.3); opacity: 0; }
|
||||
50% { transform: scale(1.05); opacity: 1; }
|
||||
70% { transform: scale(0.9); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
10%, 90% { transform: translate3d(-1px, 0, 0); }
|
||||
20%, 80% { transform: translate3d(2px, 0, 0); }
|
||||
30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
|
||||
40%, 60% { transform: translate3d(4px, 0, 0); }
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue