Files
bssapp/ui/src/pages/ResetPassword.vue

259 lines
6.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<q-page class="flex flex-center bg-grey-2">
<!-- VALIDATING -->
<q-inner-loading v-if="validating" showing />
<!-- TOKEN OK FORM -->
<q-card
v-else-if="tokenValid"
class="q-pa-sm"
style="width:420px; max-width:90vw"
>
<q-card-section>
<div class="text-h6 text-weight-bold">
🔐 Parola Sıfırlama
</div>
<div class="text-caption text-grey-7 q-mt-xs">
Yeni parolanızı belirleyin
</div>
</q-card-section>
<q-separator />
<q-form @submit.prevent="submit">
<q-card-section>
<!-- NEW PASSWORD -->
<q-input
v-model="password"
:type="showPassword ? 'text' : 'password'"
label="Yeni Parola"
dense
filled
:rules="[passwordRule]"
>
<template #append>
<q-icon
:name="showPassword ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showPassword = !showPassword"
/>
</template>
</q-input>
<!-- STRENGTH -->
<div class="q-mt-xs">
<q-linear-progress
:value="passwordStrength.value"
:color="passwordStrength.color"
rounded
size="6px"
/>
<div class="text-caption q-mt-xs" :class="passwordStrength.textColor">
{{ passwordStrength.label }}
</div>
</div>
<!-- CONFIRM -->
<q-input
v-model="password2"
:type="showPassword2 ? 'text' : 'password'"
label="Parola Tekrar"
dense
filled
class="q-mt-sm"
:rules="[confirmRule]"
>
<template #append>
<q-icon
:name="showPassword2 ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showPassword2 = !showPassword2"
/>
</template>
</q-input>
<!-- ERROR -->
<q-banner
v-if="error"
class="bg-red-1 text-red q-mt-md"
rounded
>
{{ error }}
</q-banner>
</q-card-section>
<q-card-actions align="right">
<q-btn
type="submit"
label="PAROLAYI GÜNCELLE"
color="primary"
:loading="loading"
:disable="!canSubmit"
/>
</q-card-actions>
</q-form>
</q-card>
<!-- TOKEN INVALID -->
<q-card
v-else
class="q-pa-md text-center"
style="width:420px; max-width:90vw"
>
<div class="text-h6 text-red">
Bağlantı Geçersiz
</div>
<div class="text-caption text-grey-7 q-mt-sm">
Parola sıfırlama bağlantısı süresi dolmuş veya daha önce kullanılmış olabilir.
</div>
<q-btn
label="GİRİŞ SAYFASINA DÖN"
color="primary"
class="q-mt-md"
@click="router.push('/')"
/>
</q-card>
</q-page>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useQuasar } from 'quasar'
import api, { post } from 'src/services/api'
import { useAuthStore } from 'stores/authStore.js'
/* -------------------------------------------------- */
/* INIT */
/* -------------------------------------------------- */
const $q = useQuasar()
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const token = ref('')
const password = ref('')
const password2 = ref('')
const loading = ref(false)
const validating = ref(true)
const tokenValid = ref(false)
const error = ref(null)
const showPassword = ref(false)
const showPassword2 = ref(false)
/* ---------------- VALIDATION ---------------- */
const passwordRule = v =>
(!!v && v.length >= 8) || 'En az 8 karakter olmalı'
const confirmRule = v =>
v === password.value || 'Parolalar eşleşmiyor'
const canSubmit = computed(() =>
tokenValid.value &&
password.value.length >= 8 &&
password.value === password2.value &&
!loading.value
)
/* ---------------- PASSWORD STRENGTH ---------------- */
const passwordStrength = computed(() => {
const v = password.value || ''
let score = 0
if (v.length >= 8) score++
if (/[A-Z]/.test(v)) score++
if (/[0-9]/.test(v)) score++
if (/[^A-Za-z0-9]/.test(v)) score++
const map = [
{ value: 0.1, label: 'Çok zayıf', color: 'red', textColor: 'text-red' },
{ value: 0.25, label: 'Zayıf', color: 'orange', textColor: 'text-orange' },
{ value: 0.5, label: 'Orta', color: 'amber', textColor: 'text-amber' },
{ value: 0.75, label: 'İyi', color: 'blue', textColor: 'text-blue' },
{ value: 1, label: 'Güçlü', color: 'green', textColor: 'text-green' }
]
return map[Math.min(score, map.length - 1)]
})
/* -------------------------------------------------- */
/* TOKEN VALIDATE */
/* -------------------------------------------------- */
onMounted(async () => {
try {
token.value = decodeURIComponent(route.params.token || '')
if (!token.value) throw new Error('empty-token')
// 🔥 MERKEZİ API
await api.get(`/password/reset/validate/${token.value}`)
tokenValid.value = true
} catch {
tokenValid.value = false
} finally {
validating.value = false
}
})
/* -------------------------------------------------- */
/* SUBMIT — RESET + AUTO LOGIN */
/* -------------------------------------------------- */
async function submit () {
error.value = null
loading.value = true
try {
const res = await post('/password/reset', {
token: token.value,
password: password.value
})
if (!res?.success || !res?.token) {
throw new Error('reset-failed')
}
/* 🔐 AUTH HYDRATE
(login response ile birebir aynı) */
auth.token = res.token
auth.user = res.user
auth.permissions = Array.isArray(res.permissions) ? res.permissions : []
auth.role_id = Number(res.user?.role_id || null)
auth.forcePasswordChange = false
auth.lastLogin = new Date().toISOString()
// STORAGE
localStorage.setItem('token', auth.token)
localStorage.setItem('user', JSON.stringify(auth.user))
localStorage.setItem('permissions', JSON.stringify(auth.permissions))
localStorage.setItem('role_id', String(auth.role_id))
localStorage.setItem('lastLogin', auth.lastLogin)
localStorage.setItem('forcePasswordChange', '0')
$q.notify({
type: 'positive',
message: 'Parolanız güncellendi, giriş yapıldı',
position: 'top-right'
})
router.replace('/app')
} catch (err) {
error.value =
err?.message ||
'Parola politikaya uymuyor (büyük/küçük/rakam/özel karakter)'
$q.notify({
type: 'negative',
message: error.value,
position: 'top-right'
})
} finally {
loading.value = false
}
}
</script>