mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-19 03:04:05 +00:00
Rename resources/scripts-v2 to resources/scripts and drop @v2 alias
Now that the legacy v1 frontend (commit 064bdf53) is gone, the v2 directory is the only frontend and the v2 suffix is just noise. Renames resources/scripts-v2 to resources/scripts via git mv (so git records the move as renames, preserving blame and log --follow), then bulk-rewrites the 152 files that imported via @v2/... to use @/scripts/... instead. The existing @ alias (resources/) covers the new path with no extra config needed.
Drops the now-unused @v2 alias from vite.config.js and points the laravel-vite-plugin entry at resources/scripts/main.ts. Updates the only blade reference (resources/views/app.blade.php) to match. The package.json test script (eslint ./resources/scripts) automatically targets the right place after the rename without any edit.
Verified: npm run build exits clean and the Vite warning lines now reference resources/scripts/plugins/i18n.ts, confirming every import resolved through the new path. git log --follow on any moved file walks back through its scripts-v2 history.
This commit is contained in:
105
resources/scripts/features/auth/views/ForgotPasswordView.vue
Normal file
105
resources/scripts/features/auth/views/ForgotPasswordView.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<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) {
|
||||
const normalized = handleApiError(err)
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: normalized.message,
|
||||
})
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
135
resources/scripts/features/auth/views/LoginView.vue
Normal file
135
resources/scripts/features/auth/views/LoginView.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<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 (err: unknown) {
|
||||
const { handleApiError } = await import('../../../utils/error-handling')
|
||||
const normalized = handleApiError(err)
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: normalized.message,
|
||||
})
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (window.demo_mode) {
|
||||
authStore.loginData.email = 'demo@invoiceshelf.com'
|
||||
authStore.loginData.password = 'demo'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<!-- Loading -->
|
||||
<div v-if="isLoading" class="mt-12 text-center">
|
||||
<BaseSpinner class="w-8 h-8 text-primary-400 mx-auto" />
|
||||
<p class="text-muted mt-4 text-sm">Loading invitation details...</p>
|
||||
</div>
|
||||
|
||||
<!-- Invalid/Expired -->
|
||||
<div v-else-if="error" class="mt-12 text-center">
|
||||
<BaseIcon
|
||||
name="ExclamationCircleIcon"
|
||||
class="w-12 h-12 mx-auto text-red-400 mb-4"
|
||||
/>
|
||||
<h2 class="text-lg font-semibold text-heading mb-2">
|
||||
Invalid Invitation
|
||||
</h2>
|
||||
<p class="text-sm text-muted mb-4">{{ error }}</p>
|
||||
<router-link
|
||||
to="/login"
|
||||
class="text-sm text-primary-400 hover:text-primary-500"
|
||||
>
|
||||
Go to Login
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Registration Form -->
|
||||
<div v-else class="mt-12">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-semibold text-heading">
|
||||
Create Your Account
|
||||
</h1>
|
||||
<p class="text-sm text-muted mt-2">
|
||||
You've been invited to join
|
||||
<strong class="text-heading">{{ invitationDetails.company_name }}</strong>
|
||||
as <strong class="text-heading">{{ invitationDetails.role_name }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submitRegistration">
|
||||
<BaseInputGroup
|
||||
label="Name"
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="form.name"
|
||||
:invalid="v$.name.$error"
|
||||
focus
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup label="Email" class="mb-4">
|
||||
<BaseInput
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
disabled
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
label="Password"
|
||||
:error="v$.password.$error && v$.password.$errors[0].$message"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="form.password"
|
||||
:type="isShowPassword ? 'text' : 'password'"
|
||||
:invalid="v$.password.$error"
|
||||
@input="v$.password.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<BaseIcon
|
||||
:name="isShowPassword ? 'EyeIcon' : 'EyeSlashIcon'"
|
||||
class="mr-1 text-muted cursor-pointer"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/>
|
||||
</template>
|
||||
</BaseInput>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
label="Confirm Password"
|
||||
:error="
|
||||
v$.password_confirmation.$error &&
|
||||
v$.password_confirmation.$errors[0].$message
|
||||
"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="form.password_confirmation"
|
||||
:type="isShowConfirmPassword ? 'text' : 'password'"
|
||||
:invalid="v$.password_confirmation.$error"
|
||||
@input="v$.password_confirmation.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<BaseIcon
|
||||
:name="isShowConfirmPassword ? 'EyeIcon' : 'EyeSlashIcon'"
|
||||
class="mr-1 text-muted cursor-pointer"
|
||||
@click="isShowConfirmPassword = !isShowConfirmPassword"
|
||||
/>
|
||||
</template>
|
||||
</BaseInput>
|
||||
</BaseInputGroup>
|
||||
|
||||
<div class="mt-5 mb-8">
|
||||
<router-link
|
||||
to="/login"
|
||||
class="text-sm text-primary-400 hover:text-body"
|
||||
>
|
||||
Already have an account? Log in
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSubmitting"
|
||||
:disabled="isSubmitting"
|
||||
type="submit"
|
||||
>
|
||||
Create Account & Join
|
||||
</BaseButton>
|
||||
</form>
|
||||
</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'
|
||||
|
||||
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 isLoading = ref<boolean>(true)
|
||||
const isSubmitting = ref<boolean>(false)
|
||||
const isShowPassword = ref<boolean>(false)
|
||||
const isShowConfirmPassword = 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 details = await authService.getInvitationDetails(token.value) as unknown as InvitationDetailsData
|
||||
invitationDetails.value = {
|
||||
email: details.email,
|
||||
company_name: details.company_name,
|
||||
role_name: details.role_name,
|
||||
}
|
||||
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 {
|
||||
const response = await authService.registerWithInvitation({
|
||||
name: form.name,
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
password_confirmation: form.password_confirmation,
|
||||
invitation_token: token.value,
|
||||
})
|
||||
|
||||
// Save the auth token before navigating (matching old version's pattern)
|
||||
localStorage.setItem('auth.token', `Bearer ${response.token}`)
|
||||
|
||||
router.push('/admin/dashboard')
|
||||
} catch (err: unknown) {
|
||||
const { handleApiError } = await import('../../../utils/error-handling')
|
||||
const { useNotificationStore } = await import('../../../stores/notification.store')
|
||||
const normalized = handleApiError(err)
|
||||
const notificationStore = useNotificationStore()
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: normalized.message,
|
||||
})
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
169
resources/scripts/features/auth/views/ResetPasswordView.vue
Normal file
169
resources/scripts/features/auth/views/ResetPasswordView.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<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) {
|
||||
const normalized = handleApiError(err)
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: normalized.message,
|
||||
})
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user