+6
-7
@@ -39,16 +39,15 @@ export const STUDENT_REGISTRATION = Object.freeze({
|
|||||||
export const STUDENT_REGISTRATION_ORDER = Object.freeze(Object.values(STUDENT_REGISTRATION))
|
export const STUDENT_REGISTRATION_ORDER = Object.freeze(Object.values(STUDENT_REGISTRATION))
|
||||||
|
|
||||||
// User lifecycle states (Set User Status endpoint):
|
// User lifecycle states (Set User Status endpoint):
|
||||||
// pending – just registered
|
// pending – registered, phone not yet OTP-verified
|
||||||
// verified – phone OTP-confirmed
|
// verified – phone OTP-verified and registration steps completed (awaiting admin)
|
||||||
// approved – full access
|
// approved – approved by admin (full access)
|
||||||
// blocked – no access (existing tokens revoked)
|
// blocked – blocked by admin (no access; existing tokens revoked)
|
||||||
export const USER_STATUS = Object.freeze({
|
export const USER_STATUS = Object.freeze({
|
||||||
pending: 'در انتظار',
|
pending: 'تأیید نشده',
|
||||||
verified: 'تأیید تلفن',
|
verified: 'در انتظار تأیید',
|
||||||
approved: 'تأیید شده',
|
approved: 'تأیید شده',
|
||||||
blocked: 'مسدود',
|
blocked: 'مسدود',
|
||||||
rejected: 'رد شده',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const MARITAL_STATUS = Object.freeze({
|
export const MARITAL_STATUS = Object.freeze({
|
||||||
|
|||||||
@@ -21,11 +21,11 @@
|
|||||||
<div class="verification-item__date">
|
<div class="verification-item__date">
|
||||||
<span class="verification-item__date-label">تاریخ ثبت نام:</span>
|
<span class="verification-item__date-label">تاریخ ثبت نام:</span>
|
||||||
<span class="verification-item__date-value">
|
<span class="verification-item__date-value">
|
||||||
{{ user.faSubmittedAt || formatJalaaliDate(user.submittedAt) || '—' }}
|
{{ formatJalaaliDate(user.createdAt) || '—' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="user.status === 'pending'"
|
v-if="user.status === 'verified'"
|
||||||
class="verification-item__badge verification-item__badge--pending"
|
class="verification-item__badge verification-item__badge--pending"
|
||||||
>
|
>
|
||||||
<span class="verification-item__dot" />
|
<span class="verification-item__dot" />
|
||||||
@@ -92,9 +92,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['show-details', 'show-reject-reason', 'verify-user'])
|
const emit = defineEmits(['show-details', 'show-reject-reason', 'verify-user'])
|
||||||
|
|
||||||
const fullName = computed(
|
const fullName = computed(() => `${props.user.name || ''}`.trim() || '—')
|
||||||
() => `${props.user.firstName || ''} ${props.user.lastName || ''}`.trim() || '—'
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -77,15 +77,12 @@ import TextField from '@/components/form/TextField.vue'
|
|||||||
import { rejectionReasonSchema } from '../../../schema'
|
import { rejectionReasonSchema } from '../../../schema'
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
import TextareaField from '@/components/form/TextareaField.vue'
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
import {
|
import { adminUsersKeys, useChangeAdminUserStatusMutation } from '@/services/query/admin-users'
|
||||||
adminPendingStudentsKeys,
|
|
||||||
useChangePendingStudentStatusMutation,
|
|
||||||
} from '@/services/query/admin-pending-students'
|
|
||||||
|
|
||||||
defineOptions({ name: 'RejectReasonModal' })
|
defineOptions({ name: 'RejectReasonModal' })
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const changeStatusMutation = useChangePendingStudentStatusMutation()
|
const changeStatusMutation = useChangeAdminUserStatusMutation()
|
||||||
|
|
||||||
const DEFAULT_FORM = { rejectionReason: '', adminNote: '' }
|
const DEFAULT_FORM = { rejectionReason: '', adminNote: '' }
|
||||||
|
|
||||||
@@ -105,16 +102,14 @@ const handleChange = (key, value) => {
|
|||||||
|
|
||||||
const onSubmit = async (data, close) => {
|
const onSubmit = async (data, close) => {
|
||||||
const { isValid, payload } = await validate(form.value)
|
const { isValid, payload } = await validate(form.value)
|
||||||
console.log(errors)
|
|
||||||
|
|
||||||
if (!isValid) return
|
if (!isValid) return
|
||||||
const id = data?.id
|
const id = data?.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
await changeStatusMutation.mutateAsync({
|
await changeStatusMutation.mutateAsync({
|
||||||
id,
|
id,
|
||||||
payload: { status: 'rejected', ...payload },
|
payload: { status: 'blocked', ...payload },
|
||||||
})
|
})
|
||||||
await queryClient.invalidateQueries({ queryKey: adminPendingStudentsKeys.all })
|
await queryClient.invalidateQueries({ queryKey: adminUsersKeys.all })
|
||||||
form.value = structuredClone({ ...DEFAULT_FORM })
|
form.value = structuredClone({ ...DEFAULT_FORM })
|
||||||
resetErrors()
|
resetErrors()
|
||||||
close()
|
close()
|
||||||
|
|||||||
+4
-49
@@ -7,7 +7,7 @@
|
|||||||
min-width="auto"
|
min-width="auto"
|
||||||
:show-close-button="true"
|
:show-close-button="true"
|
||||||
>
|
>
|
||||||
<template #default="{ data, close }">
|
<template #default>
|
||||||
<div v-if="!user" class="user-verification-details">
|
<div v-if="!user" class="user-verification-details">
|
||||||
<p class="user-verification-details__loading">در حال بارگذاری…</p>
|
<p class="user-verification-details__loading">در حال بارگذاری…</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,39 +203,19 @@
|
|||||||
class="user-verification-details__video"
|
class="user-verification-details__video"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="user-verification-details__divider" />
|
|
||||||
|
|
||||||
<div class="user-verification-details__actions">
|
|
||||||
<BaseButton
|
|
||||||
text="ذخیره به صورت PDF"
|
|
||||||
:loading="pdfLoading"
|
|
||||||
custom-class="user-verification-details__pdf-btn"
|
|
||||||
@click="onSaveAsPdf(data, close)"
|
|
||||||
>
|
|
||||||
<template #appendIcon>
|
|
||||||
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
|
||||||
</template>
|
|
||||||
</BaseButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</BasicModal>
|
</BasicModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
import useModal from '@/composables/useModal'
|
import useModal from '@/composables/useModal'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||||
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||||
|
import { useAdminUserQuery } from '@/services/query/admin-users'
|
||||||
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
||||||
import {
|
|
||||||
usePendingStudentQuery,
|
|
||||||
useDownloadPendingStudentPdfMutation,
|
|
||||||
} from '@/services/query/admin-pending-students'
|
|
||||||
import {
|
import {
|
||||||
AUDIENCE_CONFLICT_RESPONSE,
|
AUDIENCE_CONFLICT_RESPONSE,
|
||||||
COMPETING_PROPAGATOR_RESPONSE,
|
COMPETING_PROPAGATOR_RESPONSE,
|
||||||
@@ -258,7 +238,7 @@ const modalData = computed(() => getModal('UserVerificationDetailsModal')?.data
|
|||||||
const userId = computed(() => modalData.value.id ?? null)
|
const userId = computed(() => modalData.value.id ?? null)
|
||||||
const userIdRef = computed(() => userId.value)
|
const userIdRef = computed(() => userId.value)
|
||||||
|
|
||||||
const { data: fetched } = usePendingStudentQuery(userIdRef, {
|
const { data: fetched } = useAdminUserQuery(userIdRef, {
|
||||||
enabled: () => !!userIdRef.value,
|
enabled: () => !!userIdRef.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -293,31 +273,6 @@ const faithProduction = computed(() =>
|
|||||||
const leaderMessage = computed(() =>
|
const leaderMessage = computed(() =>
|
||||||
verificationMedia.value.find((m) => m.type === VERIFICATION_MEDIA_TYPE.LEADER_MESSAGE)
|
verificationMedia.value.find((m) => m.type === VERIFICATION_MEDIA_TYPE.LEADER_MESSAGE)
|
||||||
)
|
)
|
||||||
|
|
||||||
const pdfLoading = ref(false)
|
|
||||||
const pdfMutation = useDownloadPendingStudentPdfMutation()
|
|
||||||
|
|
||||||
const onSaveAsPdf = async (data, close) => {
|
|
||||||
if (!user.value?.id) return
|
|
||||||
pdfLoading.value = true
|
|
||||||
try {
|
|
||||||
const response = await pdfMutation.mutateAsync(user.value.id)
|
|
||||||
const url = response?.data?.downloadUrl || response?.downloadUrl
|
|
||||||
if (url) {
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = url
|
|
||||||
link.target = '_blank'
|
|
||||||
link.download = 'profile.pdf'
|
|
||||||
document.body.append(link)
|
|
||||||
link.click()
|
|
||||||
link.remove()
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
} finally {
|
|
||||||
pdfLoading.value = false
|
|
||||||
}
|
|
||||||
data
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -54,12 +54,12 @@ import VerificationItem from '@/features/admin/users/components/verifications/Ve
|
|||||||
import VerificationsFilters from '@/features/admin/users/components/verifications/VerificationsFilters.vue'
|
import VerificationsFilters from '@/features/admin/users/components/verifications/VerificationsFilters.vue'
|
||||||
import RejectReasonModal from '@/features/admin/users/components/verifications/modals/RejectReasonModal.vue'
|
import RejectReasonModal from '@/features/admin/users/components/verifications/modals/RejectReasonModal.vue'
|
||||||
import RejectReasonDetailsModal from '@/features/admin/users/components/verifications/modals/RejectReasonDetailsModal.vue'
|
import RejectReasonDetailsModal from '@/features/admin/users/components/verifications/modals/RejectReasonDetailsModal.vue'
|
||||||
import UserVerificationDetailsModal from '@/features/admin/users/components/verifications/modals/UserVerificationDetailsModal.vue'
|
|
||||||
import {
|
import {
|
||||||
adminPendingStudentsKeys,
|
adminUsersKeys,
|
||||||
useChangePendingStudentStatusMutation,
|
useAdminUsersListQuery,
|
||||||
usePendingStudentsListQuery,
|
useChangeAdminUserStatusMutation,
|
||||||
} from '@/services/query/admin-pending-students'
|
} from '@/services/query/admin-users'
|
||||||
|
import UserVerificationDetailsModal from '@/features/admin/users/components/verifications/modals/UserVerificationDetailsModal.vue'
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { openModal, isModal } = useModal()
|
const { openModal, isModal } = useModal()
|
||||||
@@ -67,7 +67,9 @@ const { openModal, isModal } = useModal()
|
|||||||
const filters = ref({ name: '', nationalCode: '', fromDate: '', toDate: '' })
|
const filters = ref({ name: '', nationalCode: '', fromDate: '', toDate: '' })
|
||||||
const { pagination, setPage, reset: resetPagination } = usePagination({ page: 1, perPage: 10 })
|
const { pagination, setPage, reset: resetPagination } = usePagination({ page: 1, perPage: 10 })
|
||||||
|
|
||||||
const { data, isLoading } = usePendingStudentsListQuery(filters, pagination, {
|
const listFilters = computed(() => ({ ...filters.value, status: 'verified' }))
|
||||||
|
|
||||||
|
const { data, isLoading } = useAdminUsersListQuery(listFilters, pagination, {
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -81,7 +83,7 @@ const paginationMeta = computed(() => ({
|
|||||||
const onFilterApply = () => resetPagination()
|
const onFilterApply = () => resetPagination()
|
||||||
const onFilterReset = () => resetPagination()
|
const onFilterReset = () => resetPagination()
|
||||||
|
|
||||||
const changeStatusMutation = useChangePendingStudentStatusMutation()
|
const changeStatusMutation = useChangeAdminUserStatusMutation()
|
||||||
|
|
||||||
const onVerifyUser = ({ id, status }) => {
|
const onVerifyUser = ({ id, status }) => {
|
||||||
if (status === 'rejected') {
|
if (status === 'rejected') {
|
||||||
@@ -91,7 +93,7 @@ const onVerifyUser = ({ id, status }) => {
|
|||||||
changeStatusMutation.mutate(
|
changeStatusMutation.mutate(
|
||||||
{ id, payload: { status } },
|
{ id, payload: { status } },
|
||||||
{
|
{
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: adminPendingStudentsKeys.all }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: adminUsersKeys.all }),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ import TextField from '@/components/form/TextField.vue'
|
|||||||
import { loginWithCodeSchema } from '@/features/auth/schema'
|
import { loginWithCodeSchema } from '@/features/auth/schema'
|
||||||
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
|
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
|
||||||
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
|
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
|
||||||
import { useForgotPasswordMutation, useLoginWithCodeMutation } from '@/services/query/auth'
|
import { useForgotPasswordMutation, useRequestLoginOtpMutation } from '@/services/query/auth'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
mode: { type: String, default: 'loginWithCode' },
|
mode: { type: String, default: 'loginWithCode' },
|
||||||
@@ -71,20 +71,20 @@ const form = ref({ phone: '' })
|
|||||||
const { validate, validateAt, errors } = useYup(schema)
|
const { validate, validateAt, errors } = useYup(schema)
|
||||||
|
|
||||||
const { savePhoneNumber } = useAuth()
|
const { savePhoneNumber } = useAuth()
|
||||||
const loginCodeMutation = useLoginWithCodeMutation()
|
const requestLoginOtpMutation = useRequestLoginOtpMutation()
|
||||||
const forgotPasswordMutation = useForgotPasswordMutation()
|
const forgotPasswordMutation = useForgotPasswordMutation()
|
||||||
|
|
||||||
const loading = computed(
|
const loading = computed(
|
||||||
() => loginCodeMutation.isPending.value || forgotPasswordMutation.isPending.value
|
() => requestLoginOtpMutation.isPending.value || forgotPasswordMutation.isPending.value
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
const { isValid, payload } = await validate(form.value)
|
const { isValid, payload } = await validate(form.value)
|
||||||
if (!isValid) return
|
if (!isValid) return
|
||||||
if (isForgotPassword.value) {
|
if (isForgotPassword.value) {
|
||||||
await forgotPasswordMutation.mutateAsync(payload)
|
await forgotPasswordMutation.mutateAsync({ phone: payload.phone })
|
||||||
} else {
|
} else {
|
||||||
await loginCodeMutation.mutateAsync(payload)
|
await requestLoginOtpMutation.mutateAsync({ phone: payload.phone })
|
||||||
}
|
}
|
||||||
savePhoneNumber(payload.phone)
|
savePhoneNumber(payload.phone)
|
||||||
emit('next', { phone: payload.phone, mode: props.mode })
|
emit('next', { phone: payload.phone, mode: props.mode })
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ import TextField from '@/components/form/TextField.vue'
|
|||||||
import { verifyCodeSchema } from '@/features/auth/schema'
|
import { verifyCodeSchema } from '@/features/auth/schema'
|
||||||
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
|
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
|
||||||
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
|
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
|
||||||
|
import { usePasswordReset } from '@/features/auth/composables/usePasswordReset'
|
||||||
import { useRedirectAfterLogin } from '@/features/auth/composables/useRedirectAfterLogin'
|
import { useRedirectAfterLogin } from '@/features/auth/composables/useRedirectAfterLogin'
|
||||||
import {
|
import {
|
||||||
useForgotPasswordMutation,
|
useForgotPasswordMutation,
|
||||||
useLoginWithCodeMutation,
|
useRequestLoginOtpMutation,
|
||||||
|
useVerifyLoginOtpMutation,
|
||||||
useResendVerificationCodeMutation,
|
useResendVerificationCodeMutation,
|
||||||
useVerifyCodeMutation,
|
useVerifyCodeMutation,
|
||||||
useVerifyForgotPasswordCodeMutation,
|
useVerifyForgotPasswordCodeMutation,
|
||||||
@@ -76,15 +78,16 @@ const form = ref({ verifyCode: '' })
|
|||||||
const { validate, validateAt, errors } = useYup(schema)
|
const { validate, validateAt, errors } = useYup(schema)
|
||||||
|
|
||||||
const { applySession } = useAuth()
|
const { applySession } = useAuth()
|
||||||
|
const { setResetTicket } = usePasswordReset()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const redirectAfterLogin = useRedirectAfterLogin()
|
const redirectAfterLogin = useRedirectAfterLogin()
|
||||||
|
|
||||||
const verifyLoginMutation = useLoginWithCodeMutation()
|
const verifyLoginMutation = useVerifyLoginOtpMutation()
|
||||||
const verifySignupMutation = useVerifyCodeMutation()
|
const verifySignupMutation = useVerifyCodeMutation()
|
||||||
const verifyForgotMutation = useVerifyForgotPasswordCodeMutation()
|
const verifyForgotMutation = useVerifyForgotPasswordCodeMutation()
|
||||||
const resendCodeMutation = useResendVerificationCodeMutation()
|
const resendCodeMutation = useResendVerificationCodeMutation()
|
||||||
const forgotMutation = useForgotPasswordMutation()
|
const forgotMutation = useForgotPasswordMutation()
|
||||||
const loginCodeMutation = useLoginWithCodeMutation()
|
const requestLoginOtpMutation = useRequestLoginOtpMutation()
|
||||||
|
|
||||||
const submitting = computed(
|
const submitting = computed(
|
||||||
() =>
|
() =>
|
||||||
@@ -97,37 +100,43 @@ const resending = computed(
|
|||||||
() =>
|
() =>
|
||||||
resendCodeMutation.isPending.value ||
|
resendCodeMutation.isPending.value ||
|
||||||
forgotMutation.isPending.value ||
|
forgotMutation.isPending.value ||
|
||||||
loginCodeMutation.isPending.value
|
requestLoginOtpMutation.isPending.value
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
const { isValid, payload } = await validate(form.value)
|
const { isValid, payload } = await validate(form.value)
|
||||||
if (!isValid) return
|
if (!isValid) return
|
||||||
const body = { phone: props.phoneNumber, verifyCode: payload.verifyCode }
|
const code = payload.verifyCode
|
||||||
|
|
||||||
if (props.mode === 'loginWithCode') {
|
if (props.mode === 'loginWithCode') {
|
||||||
const response = await verifyLoginMutation.mutateAsync(body)
|
const response = await verifyLoginMutation.mutateAsync({
|
||||||
|
phone: props.phoneNumber,
|
||||||
|
code,
|
||||||
|
deviceName: 'web',
|
||||||
|
})
|
||||||
applySession(response?.data ?? response)
|
applySession(response?.data ?? response)
|
||||||
redirectAfterLogin()
|
redirectAfterLogin()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.mode === 'forgotPassword') {
|
if (props.mode === 'forgotPassword') {
|
||||||
await verifyForgotMutation.mutateAsync(body)
|
const response = await verifyForgotMutation.mutateAsync({ phone: props.phoneNumber, code })
|
||||||
|
const data = response?.data ?? response
|
||||||
|
setResetTicket({ phone: props.phoneNumber, token: data?.resetToken })
|
||||||
router.push({ name: 'reset-password' })
|
router.push({ name: 'reset-password' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await verifySignupMutation.mutateAsync(body)
|
await verifySignupMutation.mutateAsync({ phone: props.phoneNumber, verifyCode: code })
|
||||||
emit('verified', body)
|
emit('verified', { phone: props.phoneNumber, verifyCode: code })
|
||||||
}
|
}
|
||||||
|
|
||||||
const onResend = async () => {
|
const onResend = async () => {
|
||||||
if (!props.phoneNumber) return
|
if (!props.phoneNumber) return
|
||||||
const body = { phoneNumber: props.phoneNumber }
|
const body = { phone: props.phoneNumber }
|
||||||
if (props.mode === 'forgotPassword') await forgotMutation.mutateAsync(body)
|
if (props.mode === 'forgotPassword') await forgotMutation.mutateAsync(body)
|
||||||
else if (props.mode === 'loginWithCode') await loginCodeMutation.mutateAsync(body)
|
else if (props.mode === 'loginWithCode') await requestLoginOtpMutation.mutateAsync(body)
|
||||||
else await resendCodeMutation.mutateAsync(body)
|
else await resendCodeMutation.mutateAsync({ phoneNumber: props.phoneNumber })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const phone = ref('')
|
||||||
|
const token = ref('')
|
||||||
|
|
||||||
|
export const usePasswordReset = () => {
|
||||||
|
const setResetTicket = ({ phone: phoneNumber, token: resetToken }) => {
|
||||||
|
if (phoneNumber) phone.value = phoneNumber
|
||||||
|
if (resetToken) token.value = resetToken
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearResetTicket = () => {
|
||||||
|
phone.value = ''
|
||||||
|
token.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return { phone, token, setResetTicket, clearResetTicket }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default usePasswordReset
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import useYup from '@/composables/useYup'
|
import useYup from '@/composables/useYup'
|
||||||
import useAuth from '@/composables/useAuth'
|
import useAuth from '@/composables/useAuth'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
@@ -63,15 +64,16 @@ import AuthShell from '@/features/auth/components/AuthShell.vue'
|
|||||||
import { useResetPasswordMutation } from '@/services/query/auth'
|
import { useResetPasswordMutation } from '@/services/query/auth'
|
||||||
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
|
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
|
||||||
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
|
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
|
||||||
import { useRedirectAfterLogin } from '@/features/auth/composables/useRedirectAfterLogin'
|
import { usePasswordReset } from '@/features/auth/composables/usePasswordReset'
|
||||||
|
|
||||||
const { user, applySession } = useAuth()
|
const { user } = useAuth()
|
||||||
const redirectAfterLogin = useRedirectAfterLogin()
|
const { phone: resetPhone, token: resetToken, clearResetTicket } = usePasswordReset()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const schema = resetPasswordSchema
|
const schema = resetPasswordSchema
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
phone: user.value?.phone || '',
|
phone: resetPhone.value || user.value?.phone || '',
|
||||||
password: '',
|
password: '',
|
||||||
passwordConfirmation: '',
|
passwordConfirmation: '',
|
||||||
})
|
})
|
||||||
@@ -82,9 +84,14 @@ const resetMutation = useResetPasswordMutation()
|
|||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
const { isValid, payload } = await validate(form.value)
|
const { isValid, payload } = await validate(form.value)
|
||||||
if (!isValid) return
|
if (!isValid) return
|
||||||
const response = await resetMutation.mutateAsync(payload)
|
await resetMutation.mutateAsync({
|
||||||
applySession(response?.data ?? response)
|
phone: form.value.phone,
|
||||||
redirectAfterLogin()
|
token: resetToken.value,
|
||||||
|
password: payload.password,
|
||||||
|
passwordConfirmation: payload.passwordConfirmation,
|
||||||
|
})
|
||||||
|
clearResetTicket()
|
||||||
|
router.push({ name: 'login' })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -226,7 +226,6 @@ import { studentProfileSchema, studentResetPasswordSchema } from '@/features/stu
|
|||||||
import {
|
import {
|
||||||
authKeys,
|
authKeys,
|
||||||
useGetMeQuery,
|
useGetMeQuery,
|
||||||
useResetPasswordMutation,
|
|
||||||
useUpdateProfileMutation,
|
useUpdateProfileMutation,
|
||||||
useUploadMediaMutation,
|
useUploadMediaMutation,
|
||||||
} from '@/services/query/auth'
|
} from '@/services/query/auth'
|
||||||
@@ -339,7 +338,7 @@ const onAvatarCropped = async (file) => {
|
|||||||
const onAvatarError = (msg) => toast.error(msg)
|
const onAvatarError = (msg) => toast.error(msg)
|
||||||
|
|
||||||
const updateMutation = useUpdateProfileMutation()
|
const updateMutation = useUpdateProfileMutation()
|
||||||
const resetPasswordMutation = useResetPasswordMutation()
|
const resetPasswordMutation = useUpdateProfileMutation()
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
const { isValid, payload } = await validate(form.value)
|
const { isValid, payload } = await validate(form.value)
|
||||||
@@ -369,7 +368,10 @@ const onResetPassword = async () => {
|
|||||||
})
|
})
|
||||||
if (!isValid) return
|
if (!isValid) return
|
||||||
try {
|
try {
|
||||||
await resetPasswordMutation.mutateAsync(payload)
|
await resetPasswordMutation.mutateAsync({
|
||||||
|
password: payload.password,
|
||||||
|
passwordConfirmation: payload.passwordConfirmation,
|
||||||
|
})
|
||||||
passwordForm.value.password = ''
|
passwordForm.value.password = ''
|
||||||
passwordForm.value.passwordConfirmation = ''
|
passwordForm.value.passwordConfirmation = ''
|
||||||
resetPasswordErrors()
|
resetPasswordErrors()
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import { endpoints } from '@/services/api/endpoints'
|
|||||||
|
|
||||||
export const apiLogin = (payload) => http.post(endpoints.login, payload)
|
export const apiLogin = (payload) => http.post(endpoints.login, payload)
|
||||||
|
|
||||||
export const apiLoginWithCode = (payload) => http.post(endpoints.loginWithCode2Step, payload)
|
export const apiRequestLoginOtp = (payload) => http.post(endpoints.requestLoginOtp, payload)
|
||||||
|
|
||||||
|
export const apiVerifyLoginOtp = (payload) => http.post(endpoints.verifyLoginOtp, payload)
|
||||||
|
|
||||||
export const apiRegister = (payload) => http.post(endpoints.register, payload)
|
export const apiRegister = (payload) => http.post(endpoints.register, payload)
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,16 @@ export const endpoints = {
|
|||||||
updateProfile: '/me',
|
updateProfile: '/me',
|
||||||
registerData: '/me/register-data',
|
registerData: '/me/register-data',
|
||||||
|
|
||||||
// FE-only — NOT in backend Postman doc; kept so existing UI/mock layer works.
|
requestLoginOtp: '/auth/login/otp/request',
|
||||||
|
verifyLoginOtp: '/auth/login/otp/verify',
|
||||||
|
forgotPassword: '/auth/password/forgot',
|
||||||
|
verifyForgotPasswordCode: '/auth/password/verify-otp',
|
||||||
|
resetPassword: '/auth/password/reset',
|
||||||
|
|
||||||
resendVerificationCodeForRegister: '/resend-verification-code',
|
resendVerificationCodeForRegister: '/resend-verification-code',
|
||||||
verifyCode: '/verify-code',
|
verifyCode: '/verify-code',
|
||||||
completeProfile: '/complete-profile',
|
completeProfile: '/complete-profile',
|
||||||
getRegistrationQuestionVideo: '/verification-registration-media',
|
getRegistrationQuestionVideo: '/verification-registration-media',
|
||||||
loginWithCode2Step: '/login-with-code',
|
|
||||||
forgotPassword: '/forgot-password',
|
|
||||||
verifyForgotPasswordCode: '/verify-forgot-password-code',
|
|
||||||
resetPassword: '/reset-password',
|
|
||||||
|
|
||||||
// ─── Geo ───────────────────────────────────────────────────────────────────
|
// ─── Geo ───────────────────────────────────────────────────────────────────
|
||||||
provinceList: '/provinces',
|
provinceList: '/provinces',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const pick = (arr, i) => arr[i % arr.length]
|
|||||||
const makeStudent = (i) => {
|
const makeStudent = (i) => {
|
||||||
const firstName = pick(firstNames, i)
|
const firstName = pick(firstNames, i)
|
||||||
const lastName = pick(lastNames, i + 2)
|
const lastName = pick(lastNames, i + 2)
|
||||||
const status = i % 3 === 0 ? 'rejected' : 'pending'
|
const status = i % 3 === 0 ? 'rejected' : 'verified'
|
||||||
const submittedAt = new Date(Date.now() - i * 86_400_000).toISOString()
|
const submittedAt = new Date(Date.now() - i * 86_400_000).toISOString()
|
||||||
return {
|
return {
|
||||||
id: 500 + i,
|
id: 500 + i,
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ const makeUser = (i, overrides = {}) => {
|
|||||||
fullName: `${firstName} ${lastName}`,
|
fullName: `${firstName} ${lastName}`,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
nationalCode: String(1_000_000_000 + i * 7).padStart(10, '0'),
|
nationalCode: String(1_000_000_000 + i * 7).padStart(10, '0'),
|
||||||
status: i % 5 === 0 ? 'pending' : 'approved',
|
status: i % 5 === 0 ? 'pending' : i % 3 === 0 ? 'verified' : 'approved',
|
||||||
roleId: roles.find((r) => r.name === role)?.id,
|
roleId: roles.find((r) => r.name === role)?.id,
|
||||||
address: {
|
address: {
|
||||||
address: 'آدرس نمونه',
|
address: 'آدرس نمونه',
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ register('GET', endpoints.getApprovedUsers, ({ query }) => {
|
|||||||
nationalCode: 'includes',
|
nationalCode: 'includes',
|
||||||
phoneNumber: 'includes',
|
phoneNumber: 'includes',
|
||||||
roleId: (item, v) => String(item.roleId) === String(v),
|
roleId: (item, v) => String(item.roleId) === String(v),
|
||||||
|
status: (item, v) => String(item.status) === String(v),
|
||||||
})
|
})
|
||||||
list = filterDateRange(list, query)
|
list = filterDateRange(list, query)
|
||||||
const { data: items, meta } = paginate(list, query)
|
const { data: items, meta } = paginate(list, query)
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ register('POST', endpoints.login, () => ({
|
|||||||
data: { user: currentMe, token: fakeToken },
|
data: { user: currentMe, token: fakeToken },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
register('POST', endpoints.loginWithCode2Step, () => ({
|
register('POST', endpoints.requestLoginOtp, () => ({
|
||||||
|
success: true,
|
||||||
|
message: 'If the phone is registered, a code has been sent.',
|
||||||
|
data: null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
register('POST', endpoints.verifyLoginOtp, () => ({
|
||||||
data: { user: currentMe, token: fakeToken },
|
data: { user: currentMe, token: fakeToken },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -26,15 +32,19 @@ register('POST', endpoints.resendVerificationCodeForRegister, () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
register('POST', endpoints.forgotPassword, () => ({
|
register('POST', endpoints.forgotPassword, () => ({
|
||||||
data: { message: 'کد بازیابی ارسال شد' },
|
success: true,
|
||||||
|
message: 'If the phone is registered, a code has been sent.',
|
||||||
|
data: null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
register('POST', endpoints.verifyForgotPasswordCode, () => ({
|
register('POST', endpoints.verifyForgotPasswordCode, () => ({
|
||||||
data: { token: fakeToken },
|
data: { reset_token: fakeToken },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
register('POST', endpoints.resetPassword, () => ({
|
register('POST', endpoints.resetPassword, () => ({
|
||||||
data: { message: 'رمز عبور با موفقیت تغییر کرد' },
|
success: true,
|
||||||
|
message: 'Password updated.',
|
||||||
|
data: null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
register('POST', endpoints.logout, () => ({ data: { message: 'خروج موفق' } }))
|
register('POST', endpoints.logout, () => ({ data: { message: 'خروج موفق' } }))
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
apiGetRegisterData,
|
apiGetRegisterData,
|
||||||
apiGetRegistrationQuestionVideo,
|
apiGetRegistrationQuestionVideo,
|
||||||
apiLogin,
|
apiLogin,
|
||||||
apiLoginWithCode,
|
apiRequestLoginOtp,
|
||||||
|
apiVerifyLoginOtp,
|
||||||
apiLogout,
|
apiLogout,
|
||||||
apiRegister,
|
apiRegister,
|
||||||
apiResendVerificationCode,
|
apiResendVerificationCode,
|
||||||
@@ -27,7 +28,9 @@ export const authKeys = {
|
|||||||
|
|
||||||
export const useLoginMutation = () => useMutation({ mutationFn: apiLogin })
|
export const useLoginMutation = () => useMutation({ mutationFn: apiLogin })
|
||||||
|
|
||||||
export const useLoginWithCodeMutation = () => useMutation({ mutationFn: apiLoginWithCode })
|
export const useRequestLoginOtpMutation = () => useMutation({ mutationFn: apiRequestLoginOtp })
|
||||||
|
|
||||||
|
export const useVerifyLoginOtpMutation = () => useMutation({ mutationFn: apiVerifyLoginOtp })
|
||||||
|
|
||||||
export const useRegisterMutation = () => useMutation({ mutationFn: apiRegister })
|
export const useRegisterMutation = () => useMutation({ mutationFn: apiRegister })
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user