first commit

This commit is contained in:
Noor E Ilahi
2026-01-09 12:54:53 +05:30
commit 7ccf44f7da
1070 changed files with 113036 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
<template>
<div>
<validation-provider v-slot="{ errors }" rules="required|email" mode="passive" name="E-mail">
<div class="input-group">
<small>E-Mail</small>
<at-input
v-model="user.email"
name="login"
:status="errors.length > 0 ? 'error' : ''"
placeholder="E-Mail"
icon="mail"
type="text"
required
@keydown.native.enter.prevent="submit"
></at-input>
<small>{{ errors[0] }}</small>
</div>
<!-- /.input-group -->
</validation-provider>
<validation-provider v-slot="{ errors }" rules="required" mode="passive" :name="$t('field.password')">
<div class="input-group">
<small>{{ $t('field.password') }}</small>
<at-input
v-model="user.password"
name="password"
:status="errors.length > 0 ? 'error' : ''"
:placeholder="$t('field.password')"
type="password"
icon="lock"
required
@keydown.native.enter.prevent="submit"
></at-input>
<small>{{ errors[0] }}</small>
</div>
<!-- /.input-group -->
</validation-provider>
</div>
</template>
<script>
import { ValidationProvider } from 'vee-validate';
export default {
name: 'AuthInput',
components: {
ValidationProvider,
},
data() {
return {
user: {
email: null,
password: null,
},
};
},
methods: {
submit() {
this.$emit('submit');
},
},
watch: {
'user.email'(value) {
// Trim space
this.user.email = value.replace(/\s/, '');
this.$emit('change', this.user);
},
'user.password'() {
this.$emit('change', this.user);
},
},
};
</script>

View File

@@ -0,0 +1,178 @@
<template>
<div class="login row at-row no-gutter">
<div class="login__wrap">
<div class="login__form">
<div class="box">
<div class="top">
<div class="static-message">
<div class="logo" />
</div>
<h1 class="login__title">Cattr</h1>
</div>
<template v-if="error">
<div>
<at-alert :message="$t('auth.desktop_error')" class="login__error" type="error" />
</div>
<at-button class="login__btn" type="primary" @click="commonLogin"
>{{ $t('auth.switch_to_common') }}
</at-button>
</template>
<template v-else>
<at-alert :message="$t('auth.desktop_working')" class="login__error" type="info" />
</template>
</div>
</div>
<a class="login__slogan" href="https://cattr.app" v-html="slogan" />
</div>
<div class="hero col-16" />
</div>
</template>
<style lang="scss" scoped>
.login {
flex-wrap: nowrap;
height: 100vh;
margin: 0;
max-height: 100vh;
position: relative;
width: 100%;
&__wrap {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
}
&__form {
flex: 8;
width: 100%;
}
&__slogan {
align-content: flex-start;
color: $gray-3;
display: flex;
flex: 1;
justify-content: center;
margin: 0;
width: 100%;
}
&__title {
color: $black-900;
font-size: 1.8rem;
text-align: center;
}
&__btn {
margin-bottom: 1rem;
}
&__error {
margin-bottom: 1rem;
overflow: initial;
text-align: center;
}
.box {
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
padding: 0 $spacing-08;
width: 100%;
.top {
display: flex;
flex-flow: column nowrap;
margin-bottom: $layout-01;
.static-message {
align-items: center;
display: flex;
flex-flow: column nowrap;
.logo {
align-items: center;
background: url('../../assets/logo.svg');
background-size: cover;
border-radius: 10px;
color: #ffffff;
display: flex;
font-size: 1.8rem;
font-weight: bold;
height: 60px;
justify-content: center;
text-transform: uppercase;
width: 60px;
}
}
}
}
.link {
color: $blue-1;
font-weight: 600;
text-align: center;
}
::v-deep .input-group {
margin-bottom: 0.75rem;
}
.hero {
background: #6159e6 url('../../assets/login.svg') no-repeat;
background-size: 100%;
display: flex;
}
}
</style>
<script>
import sloganGenerator from '@/helpers/sloganGenerator';
export default {
name: 'desktop-login',
computed: {
slogan() {
return sloganGenerator();
},
},
data() {
return {
error: false,
};
},
methods: {
commonLogin() {
this.$router.replace({ name: 'auth.login' });
},
finish(error = true) {
this.error = error;
this.$Loading.error();
},
},
mounted() {
this.$Loading.start();
if (location.search.length === 0) {
this.finish();
} else {
const query = location.search.substr(1).split('=');
if (query[0] !== 'token' && query.length !== 2) {
this.finish();
} else {
const apiService = this.$store.getters['user/apiService'];
apiService
.attemptDesktopLogin(query[1])
.then(() => apiService.getCompanyData())
.then(() => this.finish(false))
.catch(() => this.finish());
}
}
},
};
</script>

View File

@@ -0,0 +1,270 @@
<template>
<div class="login row at-row no-gutter">
<div class="login__wrap">
<div class="login__form">
<validation-observer ref="observer" class="box" tag="div" @submit.prevent="submit">
<div class="top">
<div class="static-message">
<div class="logo"></div>
</div>
<h1 class="login__title">Cattr</h1>
</div>
<div>
<at-alert
v-if="error"
type="error"
class="login__error"
closable
:message="error"
@on-close="error = null"
/>
<component :is="config.authInput" @change="change" @submit="submit" />
<vue-recaptcha
v-if="recaptchaKey"
ref="recaptcha"
:loadRecaptchaScript="true"
:sitekey="recaptchaKey"
class="recaptcha"
@verify="onCaptchaVerify"
@expired="onCaptchaExpired"
></vue-recaptcha>
</div>
<at-button
class="login__btn"
native-type="submit"
type="primary"
:loading="isLoading"
:disabled="isLoading"
@click="submit"
>{{ $t('auth.submit') }}</at-button
>
<router-link class="link" to="/auth/password/reset">{{ $t('auth.forgot_password') }}</router-link>
</validation-observer>
</div>
<a class="login__slogan" href="https://cattr.app" v-html="slogan" />
</div>
<div class="hero col-lg-16 col-md-14 col-sm-12"></div>
</div>
</template>
<script>
import { ValidationObserver } from 'vee-validate';
import { VueRecaptcha } from 'vue-recaptcha';
import AuthInput from './AuthInput';
import sloganGenerator from '@/helpers/sloganGenerator';
import has from 'lodash/has';
export const config = { authInput: AuthInput };
export default {
name: 'Login',
components: {
ValidationObserver,
VueRecaptcha,
AuthInput,
},
data() {
return {
user: {
email: null,
password: null,
recaptcha: null,
},
recaptchaKey: null,
error: null,
isLoading: false,
};
},
computed: {
config() {
return config;
},
slogan() {
return sloganGenerator();
},
},
mounted() {
if (this.$store.getters['user/isLoggedIn']) {
this.$router.push({ name: 'dashboard' });
}
},
methods: {
getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
},
onCaptchaVerify(response) {
this.user.recaptcha = response;
},
onCaptchaExpired() {
this.$refs.recaptcha.reset();
},
change(user) {
this.user = { ...this.user, ...user };
},
async submit() {
const valid = await this.$refs.observer.validate();
if (!valid) {
return;
}
this.$Loading.start();
this.isLoading = true;
const apiService = this.$store.getters['user/apiService'];
try {
if ('grecaptcha' in window) {
this.$refs.recaptcha.reset();
}
await apiService.attemptLogin(this.user);
await apiService.getCompanyData();
this.error = null;
this.$Loading.finish();
} catch (e) {
this.$Loading.error();
if (has(e, 'response.status')) {
if (e.response.status === 429 && this.recaptchaKey === null) {
this.recaptchaKey = e.response.data.info.site_key;
}
let message;
if (e.response.status === 401) {
message = this.$t('auth.message.user_not_found');
} else if (e.response.status === 429) {
message = this.$t('auth.message.solve_captcha');
} else if (e.response.status === 503) {
message = this.$t('auth.message.data_reset');
} else {
message = this.$t('auth.message.auth_error');
}
this.error = message;
}
} finally {
this.isLoading = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.login {
flex-wrap: nowrap;
height: 100vh;
margin: 0;
max-height: 100vh;
position: relative;
width: 100%;
&__wrap {
align-items: center;
display: flex;
width: 100%;
flex-direction: column;
justify-content: center;
}
&__form {
width: 100%;
flex: 8;
}
&__slogan {
flex: 1;
margin: 0;
width: 100%;
display: flex;
justify-content: center;
align-content: flex-start;
color: $gray-3;
}
&__title {
color: $black-900;
font-size: 1.8rem;
text-align: center;
}
&__btn {
margin-bottom: 1rem;
}
&__error {
margin-bottom: 1rem;
overflow: initial;
}
.box {
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
padding: 0 $spacing-08;
width: 100%;
.top {
display: flex;
flex-flow: column nowrap;
margin-bottom: $layout-01;
.static-message {
align-items: center;
display: flex;
flex-flow: column nowrap;
.logo {
align-items: center;
background: url('../../assets/logo.svg');
background-size: cover;
border-radius: 10px;
color: #ffffff;
display: flex;
font-size: 1.8rem;
font-weight: bold;
height: 60px;
justify-content: center;
text-transform: uppercase;
width: 60px;
}
}
}
.recaptcha {
margin-bottom: 10px;
}
}
.link {
color: $blue-1;
font-weight: 600;
text-align: center;
}
::v-deep .input-group {
margin-bottom: 0.75rem;
}
.hero {
background: url('../../assets/login.svg') #6159e6;
background-repeat: no-repeat;
background-size: 100%;
display: flex;
}
}
</style>

View File

@@ -0,0 +1,232 @@
<template>
<div class="content-wrapper">
<div v-if="isTokenValid" class="container">
<div class="at-container">
<div class="at-container__inner">
<div v-if="isRegisterSuccess">
<div class="header-text">
<i class="icon icon-check"></i>
<h2 class="header-text__title">
{{ $t('register.success_title') }}
</h2>
<p class="header-text__subtitle">{{ $t('register.success_subtitle') }}</p>
<router-link to="/auth/login">{{ $t('reset.go_away') }}</router-link>
</div>
</div>
<div v-else class="row">
<div class="col-6 col-offset-9">
<div class="header-text">
<h2 class="header-text__title">
{{ $t('register.title') }}
</h2>
<p class="header-text__subtitle">
{{ $t('register.subtitle') }}
</p>
</div>
<at-alert
v-if="errorMessage"
type="error"
class="alert"
closable
:message="errorMessage"
@on-close="errorMessage = null"
/>
<validation-observer v-slot="{ invalid }">
<validation-provider v-slot="{ errors }" rules="required|email" name="E-mail">
<div class="input-group">
<small>E-Mail</small>
<at-input
v-model="email"
name="login"
:status="errors.length > 0 ? 'error' : ''"
placeholder="E-Mail"
icon="mail"
type="text"
disabled="true"
>
</at-input>
<p class="error-message">
<small>{{ errors[0] }}</small>
</p>
</div>
</validation-provider>
<validation-provider v-slot="{ errors }" rules="required" :name="$t('field.full_name')">
<div class="input-group">
<small>{{ $t('field.full_name') }}</small>
<at-input
v-model="fullName"
name="full_name"
:status="errors.length > 0 ? 'error' : ''"
:placeholder="$t('field.full_name')"
icon="user"
type="text"
>
</at-input>
<p class="error-message">
<small>{{ errors[0] }}</small>
</p>
</div>
</validation-provider>
<validation-provider
v-slot="{ errors }"
rules="required|min:6"
vid="password"
:name="$t('field.password')"
>
<div class="input-group">
<small>{{ $t('field.password') }}</small>
<at-input
v-model="password"
name="password"
:status="errors.length > 0 ? 'error' : ''"
:placeholder="$t('field.full_name')"
icon="lock"
type="password"
>
</at-input>
<p class="error-message">
<small>{{ errors[0] }}</small>
</p>
</div>
</validation-provider>
<validation-provider
v-slot="{ errors }"
rules="required|min:6|confirmed:password"
:name="$t('reset.confirm_password')"
>
<div class="input-group">
<small>{{ $t('reset.confirm_password') }}</small>
<at-input
v-model="passwordConfirmation"
name="passwordConfirmation"
:status="errors.length > 0 ? 'error' : ''"
:placeholder="$t('reset.confirm_password')"
icon="lock"
type="password"
>
</at-input>
<p class="error-message">
<small>{{ errors[0] }}</small>
</p>
</div>
</validation-provider>
<at-button
class="btn"
native-type="submit"
type="primary"
:disabled="invalid || isLoading"
:loading="isLoading"
@click="register"
>{{ $t('register.register_btn') }}</at-button
>
</validation-observer>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- /.content-wrapper -->
</template>
<script>
import { ValidationObserver, ValidationProvider } from 'vee-validate';
import axios from 'axios';
export default {
name: 'ResetPassword',
components: {
ValidationProvider,
ValidationObserver,
},
created() {
if (this.$route.query.token) {
this.token = this.$route.query.token;
this.validateToken();
} else {
this.$router.push({ name: 'not-found' });
}
},
data() {
return {
email: null,
password: null,
passwordConfirmation: null,
fullName: null,
token: null,
isTokenValid: false,
isLoading: false,
isRegisterSuccess: false,
errorMessage: null,
};
},
methods: {
async validateToken() {
try {
const { data } = await axios.get(`/auth/register/${this.token}`);
this.email = data.email;
this.isTokenValid = true;
} catch ({ response }) {
if (response.status === 404) {
this.$router.replace({ name: 'not-found' });
}
}
},
async register() {
this.isLoading = true;
const data = {
email: this.email,
full_name: this.fullName,
password: this.password,
password_confirmation: this.passwordConfirmation,
};
try {
await axios.post(`auth/register/${this.token}`, data);
this.isRegisterSuccess = true;
} catch ({ response }) {
this.errorMessage = response.data.error;
} finally {
this.isLoading = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.header-text {
text-align: center;
&__title {
margin-bottom: 1rem;
}
&__subtitle {
margin-bottom: 1rem;
}
}
.btn {
width: 100%;
}
.input-group {
margin-bottom: $layout-01;
}
.icon {
margin-bottom: 1rem;
font-size: 92px;
&-check {
color: $green-1;
}
}
.alert {
margin-bottom: $layout-01;
}
</style>

View File

@@ -0,0 +1,281 @@
<template>
<div class="content-wrapper">
<div class="container">
<div class="at-container crud__content">
<at-steps :current="currentStep" class="steps col-lg-offset-4">
<at-step
:title="$t('reset.step', { n: 1 })"
:description="$t('reset.step_description.step_1')"
></at-step>
<at-step
:title="$t('reset.step', { n: 2 })"
:description="$t('reset.step_description.step_2')"
></at-step>
<at-step
:title="$t('reset.step', { n: 3 })"
:description="$t('reset.step_description.step_3')"
></at-step>
<at-step
:title="$t('reset.step', { n: 4 })"
:description="$t('reset.step_description.step_4')"
></at-step>
</at-steps>
<div class="row">
<div v-if="currentStep == 0" class="col-6 col-offset-9">
<validation-observer v-slot="{ invalid }">
<div class="header-text">
<h2 class="header-text__title">
{{ $t('reset.tabs.enter_email.title') }}
</h2>
<p class="header-text__subtitle">
{{ $t('reset.tabs.enter_email.subtitle') }}
</p>
</div>
<validation-provider v-slot="{ errors }" rules="required|email">
<small>E-Mail</small>
<at-input
v-model="email"
name="login"
:status="errors.length > 0 ? 'error' : ''"
placeholder="E-Mail"
icon="mail"
type="text"
:disabled="disabledForm"
>
</at-input>
</validation-provider>
<at-button
class="btn"
native-type="submit"
type="primary"
:disabled="invalid || disabledForm"
@click="resetPassword"
>{{ $t('reset.reset_password') }}</at-button
>
</validation-observer>
</div>
<div v-if="currentStep == 1" class="col-6 col-offset-9">
<div class="header-text">
<i class="icon icon-mail"></i>
<h2 class="header-text__title">
{{ $t('reset.tabs.check_email.title') }}
</h2>
<p class="header-text__subtitle">
{{ $t('reset.tabs.check_email.subtitle') }}
</p>
</div>
</div>
<div v-if="currentStep == 2" class="col-6 col-offset-9">
<validation-observer v-if="isValidToken" ref="form" v-slot="{ invalid }">
<div class="header-text">
<h2 class="header-text__title">
{{ $t('reset.tabs.new_password.title') }}
</h2>
<p class="header-text__subtitle">
{{ $t('reset.tabs.new_password.subtitle') }}
</p>
</div>
<validation-provider
v-slot="{ errors }"
rules="required|min:6"
:name="$t('field.password')"
vid="password"
>
<small>{{ $t('field.password') }}</small>
<at-input
v-model="password"
:name="$t('field.password')"
:status="errors.length > 0 ? 'error' : ''"
:placeholder="$t('field.password')"
icon="lock"
type="password"
:disabled="disabledForm"
>
</at-input>
<p class="error-message">
<small>{{ errors[0] }}</small>
</p>
</validation-provider>
<validation-provider
v-slot="{ errors }"
rules="required|min:6|confirmed:password"
:name="$t('reset.confirm_password')"
vid="passwordConfirmation"
>
<small>{{ $t('reset.confirm_password') }}</small>
<at-input
v-model="passwordConfirmation"
name="passwordConfirmation"
:status="errors.length > 0 ? 'error' : ''"
:placeholder="$t('reset.confirm_password')"
icon="lock"
type="password"
:disabled="disabledForm"
>
</at-input>
<p class="error-message">
<small>{{ errors[0] }}</small>
</p>
</validation-provider>
<at-button
class="btn"
native-type="submit"
type="primary"
:disabled="invalid || disabledForm"
@click="submitNewPassword"
>{{ $t('control.submit') }}</at-button
>
</validation-observer>
<div v-else>
<div class="header-text">
<h2 class="header-text__title">
{{ $t('reset.page_is_not_available') }}
</h2>
<router-link to="/auth/login">{{ $t('reset.go_away') }}</router-link>
</div>
</div>
</div>
<div v-if="currentStep == 3" class="col-6 col-offset-9">
<div class="header-text">
<i class="icon icon-check"></i>
<h2 class="header-text__title">
{{ $t('reset.tabs.success.title') }}
</h2>
<p class="header-text__subtitle"></p>
<router-link to="/auth/login">{{ $t('reset.go_away') }}</router-link>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- /.content-wrapper -->
</template>
<script>
import { ValidationObserver, ValidationProvider } from 'vee-validate';
import AuthService from '@/services/auth.service';
export default {
name: 'ResetPassword',
components: {
ValidationProvider,
ValidationObserver,
},
created() {
if (this.$route.query.token && this.$route.query.email) {
this.currentStep = 2;
this.validateToken();
}
},
data() {
return {
email: null,
password: null,
passwordConfirmation: null,
currentStep: 0,
disabledForm: false,
isValidToken: true,
authService: new AuthService(),
};
},
methods: {
async resetPassword() {
this.disabledForm = true;
const payload = {
email: this.email,
};
try {
await this.authService.resetPasswordRequest(payload);
this.currentStep = 1;
} catch (e) {
//
} finally {
this.disabledForm = false;
}
},
async validateToken() {
const payload = {
email: this.$route.query.email,
token: this.$route.query.token,
};
try {
await this.authService.resetPasswordValidateToken(payload);
this.isValidToken = true;
} catch (e) {
this.isValidToken = false;
}
},
async submitNewPassword() {
this.disabledForm = true;
const payload = {
email: this.$route.query.email,
token: this.$route.query.token,
password: this.password,
password_confirmation: this.passwordConfirmation,
};
try {
await this.authService.resetPasswordProcess(payload);
this.currentStep = 3;
this.disabledForm = false;
} catch (e) {
//
} finally {
this.disabledForm = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.steps {
margin-bottom: 1.5rem;
}
.header-text {
text-align: center;
&__title {
margin-bottom: 1rem;
}
&__subtitle {
margin-bottom: 1rem;
}
}
.icon {
margin-bottom: 1rem;
font-size: 92px;
&-mail {
color: $blue-2;
}
&-check {
color: $green-1;
}
}
.error-message {
margin-bottom: 1rem;
}
.btn {
width: 100%;
}
.at-input {
margin-bottom: 0.75rem;
}
</style>