259 lines
6.9 KiB
Vue
259 lines
6.9 KiB
Vue
<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>
|