This commit is contained in:
2026-02-11 17:46:22 +03:00
commit eacfacb13b
266 changed files with 51337 additions and 0 deletions

View File

@@ -0,0 +1,264 @@
<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-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
label="PAROLAYI GÜNCELLE"
color="primary"
:loading="loading"
:disable="!canSubmit"
@click="submit"
/>
</q-card-actions>
</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'
import { usePermission } from 'src/composables/usePermission'
const { canRead, canWrite, canUpdate } = usePermission()
const canReadOrder = canRead('order')
const canWriteOrder = canWrite('order')
const canUpdateOrder = canUpdate('order')
/* -------------------------------------------------- */
/* 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>