mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-11 13:28:57 +00:00
feat: Use dynamic site name from app settings in document title and user login form. Fixes #49
Refactor: Use common AuthLayout for resetpassword, setpassword and use login view. Fix: Accessibility fixes to all auth forms
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col bg-background">
|
||||
<main class="flex-1 flex items-center justify-center p-4">
|
||||
<div class="w-full max-w-[450px]">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</main>
|
||||
<div class="flex justify-center mb-6">
|
||||
<a
|
||||
href="https://libredesk.io/"
|
||||
class="text-sm text-center text-muted-foreground"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Powered by Libredesk
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
@@ -1,16 +1,17 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import App from '../App.vue'
|
||||
import OuterApp from '../OuterApp.vue'
|
||||
import DashboardView from '../views/reports/DashboardView.vue'
|
||||
import InboxLayout from '../layouts/inbox/InboxLayout.vue'
|
||||
import SearchView from '../views/search/SearchView.vue'
|
||||
import UserLoginView from '../views/login/UserLoginView.vue'
|
||||
import App from '@/App.vue'
|
||||
import OuterApp from '@/OuterApp.vue'
|
||||
import DashboardView from '@/views/reports/DashboardView.vue'
|
||||
import InboxLayout from '@/layouts/inbox/InboxLayout.vue'
|
||||
import SearchView from '@/views/search/SearchView.vue'
|
||||
import AccountLayout from '@/layouts/account/AccountLayout.vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.vue'
|
||||
import ResetPasswordView from '@/views/outerapp/ResetPasswordView.vue'
|
||||
import SetPasswordView from '@/views/outerapp/SetPasswordView.vue'
|
||||
import UserLoginView from '@/views/auth/UserLoginView.vue'
|
||||
import ResetPasswordView from '@/views/auth/ResetPasswordView.vue'
|
||||
import SetPasswordView from '@/views/auth/SetPasswordView.vue'
|
||||
import InboxView from '@/views/inbox/InboxView.vue'
|
||||
import ConversationDetailView from '@/views/conversation/ConversationDetailView.vue'
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -465,7 +466,11 @@ const router = createRouter({
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
document.title = to.meta.title + ' - Libredesk'
|
||||
// Make page title with the route name and site name
|
||||
const appSettingsStore = useAppSettingsStore()
|
||||
const siteName = appSettingsStore.settings?.['app.site_name'] || 'Libredesk'
|
||||
const pageTitle = to.meta?.title || ''
|
||||
document.title = `${pageTitle} - ${siteName}`
|
||||
next()
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<AuthLayout>
|
||||
<Card class="bg-card box">
|
||||
<CardContent class="p-6 space-y-6">
|
||||
<div class="space-y-2 text-center">
|
||||
<CardTitle class="text-3xl font-bold text-foreground">{{
|
||||
t('auth.resetPassword')
|
||||
}}</CardTitle>
|
||||
<p class="text-muted-foreground">{{ t('auth.enterEmailForReset') }}</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="requestResetAction" class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="email" class="text-sm font-medium text-foreground">{{
|
||||
t('globals.terms.email')
|
||||
}}</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
:placeholder="t('auth.enterEmail')"
|
||||
v-model.trim="resetForm.email"
|
||||
:class="{ 'border-destructive': emailHasError }"
|
||||
class="w-full bg-card border-border text-foreground placeholder:text-muted-foreground rounded-lg py-2 px-3 focus:ring-2 focus:ring-ring focus:border-ring transition-all duration-200 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="w-full bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg py-2 transition-all duration-200 ease-in-out transform hover:scale-105"
|
||||
:disabled="isLoading"
|
||||
type="submit"
|
||||
>
|
||||
<span v-if="isLoading" class="flex items-center justify-center">
|
||||
<svg
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5 text-primary-foreground"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{{ t('auth.sending') }}
|
||||
</span>
|
||||
<span v-else>{{ t('auth.sendResetLink') }}</span>
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<Error
|
||||
v-if="errorMessage"
|
||||
:errorMessage="errorMessage"
|
||||
:border="true"
|
||||
class="w-full bg-destructive/10 text-destructive border-destructive/20 p-3 rounded-lg text-sm"
|
||||
/>
|
||||
|
||||
<div class="text-center">
|
||||
<router-link
|
||||
to="/"
|
||||
class="text-sm text-primary hover:text-primary/80 transition-all duration-200 ease-in-out"
|
||||
>
|
||||
{{ t('auth.backToLogin') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AuthLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { handleHTTPError } from '@/utils/http'
|
||||
import api from '@/api'
|
||||
import { validateEmail } from '@/utils/strings'
|
||||
import { useTemporaryClass } from '@/composables/useTemporaryClass'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Error } from '@/components/ui/error'
|
||||
import { Card, CardContent, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AuthLayout from '@/layouts/auth/AuthLayout.vue'
|
||||
|
||||
const errorMessage = ref('')
|
||||
const { t } = useI18n()
|
||||
const isLoading = ref(false)
|
||||
const emitter = useEmitter()
|
||||
const router = useRouter()
|
||||
const resetForm = ref({
|
||||
email: ''
|
||||
})
|
||||
|
||||
const validateForm = () => {
|
||||
if (!validateEmail(resetForm.value.email)) {
|
||||
errorMessage.value = t('globals.messages.invalidEmailAddress')
|
||||
useTemporaryClass('reset-password-container', 'animate-shake')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const requestResetAction = async () => {
|
||||
if (!validateForm()) return
|
||||
|
||||
errorMessage.value = ''
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
await api.resetPassword({
|
||||
email: resetForm.value.email
|
||||
})
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
description: t('auth.checkEmailForReset')
|
||||
})
|
||||
router.push({ name: 'login' })
|
||||
} catch (err) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(err).message
|
||||
})
|
||||
errorMessage.value = handleHTTPError(err).message
|
||||
useTemporaryClass('reset-password-container', 'animate-shake')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const emailHasError = computed(() => {
|
||||
return !validateEmail(resetForm.value.email) && resetForm.value.email !== ''
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<AuthLayout>
|
||||
<Card class="bg-card box">
|
||||
<CardContent class="p-6 space-y-6">
|
||||
<div class="space-y-2 text-center">
|
||||
<CardTitle class="text-3xl font-bold text-foreground">{{
|
||||
t('auth.setNewPassword')
|
||||
}}</CardTitle>
|
||||
<p class="text-muted-foreground">{{ t('auth.enterNewPasswordTwice') }}</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="setPasswordAction" class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="password" class="text-sm font-medium text-foreground">
|
||||
{{
|
||||
t('globals.messages.new', {
|
||||
name: t('globals.terms.password').toLowerCase()
|
||||
})
|
||||
}}
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:placeholder="t('auth.enterNewPassword')"
|
||||
v-model="passwordForm.password"
|
||||
:class="{ 'border-destructive': passwordHasError }"
|
||||
class="w-full bg-card border-border text-foreground placeholder:text-muted-foreground rounded-lg py-2 px-3 focus:ring-2 focus:ring-ring focus:border-ring transition-all duration-200 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="confirmPassword" class="text-sm font-medium text-foreground">{{
|
||||
t('auth.confirmPassword')
|
||||
}}</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:placeholder="t('auth.confirmNewPassword')"
|
||||
v-model="passwordForm.confirmPassword"
|
||||
:class="{ 'border-destructive': confirmPasswordHasError }"
|
||||
class="w-full bg-card border-border text-foreground placeholder:text-muted-foreground rounded-lg py-2 px-3 focus:ring-2 focus:ring-ring focus:border-ring transition-all duration-200 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="w-full bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg py-2 transition-all duration-200 ease-in-out transform hover:scale-105"
|
||||
:disabled="isLoading"
|
||||
type="submit"
|
||||
>
|
||||
<span v-if="isLoading" class="flex items-center justify-center">
|
||||
<svg
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5 text-primary-foreground"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{{ t('auth.settingPassword') }}
|
||||
</span>
|
||||
<span v-else>{{ t('auth.setNewPassword') }}</span>
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<Error
|
||||
v-if="errorMessage"
|
||||
:errorMessage="errorMessage"
|
||||
:border="true"
|
||||
class="w-full bg-destructive/10 text-destructive border-destructive/20 p-3 rounded-lg text-sm"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AuthLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { handleHTTPError } from '@/utils/http'
|
||||
import api from '@/api'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
||||
import { useTemporaryClass } from '@/composables/useTemporaryClass'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Error } from '@/components/ui/error'
|
||||
import { Card, CardContent, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AuthLayout from '@/layouts/auth/AuthLayout.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const errorMessage = ref('')
|
||||
const isLoading = ref(false)
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const emitter = useEmitter()
|
||||
const passwordForm = ref({
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
token: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
passwordForm.value.token = route.query.token
|
||||
if (!passwordForm.value.token) {
|
||||
router.push({ name: 'login' })
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
variant: 'destructive',
|
||||
description: 'Invalid reset link. Please request a new password reset link.'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const validateForm = () => {
|
||||
if (!passwordForm.value.password) {
|
||||
errorMessage.value = t('auth.passwordRequired')
|
||||
useTemporaryClass('set-password-container', 'animate-shake')
|
||||
return false
|
||||
}
|
||||
if (passwordForm.value.password !== passwordForm.value.confirmPassword) {
|
||||
errorMessage.value = t('auth.passwordsDoNotMatch')
|
||||
useTemporaryClass('set-password-container', 'animate-shake')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const setPasswordAction = async () => {
|
||||
if (!validateForm()) return
|
||||
|
||||
errorMessage.value = ''
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
await api.setPassword({
|
||||
token: passwordForm.value.token,
|
||||
password: passwordForm.value.password
|
||||
})
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
description: t('auth.passwordSetSuccess')
|
||||
})
|
||||
router.push({ name: 'login' })
|
||||
} catch (err) {
|
||||
errorMessage.value = handleHTTPError(err).message
|
||||
useTemporaryClass('set-password-container', 'animate-shake')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const passwordHasError = computed(() => {
|
||||
return passwordForm.value.password !== '' && passwordForm.value.password.length < 8
|
||||
})
|
||||
|
||||
const confirmPasswordHasError = computed(() => {
|
||||
return (
|
||||
passwordForm.value.confirmPassword !== '' &&
|
||||
passwordForm.value.password !== passwordForm.value.confirmPassword
|
||||
)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<AuthLayout>
|
||||
<Card
|
||||
class="bg-card box"
|
||||
:class="{ 'animate-shake': shakeCard }"
|
||||
id="login-container"
|
||||
ref="cardRef"
|
||||
>
|
||||
<CardContent class="p-6 space-y-6">
|
||||
<div class="space-y-2 text-center">
|
||||
<CardTitle class="text-3xl font-bold text-foreground">
|
||||
{{ appSettingsStore.settings?.['app.site_name'] || 'Libredesk' }}
|
||||
</CardTitle>
|
||||
<p class="text-muted-foreground">{{ t('auth.signIn') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="enabledOIDCProviders.length" class="space-y-4">
|
||||
<Button
|
||||
v-for="oidcProvider in enabledOIDCProviders"
|
||||
:key="oidcProvider.id"
|
||||
variant="outline"
|
||||
type="button"
|
||||
@click="redirectToOIDC(oidcProvider)"
|
||||
class="w-full bg-card hover:bg-secondary text-foreground border-border rounded-lg py-2 transition-all duration-200 ease-in-out transform hover:scale-105"
|
||||
>
|
||||
<img
|
||||
:src="oidcProvider.logo_url"
|
||||
width="20"
|
||||
class="mr-2"
|
||||
alt=""
|
||||
v-if="oidcProvider.logo_url"
|
||||
/>
|
||||
{{ oidcProvider.name }}
|
||||
</Button>
|
||||
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<span class="w-full border-t border-border"></span>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-xs uppercase">
|
||||
<span class="px-2 text-muted-foreground bg-card">{{ t('auth.orContinueWith') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="loginAction" class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="email" class="text-sm font-medium text-foreground">{{
|
||||
t('globals.terms.email')
|
||||
}}</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
:placeholder="t('auth.enterEmail')"
|
||||
v-model.trim="loginForm.email"
|
||||
:class="{ 'border-destructive': emailHasError }"
|
||||
class="w-full bg-card border-border text-foreground placeholder:text-muted-foreground rounded-lg py-2 px-3 focus:ring-2 focus:ring-ring focus:border-ring transition-all duration-200 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="password" class="text-sm font-medium text-foreground">{{
|
||||
t('auth.password')
|
||||
}}</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
:placeholder="t('auth.enterPassword')"
|
||||
v-model="loginForm.password"
|
||||
:class="{ 'border-destructive': passwordHasError }"
|
||||
class="w-full bg-card border-border text-foreground placeholder:text-muted-foreground rounded-lg py-2 px-3 focus:ring-2 focus:ring-ring focus:border-ring transition-all duration-200 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<router-link
|
||||
to="/reset-password"
|
||||
class="text-sm text-primary hover:text-primary/80 transition-all duration-200 ease-in-out"
|
||||
>
|
||||
{{ t('auth.forgotPassword') }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="w-full bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg py-2 transition-all duration-200 ease-in-out transform hover:scale-105"
|
||||
:disabled="isLoading"
|
||||
type="submit"
|
||||
>
|
||||
<span v-if="isLoading" class="flex items-center justify-center">
|
||||
<svg
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5 text-primary-foreground"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{{ t('auth.loggingIn') }}
|
||||
</span>
|
||||
<span v-else>{{ t('auth.signInButton') }}</span>
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<Error
|
||||
v-if="errorMessage"
|
||||
:errorMessage="errorMessage"
|
||||
:border="true"
|
||||
class="w-full bg-destructive/10 text-destructive border-destructive/20 p-3 rounded-lg text-sm"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AuthLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { handleHTTPError } from '@/utils/http'
|
||||
import api from '@/api'
|
||||
import { validateEmail } from '@/utils/strings'
|
||||
import { useTemporaryClass } from '@/composables/useTemporaryClass'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Error } from '@/components/ui/error'
|
||||
import { Card, CardContent, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
import AuthLayout from '@/layouts/auth/AuthLayout.vue'
|
||||
|
||||
const emitter = useEmitter()
|
||||
const { t } = useI18n()
|
||||
const errorMessage = ref('')
|
||||
const isLoading = ref(false)
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const shakeCard = ref(false)
|
||||
const loginForm = ref({
|
||||
email: '',
|
||||
password: ''
|
||||
})
|
||||
const oidcProviders = ref([])
|
||||
const appSettingsStore = useAppSettingsStore()
|
||||
|
||||
// Demo build has the credentials prefilled.
|
||||
const isDemoBuild = import.meta.env.VITE_DEMO_BUILD === 'true'
|
||||
|
||||
const demoCredentials = {
|
||||
email: 'demo@libredesk.io',
|
||||
password: 'demo@libredesk.io'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Prefill the login form with demo credentials if it's a demo build
|
||||
if (isDemoBuild) {
|
||||
loginForm.value.email = demoCredentials.email
|
||||
loginForm.value.password = demoCredentials.password
|
||||
}
|
||||
fetchOIDCProviders()
|
||||
})
|
||||
|
||||
const fetchOIDCProviders = async () => {
|
||||
try {
|
||||
const resp = await api.getAllEnabledOIDC()
|
||||
oidcProviders.value = resp.data.data
|
||||
} catch (error) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const redirectToOIDC = (provider) => {
|
||||
window.location.href = `/api/v1/oidc/${provider.id}/login`
|
||||
}
|
||||
|
||||
const validateForm = () => {
|
||||
if (!validateEmail(loginForm.value.email) && loginForm.value.email !== 'System') {
|
||||
errorMessage.value = t('globals.messages.invalidEmailAddress')
|
||||
useTemporaryClass('login-container', 'animate-shake')
|
||||
return false
|
||||
}
|
||||
if (!loginForm.value.password) {
|
||||
errorMessage.value = t('globals.messages.cannotBeEmpty', {
|
||||
name: t('globals.terms.password')
|
||||
})
|
||||
useTemporaryClass('login-container', 'animate-shake')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const loginAction = () => {
|
||||
if (!validateForm()) return
|
||||
|
||||
errorMessage.value = ''
|
||||
isLoading.value = true
|
||||
|
||||
api
|
||||
.login({
|
||||
email: loginForm.value.email,
|
||||
password: loginForm.value.password
|
||||
})
|
||||
.then((resp) => {
|
||||
if (resp?.data?.data) {
|
||||
userStore.setCurrentUser(resp.data.data)
|
||||
}
|
||||
router.push({ name: 'inboxes' })
|
||||
})
|
||||
.catch((error) => {
|
||||
errorMessage.value = handleHTTPError(error).message
|
||||
useTemporaryClass('login-container', 'animate-shake')
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const enabledOIDCProviders = computed(() => {
|
||||
return oidcProviders.value.filter((provider) => !provider.disabled)
|
||||
})
|
||||
|
||||
const emailHasError = computed(() => {
|
||||
const email = loginForm.value.email
|
||||
return email !== 'System' && !validateEmail(email) && email !== ''
|
||||
})
|
||||
|
||||
const passwordHasError = computed(
|
||||
() => !loginForm.value.password && loginForm.value.password !== ''
|
||||
)
|
||||
</script>
|
||||
@@ -1,246 +0,0 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col bg-background">
|
||||
<main class="flex-1 flex items-center justify-center p-4">
|
||||
<div class="w-full max-w-[400px]">
|
||||
<Card
|
||||
class="bg-card border border-border shadow-xl rounded-xl"
|
||||
:class="{ 'animate-shake': shakeCard }"
|
||||
id="login-container"
|
||||
ref="cardRef"
|
||||
>
|
||||
<CardContent class="p-8 space-y-6">
|
||||
<div class="space-y-2 text-center">
|
||||
<CardTitle class="text-3xl font-bold text-foreground">{{
|
||||
t('globals.app.name')
|
||||
}}</CardTitle>
|
||||
<p class="text-muted-foreground">{{ t('auth.signIn') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="enabledOIDCProviders.length" class="space-y-4">
|
||||
<Button
|
||||
v-for="oidcProvider in enabledOIDCProviders"
|
||||
:key="oidcProvider.id"
|
||||
variant="outline"
|
||||
type="button"
|
||||
@click="redirectToOIDC(oidcProvider)"
|
||||
class="w-full bg-card hover:bg-secondary text-foreground border-border rounded-lg py-2 transition-all duration-200 ease-in-out transform hover:scale-105"
|
||||
>
|
||||
<img
|
||||
:src="oidcProvider.logo_url"
|
||||
width="20"
|
||||
class="mr-2"
|
||||
alt=""
|
||||
v-if="oidcProvider.logo_url"
|
||||
/>
|
||||
{{ oidcProvider.name }}
|
||||
</Button>
|
||||
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<span class="w-full border-t border-border"></span>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-xs uppercase">
|
||||
<span class="px-2 text-muted-foreground bg-card">{{
|
||||
t('auth.orContinueWith')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="loginAction" class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="email" class="text-sm font-medium text-foreground">{{
|
||||
t('globals.terms.email')
|
||||
}}</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="text"
|
||||
:placeholder="t('auth.enterEmail')"
|
||||
v-model.trim="loginForm.email"
|
||||
:class="{ 'border-destructive': emailHasError }"
|
||||
class="w-full bg-card border-border text-foreground placeholder:text-muted-foreground rounded-lg py-2 px-3 focus:ring-2 focus:ring-ring focus:border-ring transition-all duration-200 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="password" class="text-sm font-medium text-foreground">{{
|
||||
t('auth.password')
|
||||
}}</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
:placeholder="t('auth.enterPassword')"
|
||||
v-model="loginForm.password"
|
||||
:class="{ 'border-destructive': passwordHasError }"
|
||||
class="w-full bg-card border-border text-foreground placeholder:text-muted-foreground rounded-lg py-2 px-3 focus:ring-2 focus:ring-ring focus:border-ring transition-all duration-200 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<router-link
|
||||
to="/reset-password"
|
||||
class="text-sm text-primary hover:text-primary/80 transition-all duration-200 ease-in-out"
|
||||
>
|
||||
{{ t('auth.forgotPassword') }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="w-full bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg py-2 transition-all duration-200 ease-in-out transform hover:scale-105"
|
||||
:disabled="isLoading"
|
||||
type="submit"
|
||||
>
|
||||
<span v-if="isLoading" class="flex items-center justify-center">
|
||||
<svg
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5 text-primary-foreground"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{{ t('auth.loggingIn') }}
|
||||
</span>
|
||||
<span v-else>{{ t('auth.signInButton') }}</span>
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<Error
|
||||
v-if="errorMessage"
|
||||
:errorMessage="errorMessage"
|
||||
:border="true"
|
||||
class="w-full bg-destructive/10 text-destructive border-destructive/20 p-3 rounded-lg text-sm"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { handleHTTPError } from '@/utils/http'
|
||||
import api from '@/api'
|
||||
import { validateEmail } from '@/utils/strings'
|
||||
import { useTemporaryClass } from '@/composables/useTemporaryClass'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Error } from '@/components/ui/error'
|
||||
import { Card, CardContent, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
||||
|
||||
const emitter = useEmitter()
|
||||
const { t } = useI18n()
|
||||
const errorMessage = ref('')
|
||||
const isLoading = ref(false)
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const shakeCard = ref(false)
|
||||
const loginForm = ref({
|
||||
email: '',
|
||||
password: ''
|
||||
})
|
||||
const oidcProviders = ref([])
|
||||
const isDemoBuild = import.meta.env.VITE_DEMO_BUILD === 'true'
|
||||
|
||||
const demoCredentials = {
|
||||
email: 'demo@libredesk.io',
|
||||
password: 'demo@libredesk.io'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Prefill the login form with demo credentials if it's a demo build
|
||||
if (isDemoBuild) {
|
||||
loginForm.value.email = demoCredentials.email
|
||||
loginForm.value.password = demoCredentials.password
|
||||
}
|
||||
fetchOIDCProviders()
|
||||
})
|
||||
|
||||
const fetchOIDCProviders = async () => {
|
||||
try {
|
||||
const resp = await api.getAllEnabledOIDC()
|
||||
oidcProviders.value = resp.data.data
|
||||
} catch (error) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const redirectToOIDC = (provider) => {
|
||||
window.location.href = `/api/v1/oidc/${provider.id}/login`
|
||||
}
|
||||
|
||||
const validateForm = () => {
|
||||
if (!validateEmail(loginForm.value.email) && loginForm.value.email !== 'System') {
|
||||
errorMessage.value = 'Invalid email address.'
|
||||
useTemporaryClass('login-container', 'animate-shake')
|
||||
return false
|
||||
}
|
||||
if (!loginForm.value.password) {
|
||||
errorMessage.value = 'Password cannot be empty.'
|
||||
useTemporaryClass('login-container', 'animate-shake')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const loginAction = () => {
|
||||
if (!validateForm()) return
|
||||
|
||||
errorMessage.value = ''
|
||||
isLoading.value = true
|
||||
|
||||
api
|
||||
.login({
|
||||
email: loginForm.value.email,
|
||||
password: loginForm.value.password
|
||||
})
|
||||
.then((resp) => {
|
||||
if (resp?.data?.data) {
|
||||
userStore.setCurrentUser(resp.data.data)
|
||||
}
|
||||
router.push({ name: 'inboxes' })
|
||||
})
|
||||
.catch((error) => {
|
||||
errorMessage.value = handleHTTPError(error).message
|
||||
useTemporaryClass('login-container', 'animate-shake')
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const enabledOIDCProviders = computed(() => {
|
||||
return oidcProviders.value.filter((provider) => !provider.disabled)
|
||||
})
|
||||
|
||||
const emailHasError = computed(() => {
|
||||
const email = loginForm.value.email
|
||||
return email !== 'System' && !validateEmail(email) && email !== ''
|
||||
})
|
||||
|
||||
const passwordHasError = computed(
|
||||
() => !loginForm.value.password && loginForm.value.password !== ''
|
||||
)
|
||||
</script>
|
||||
@@ -1,148 +0,0 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col bg-background">
|
||||
<main class="flex-1 flex items-center justify-center p-4">
|
||||
<div class="w-full max-w-[400px]">
|
||||
<Card class="bg-card border border-border shadow-xl rounded-xl">
|
||||
<CardContent class="p-8 space-y-6">
|
||||
<div class="space-y-2 text-center">
|
||||
<CardTitle class="text-3xl font-bold text-foreground">{{
|
||||
t('auth.resetPassword')
|
||||
}}</CardTitle>
|
||||
<p class="text-muted-foreground">{{ t('auth.enterEmailForReset') }}</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="requestResetAction" class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="email" class="text-sm font-medium text-foreground">{{
|
||||
t('globals.terms.email')
|
||||
}}</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
:placeholder="t('auth.enterEmail')"
|
||||
v-model.trim="resetForm.email"
|
||||
:class="{ 'border-destructive': emailHasError }"
|
||||
class="w-full bg-card border-border text-foreground placeholder:text-muted-foreground rounded-lg py-2 px-3 focus:ring-2 focus:ring-ring focus:border-ring transition-all duration-200 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="w-full bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg py-2 transition-all duration-200 ease-in-out transform hover:scale-105"
|
||||
:disabled="isLoading"
|
||||
type="submit"
|
||||
>
|
||||
<span v-if="isLoading" class="flex items-center justify-center">
|
||||
<svg
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5 text-primary-foreground"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{{ t('auth.sending') }}
|
||||
</span>
|
||||
<span v-else>{{ t('auth.sendResetLink') }}</span>
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<Error
|
||||
v-if="errorMessage"
|
||||
:errorMessage="errorMessage"
|
||||
:border="true"
|
||||
class="w-full bg-destructive/10 text-destructive border-destructive/20 p-3 rounded-lg text-sm"
|
||||
/>
|
||||
|
||||
<div class="text-center">
|
||||
<router-link
|
||||
to="/"
|
||||
class="text-sm text-primary hover:text-primary/80 transition-all duration-200 ease-in-out"
|
||||
>
|
||||
{{ t('auth.backToLogin') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { handleHTTPError } from '@/utils/http'
|
||||
import api from '@/api'
|
||||
import { validateEmail } from '@/utils/strings'
|
||||
import { useTemporaryClass } from '@/composables/useTemporaryClass'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Error } from '@/components/ui/error'
|
||||
import { Card, CardContent, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const errorMessage = ref('')
|
||||
const { t } = useI18n()
|
||||
const isLoading = ref(false)
|
||||
const emitter = useEmitter()
|
||||
const router = useRouter()
|
||||
const resetForm = ref({
|
||||
email: ''
|
||||
})
|
||||
|
||||
const validateForm = () => {
|
||||
if (!validateEmail(resetForm.value.email)) {
|
||||
errorMessage.value = 'Invalid email address.'
|
||||
useTemporaryClass('reset-password-container', 'animate-shake')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const requestResetAction = async () => {
|
||||
if (!validateForm()) return
|
||||
|
||||
errorMessage.value = ''
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
await api.resetPassword({
|
||||
email: resetForm.value.email
|
||||
})
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Reset link sent',
|
||||
description: 'Please check your email for the reset link.'
|
||||
})
|
||||
router.push({ name: 'login' })
|
||||
} catch (err) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Reset link sent',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(err).message
|
||||
})
|
||||
errorMessage.value = handleHTTPError(err).message
|
||||
useTemporaryClass('reset-password-container', 'animate-shake')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const emailHasError = computed(() => {
|
||||
return !validateEmail(resetForm.value.email) && resetForm.value.email !== ''
|
||||
})
|
||||
</script>
|
||||
@@ -1,178 +0,0 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col bg-background">
|
||||
<main class="flex-1 flex items-center justify-center p-4">
|
||||
<div class="w-full max-w-[400px]">
|
||||
<Card class="bg-card border border-border shadow-xl rounded-xl">
|
||||
<CardContent class="p-8 space-y-6">
|
||||
<div class="space-y-2 text-center">
|
||||
<CardTitle class="text-3xl font-bold text-foreground">{{
|
||||
t('auth.setNewPassword')
|
||||
}}</CardTitle>
|
||||
<p class="text-muted-foreground">{{ t('auth.enterNewPasswordTwice') }}</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="setPasswordAction" class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="password" class="text-sm font-medium text-foreground">
|
||||
{{
|
||||
t('globals.messages.new', {
|
||||
name: t('globals.terms.password')
|
||||
})
|
||||
}}
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
:placeholder="t('auth.enterNewPassword')"
|
||||
v-model="passwordForm.password"
|
||||
:class="{ 'border-destructive': passwordHasError }"
|
||||
class="w-full bg-card border-border text-foreground placeholder:text-muted-foreground rounded-lg py-2 px-3 focus:ring-2 focus:ring-ring focus:border-ring transition-all duration-200 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="confirmPassword" class="text-sm font-medium text-foreground">{{
|
||||
t('auth.confirmPassword')
|
||||
}}</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
:placeholder="t('auth.confirmNewPassword')"
|
||||
v-model="passwordForm.confirmPassword"
|
||||
:class="{ 'border-destructive': confirmPasswordHasError }"
|
||||
class="w-full bg-card border-border text-foreground placeholder:text-muted-foreground rounded-lg py-2 px-3 focus:ring-2 focus:ring-ring focus:border-ring transition-all duration-200 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="w-full bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg py-2 transition-all duration-200 ease-in-out transform hover:scale-105"
|
||||
:disabled="isLoading"
|
||||
type="submit"
|
||||
>
|
||||
<span v-if="isLoading" class="flex items-center justify-center">
|
||||
<svg
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5 text-primary-foreground"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{{ t('auth.settingPassword') }}
|
||||
</span>
|
||||
<span v-else>{{ t('auth.setNewPassword') }}</span>
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<Error
|
||||
v-if="errorMessage"
|
||||
:errorMessage="errorMessage"
|
||||
:border="true"
|
||||
class="w-full bg-destructive/10 text-destructive border-destructive/20 p-3 rounded-lg text-sm"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { handleHTTPError } from '@/utils/http'
|
||||
import api from '@/api'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
||||
import { useTemporaryClass } from '@/composables/useTemporaryClass'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Error } from '@/components/ui/error'
|
||||
import { Card, CardContent, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const errorMessage = ref('')
|
||||
const isLoading = ref(false)
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const emitter = useEmitter()
|
||||
const passwordForm = ref({
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
token: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
passwordForm.value.token = route.query.token
|
||||
if (!passwordForm.value.token) {
|
||||
router.push({ name: 'login' })
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: 'Invalid reset link. Please request a new password reset link.'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const validateForm = () => {
|
||||
if (!passwordForm.value.password) {
|
||||
errorMessage.value = t('auth.passwordRequired')
|
||||
useTemporaryClass('set-password-container', 'animate-shake')
|
||||
return false
|
||||
}
|
||||
if (passwordForm.value.password !== passwordForm.value.confirmPassword) {
|
||||
errorMessage.value = t('auth.passwordsDoNotMatch')
|
||||
useTemporaryClass('set-password-container', 'animate-shake')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const setPasswordAction = async () => {
|
||||
if (!validateForm()) return
|
||||
|
||||
errorMessage.value = ''
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
await api.setPassword({
|
||||
token: passwordForm.value.token,
|
||||
password: passwordForm.value.password
|
||||
})
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
description: t('auth.passwordSetSuccess')
|
||||
})
|
||||
router.push({ name: 'login' })
|
||||
} catch (err) {
|
||||
errorMessage.value = handleHTTPError(err).message
|
||||
useTemporaryClass('set-password-container', 'animate-shake')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const passwordHasError = computed(() => {
|
||||
return passwordForm.value.password !== '' && passwordForm.value.password.length < 8
|
||||
})
|
||||
|
||||
const confirmPasswordHasError = computed(() => {
|
||||
return (
|
||||
passwordForm.value.confirmPassword !== '' &&
|
||||
passwordForm.value.password !== passwordForm.value.confirmPassword
|
||||
)
|
||||
})
|
||||
</script>
|
||||
+4
-5
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"_.code": "en",
|
||||
"_.name": "English (en)",
|
||||
"globals.app.name": "Libredesk",
|
||||
"globals.terms.user": "User | Users",
|
||||
"globals.terms.contact": "Contact | Contacts",
|
||||
"globals.terms.agent": "Agent | Agents",
|
||||
@@ -190,21 +189,21 @@
|
||||
"auth.forgotPassword": "Forgot password?",
|
||||
"auth.loggingIn": "Logging in...",
|
||||
"auth.signInButton": "Sign in",
|
||||
"auth.resetPassword": "Reset Password",
|
||||
"auth.resetPassword": "Reset password",
|
||||
"auth.enterEmailForReset": "Enter your email to receive a password reset link.",
|
||||
"auth.sending": "Sending...",
|
||||
"auth.sendResetLink": "Send Reset Link",
|
||||
"auth.invalidEmail": "Invalid email address.",
|
||||
"auth.resetLinkSent": "Reset link sent",
|
||||
"auth.checkEmailForReset": "Please check your email for the reset link.",
|
||||
"auth.checkEmailForReset": "Check your email for the password reset link.",
|
||||
"auth.backToLogin": "Back to login",
|
||||
"auth.setNewPassword": "Set new password",
|
||||
"auth.enterNewPasswordTwice": "Please enter your new password twice to confirm.",
|
||||
"auth.enterNewPasswordTwice": "Enter your new password twice to confirm.",
|
||||
"auth.enterNewPassword": "Enter new password",
|
||||
"auth.confirmPassword": "Confirm password",
|
||||
"auth.confirmNewPassword": "Confirm new password",
|
||||
"auth.settingPassword": "Setting password...",
|
||||
"auth.invalidResetLink": "Invalid reset link. Please request a new password reset link.",
|
||||
"auth.invalidResetLink": "Invalid reset link. Request a new password reset link.",
|
||||
"auth.passwordRequired": "Password is required.",
|
||||
"auth.passwordsDoNotMatch": "Passwords do not match.",
|
||||
"auth.passwordSetSuccess": "You can now login with your new password.",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"_.code": "mr",
|
||||
"_.name": "मराठी (mr)",
|
||||
"globals.app.name": "लिब्रेडेस्क",
|
||||
"globals.terms.user": "वापरकर्ता | वापरकर्ते",
|
||||
"globals.terms.contact": "संपर्क | संपर्क",
|
||||
"globals.terms.agent": "एजंट | एजंट्स",
|
||||
|
||||
Reference in New Issue
Block a user