mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-18 18:54:07 +00:00
Phase 4a: Feature modules — layouts, auth, admin, dashboard,
customers, items, invoices, estimates, shared document form 77 files, 14451 lines. Typed layouts (CompanyLayout, AuthLayout, header, sidebar, company switcher), auth views (login, register, forgot/reset password), admin feature (dashboard, companies, users, settings with typed store), company features (dashboard with chart/ stats, customers CRUD, items CRUD, invoices CRUD with full store, estimates CRUD with full store), and shared document form components (items table, item row, totals, notes, tax popup, template select, exchange rate converter, calculation composable). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
6
resources/scripts-v2/features/auth/index.ts
Normal file
6
resources/scripts-v2/features/auth/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { authRoutes } from './routes'
|
||||
|
||||
export { default as LoginView } from './views/LoginView.vue'
|
||||
export { default as ForgotPasswordView } from './views/ForgotPasswordView.vue'
|
||||
export { default as ResetPasswordView } from './views/ResetPasswordView.vue'
|
||||
export { default as RegisterWithInvitationView } from './views/RegisterWithInvitationView.vue'
|
||||
52
resources/scripts-v2/features/auth/routes.ts
Normal file
52
resources/scripts-v2/features/auth/routes.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const AuthLayout = () => import('../../layouts/AuthLayout.vue')
|
||||
const LoginView = () => import('./views/LoginView.vue')
|
||||
const ForgotPasswordView = () => import('./views/ForgotPasswordView.vue')
|
||||
const ResetPasswordView = () => import('./views/ResetPasswordView.vue')
|
||||
const RegisterWithInvitationView = () => import('./views/RegisterWithInvitationView.vue')
|
||||
|
||||
export const authRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
component: AuthLayout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'login',
|
||||
component: LoginView,
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
title: 'Login',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/forgot-password',
|
||||
name: 'forgot-password',
|
||||
component: ForgotPasswordView,
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
title: 'Forgot Password',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/reset-password/:token',
|
||||
name: 'reset-password',
|
||||
component: ResetPasswordView,
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
title: 'Reset Password',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'register-with-invitation',
|
||||
component: RegisterWithInvitationView,
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
title: 'Register',
|
||||
},
|
||||
},
|
||||
]
|
||||
101
resources/scripts-v2/features/auth/views/ForgotPasswordView.vue
Normal file
101
resources/scripts-v2/features/auth/views/ForgotPasswordView.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<form id="loginForm" @submit.prevent="onSubmit">
|
||||
<BaseInputGroup
|
||||
:error="v$.email.$error && v$.email.$errors[0].$message"
|
||||
:label="$t('login.enter_email')"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="formData.email"
|
||||
:invalid="v$.email.$error"
|
||||
focus
|
||||
type="email"
|
||||
name="email"
|
||||
@input="v$.email.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
>
|
||||
<div v-if="!isSent">
|
||||
{{ $t('validation.send_reset_link') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ $t('validation.not_yet') }}
|
||||
</div>
|
||||
</BaseButton>
|
||||
|
||||
<div class="mt-4 mb-4 text-sm">
|
||||
<router-link
|
||||
to="/login"
|
||||
class="text-sm text-primary-400 hover:text-body"
|
||||
>
|
||||
{{ $t('general.back_to_login') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore } from '../../../stores/auth.store'
|
||||
import { useNotificationStore } from '../../../stores/notification.store'
|
||||
import { handleApiError } from '../../../utils/error-handling'
|
||||
|
||||
interface ForgotPasswordForm {
|
||||
email: string
|
||||
}
|
||||
|
||||
const notificationStore = useNotificationStore()
|
||||
const authStore = useAuthStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const formData = reactive<ForgotPasswordForm>({
|
||||
email: '',
|
||||
})
|
||||
|
||||
const isSent = ref<boolean>(false)
|
||||
const isLoading = ref<boolean>(false)
|
||||
|
||||
const rules = {
|
||||
email: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(rules, formData)
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
await authStore.forgotPassword({ email: formData.email })
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'Mail sent successfully',
|
||||
})
|
||||
|
||||
isSent.value = true
|
||||
} catch (err: unknown) {
|
||||
handleApiError(err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
129
resources/scripts-v2/features/auth/views/LoginView.vue
Normal file
129
resources/scripts-v2/features/auth/views/LoginView.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<form id="loginForm" class="mt-12 text-left" @submit.prevent="onSubmit">
|
||||
<BaseInputGroup
|
||||
:error="v$.email.$error && v$.email.$errors[0].$message"
|
||||
:label="$t('login.email')"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="authStore.loginData.email"
|
||||
:invalid="v$.email.$error"
|
||||
focus
|
||||
type="email"
|
||||
name="email"
|
||||
@input="v$.email.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:error="v$.password.$error && v$.password.$errors[0].$message"
|
||||
:label="$t('login.password')"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="authStore.loginData.password"
|
||||
:invalid="v$.password.$error"
|
||||
:type="inputType"
|
||||
name="password"
|
||||
@input="v$.password.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<BaseIcon
|
||||
:name="isShowPassword ? 'EyeIcon' : 'EyeSlashIcon'"
|
||||
class="mr-1 text-muted cursor-pointer"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/>
|
||||
</template>
|
||||
</BaseInput>
|
||||
</BaseInputGroup>
|
||||
|
||||
<div class="mt-5 mb-8">
|
||||
<div class="mb-4">
|
||||
<router-link
|
||||
to="forgot-password"
|
||||
class="text-sm text-primary-400 hover:text-body"
|
||||
>
|
||||
{{ $t('login.forgot_password') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseButton :loading="isLoading" type="submit">
|
||||
{{ $t('login.login') }}
|
||||
</BaseButton>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore } from '../../../stores/auth.store'
|
||||
import { useNotificationStore } from '../../../stores/notification.store'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
demo_mode?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const notificationStore = useNotificationStore()
|
||||
const authStore = useAuthStore()
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const isLoading = ref<boolean>(false)
|
||||
const isShowPassword = ref<boolean>(false)
|
||||
|
||||
const rules = {
|
||||
email: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
password: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => authStore.loginData)
|
||||
)
|
||||
|
||||
const inputType = computed<string>(() => {
|
||||
return isShowPassword.value ? 'text' : 'password'
|
||||
})
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
await authStore.login(authStore.loginData)
|
||||
|
||||
router.push('/admin/dashboard')
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'Logged in successfully.',
|
||||
})
|
||||
} catch {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (window.demo_mode) {
|
||||
authStore.loginData.email = 'demo@invoiceshelf.com'
|
||||
authStore.loginData.password = 'demo'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-center min-h-screen bg-surface-secondary">
|
||||
<div class="w-full max-w-md p-8">
|
||||
<!-- Logo -->
|
||||
<div class="mb-8 text-center">
|
||||
<MainLogo
|
||||
v-if="!loginPageLogo"
|
||||
class="inline-block w-48 h-auto text-primary-500"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
:src="loginPageLogo"
|
||||
class="inline-block w-48 h-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="isLoading" class="text-center">
|
||||
<p class="text-muted">Loading invitation details...</p>
|
||||
</div>
|
||||
|
||||
<!-- Invalid/Expired -->
|
||||
<div v-else-if="error" class="text-center">
|
||||
<BaseIcon
|
||||
name="ExclamationCircleIcon"
|
||||
class="w-16 h-16 mx-auto text-red-400 mb-4"
|
||||
/>
|
||||
<h1 class="text-xl font-semibold text-heading mb-2">
|
||||
Invalid Invitation
|
||||
</h1>
|
||||
<p class="text-muted">{{ error }}</p>
|
||||
<router-link to="/login" class="text-primary-500 mt-4 inline-block">
|
||||
Go to Login
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Registration Form -->
|
||||
<div v-else>
|
||||
<div class="text-center mb-6">
|
||||
<h1 class="text-2xl font-semibold text-heading">
|
||||
Create Your Account
|
||||
</h1>
|
||||
<p class="text-muted mt-2">
|
||||
You've been invited to join
|
||||
<strong>{{ invitationDetails.company_name }}</strong> as
|
||||
<strong>{{ invitationDetails.role_name }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BaseCard class="p-6">
|
||||
<form @submit.prevent="submitRegistration">
|
||||
<div class="space-y-4">
|
||||
<BaseInputGroup
|
||||
label="Name"
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="form.name"
|
||||
:invalid="v$.name.$error"
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup label="Email">
|
||||
<BaseInput
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
disabled
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
label="Password"
|
||||
:error="v$.password.$error && v$.password.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
:invalid="v$.password.$error"
|
||||
@input="v$.password.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
label="Confirm Password"
|
||||
:error="
|
||||
v$.password_confirmation.$error &&
|
||||
v$.password_confirmation.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="form.password_confirmation"
|
||||
type="password"
|
||||
:invalid="v$.password_confirmation.$error"
|
||||
@input="v$.password_confirmation.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSubmitting"
|
||||
:disabled="isSubmitting"
|
||||
class="w-full mt-6"
|
||||
type="submit"
|
||||
>
|
||||
Create Account & Join
|
||||
</BaseButton>
|
||||
</form>
|
||||
</BaseCard>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<router-link to="/login" class="text-sm text-muted hover:text-primary-500">
|
||||
Already have an account? Log in
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { helpers, required, minLength, sameAs } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { authService } from '../../../api/services/auth.service'
|
||||
import * as ls from '../../../utils/local-storage'
|
||||
import MainLogo from '../../../components/icons/MainLogo.vue'
|
||||
|
||||
interface InvitationDetailsData {
|
||||
email: string
|
||||
company_name: string
|
||||
role_name: string
|
||||
}
|
||||
|
||||
interface RegistrationForm {
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
password_confirmation: string
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loginPageLogo = computed<string | false>(() => {
|
||||
return (window as Record<string, unknown>).login_page_logo as string || false
|
||||
})
|
||||
|
||||
const isLoading = ref<boolean>(true)
|
||||
const isSubmitting = ref<boolean>(false)
|
||||
const error = ref<string | null>(null)
|
||||
const invitationDetails = ref<InvitationDetailsData>({
|
||||
email: '',
|
||||
company_name: '',
|
||||
role_name: '',
|
||||
})
|
||||
|
||||
const form = reactive<RegistrationForm>({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
})
|
||||
|
||||
const rules = computed(() => ({
|
||||
name: {
|
||||
required: helpers.withMessage('Name is required', required),
|
||||
},
|
||||
password: {
|
||||
required: helpers.withMessage('Password is required', required),
|
||||
minLength: helpers.withMessage('Password must be at least 8 characters', minLength(8)),
|
||||
},
|
||||
password_confirmation: {
|
||||
required: helpers.withMessage('Please confirm your password', required),
|
||||
sameAs: helpers.withMessage('Passwords do not match', sameAs(form.password)),
|
||||
},
|
||||
}))
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => form)
|
||||
)
|
||||
|
||||
const token = computed<string>(() => route.query.invitation as string)
|
||||
|
||||
onMounted(async () => {
|
||||
if (!token.value) {
|
||||
error.value = 'No invitation token provided.'
|
||||
isLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await authService.getInvitationDetails(token.value)
|
||||
const details = response.data
|
||||
invitationDetails.value = {
|
||||
email: details.email,
|
||||
company_name: details.company_name,
|
||||
role_name: details.invited_by,
|
||||
}
|
||||
form.email = details.email
|
||||
} catch {
|
||||
error.value = 'This invitation is invalid or has expired.'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
async function submitRegistration(): Promise<void> {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$invalid) return
|
||||
|
||||
isSubmitting.value = true
|
||||
|
||||
try {
|
||||
await authService.registerWithInvitation({
|
||||
name: form.name,
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
password_confirmation: form.password_confirmation,
|
||||
token: token.value,
|
||||
})
|
||||
|
||||
router.push('/admin/dashboard')
|
||||
} catch {
|
||||
// Validation errors handled by http interceptor
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
165
resources/scripts-v2/features/auth/views/ResetPasswordView.vue
Normal file
165
resources/scripts-v2/features/auth/views/ResetPasswordView.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<form id="loginForm" @submit.prevent="onSubmit">
|
||||
<BaseInputGroup
|
||||
:error="errorEmail"
|
||||
:label="$t('login.email')"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="formData.email"
|
||||
:invalid="v$.email.$error"
|
||||
focus
|
||||
type="email"
|
||||
name="email"
|
||||
@input="v$.email.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:error="errorPassword"
|
||||
:label="$t('login.password')"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="formData.password"
|
||||
:invalid="v$.password.$error"
|
||||
type="password"
|
||||
name="password"
|
||||
@input="v$.password.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:error="errorConfirmPassword"
|
||||
:label="$t('login.retype_password')"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="formData.password_confirmation"
|
||||
:invalid="v$.password_confirmation.$error"
|
||||
type="password"
|
||||
name="password_confirmation"
|
||||
@input="v$.password_confirmation.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseButton :loading="isLoading" type="submit" variant="primary">
|
||||
{{ $t('login.reset_password') }}
|
||||
</BaseButton>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, reactive } from 'vue'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, email, minLength, sameAs } from '@vuelidate/validators'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore } from '../../../stores/auth.store'
|
||||
import { useNotificationStore } from '../../../stores/notification.store'
|
||||
import { handleApiError } from '../../../utils/error-handling'
|
||||
|
||||
interface ResetPasswordForm {
|
||||
email: string
|
||||
password: string
|
||||
password_confirmation: string
|
||||
}
|
||||
|
||||
const notificationStore = useNotificationStore()
|
||||
const authStore = useAuthStore()
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const formData = reactive<ResetPasswordForm>({
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
})
|
||||
|
||||
const isLoading = ref<boolean>(false)
|
||||
|
||||
const rules = computed(() => ({
|
||||
email: { required, email },
|
||||
password: {
|
||||
required,
|
||||
minLength: minLength(8),
|
||||
},
|
||||
password_confirmation: {
|
||||
sameAsPassword: sameAs(formData.password),
|
||||
},
|
||||
}))
|
||||
|
||||
const v$ = useVuelidate(rules, formData)
|
||||
|
||||
const errorEmail = computed<string>(() => {
|
||||
if (!v$.value.email.$error) {
|
||||
return ''
|
||||
}
|
||||
if (v$.value.email.required.$invalid) {
|
||||
return t('validation.required')
|
||||
}
|
||||
if (v$.value.email.email) {
|
||||
return t('validation.email_incorrect')
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const errorPassword = computed<string>(() => {
|
||||
if (!v$.value.password.$error) {
|
||||
return ''
|
||||
}
|
||||
if (v$.value.password.required.$invalid) {
|
||||
return t('validation.required')
|
||||
}
|
||||
if (v$.value.password.minLength) {
|
||||
return t('validation.password_min_length', {
|
||||
count: v$.value.password.minLength.$params.min,
|
||||
})
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const errorConfirmPassword = computed<string>(() => {
|
||||
if (!v$.value.password_confirmation.$error) {
|
||||
return ''
|
||||
}
|
||||
if (v$.value.password_confirmation.sameAsPassword.$invalid) {
|
||||
return t('validation.password_incorrect')
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
await authStore.resetPassword({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
password_confirmation: formData.password_confirmation,
|
||||
token: route.params.token as string,
|
||||
})
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('login.password_reset_successfully'),
|
||||
})
|
||||
|
||||
router.push('/login')
|
||||
} catch (err: unknown) {
|
||||
handleApiError(err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user