refs #4922 password recovery, app store, error handler, fixes
gitea/hedera-web/pipeline/head This commit looks good Details

This commit is contained in:
Juan Ferrer 2022-12-09 11:28:38 +01:00
parent 0d0be4ee5f
commit 7e26aa773c
21 changed files with 314 additions and 96 deletions

View File

@ -58,8 +58,7 @@
}, },
"scripts": { "scripts": {
"front": "webpack serve --open", "front": "webpack serve --open",
"back": "cd ../salix && gulp backOnly", "back": "cd ../vn-database && myvc start && cd ../salix && gulp backOnly",
"db": "cd ../vn-database && myvc run -d",
"build": "rm -rf build/ ; webpack", "build": "rm -rf build/ ; webpack",
"clean": "rm -rf build/", "clean": "rm -rf build/",
"lint": "eslint --ext .js,.vue ./" "lint": "eslint --ext .js,.vue ./"

View File

@ -26,7 +26,9 @@ module.exports = configure(function (ctx) {
// https://v2.quasar.dev/quasar-cli-webpack/boot-files // https://v2.quasar.dev/quasar-cli-webpack/boot-files
boot: [ boot: [
'i18n', 'i18n',
'axios' 'axios',
'error-handler',
'app'
], ],
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
@ -129,7 +131,9 @@ module.exports = configure(function (ctx) {
// directives: [], // directives: [],
// Quasar plugins // Quasar plugins
plugins: [] plugins: [
'Notify'
]
}, },
// animations: 'all', // --- includes all animations // animations: 'all', // --- includes all animations

7
src/boot/app.js Normal file
View File

@ -0,0 +1,7 @@
import { boot } from 'quasar/wrappers'
import { appStore } from 'stores/app'
export default boot(({ app }) => {
const myApp = appStore()
app.config.globalProperties.$app = myApp
})

View File

@ -1,5 +1,6 @@
import { boot } from 'quasar/wrappers' import { boot } from 'quasar/wrappers'
import { Connection } from '../js/db/connection' import { Connection } from '../js/db/connection'
import { userStore } from 'stores/user'
import axios from 'axios' import axios from 'axios'
// Be careful when using SSR for cross-request state pollution // Be careful when using SSR for cross-request state pollution
@ -11,17 +12,20 @@ import axios from 'axios'
const api = axios.create({ const api = axios.create({
baseURL: `//${location.hostname}:${location.port}/api/` baseURL: `//${location.hostname}:${location.port}/api/`
}) })
api.interceptors.request.use(function addToken (config) {
const token = localStorage.getItem('vnToken')
if (token) {
config.headers.Authorization = token
}
return config
})
const jApi = new Connection() const jApi = new Connection()
export default boot(({ app }) => { export default boot(({ app }) => {
const user = userStore()
function addToken (config) {
if (user.token) {
config.headers.Authorization = user.token
}
return config
}
api.interceptors.request.use(addToken)
jApi.use(addToken)
// for use inside Vue files (Options API) through this.$axios and this.$api // for use inside Vue files (Options API) through this.$axios and this.$api
app.config.globalProperties.$axios = axios app.config.globalProperties.$axios = axios

59
src/boot/error-handler.js Normal file
View File

@ -0,0 +1,59 @@
export default async ({ app }) => {
/*
window.addEventListener('error',
e => onWindowError(e));
window.addEventListener('unhandledrejection',
e => onWindowRejection(e));
,onWindowError(event) {
errorHandler(event.error);
}
,onWindowRejection(event) {
errorHandler(event.reason);
}
*/
app.config.errorHandler = (err, vm, info) => {
errorHandler(err, vm)
}
function errorHandler (err, vm) {
let message
let tMessage
const res = err.response
if (res) {
const status = res.status
if (status >= 400 && status < 500) {
switch (status) {
case 401:
tMessage = 'loginFailed'
break
case 403:
tMessage = 'authenticationRequired'
vm.$router.push('/login')
break
case 404:
tMessage = 'notFound'
break
default:
message = res.data.error.message
}
} else if (status >= 500) {
tMessage = 'internalServerError'
}
} else {
tMessage = 'somethingWentWrong'
console.error(err)
}
if (tMessage) {
message = vm.$t(tMessage)
}
vm.$q.notify({
message,
type: 'negative'
})
}
}

View File

@ -4,8 +4,10 @@ import messages from 'src/i18n'
export default boot(({ app }) => { export default boot(({ app }) => {
const i18n = createI18n({ const i18n = createI18n({
locale: 'en-US', locale: 'es-ES',
globalInjection: true, globalInjection: true,
silentTranslationWarn: true,
silentFallbackWarn: true,
messages messages
}) })

View File

@ -13,6 +13,14 @@ body {
font-family: 'Poppins', 'Verdana', 'Sans'; font-family: 'Poppins', 'Verdana', 'Sans';
background-color: #fafafa; background-color: #fafafa;
} }
a.link {
text-decoration: none;
color: #6a1;
&:hover {
text-decoration: underline;
}
}
.q-card { .q-card {
border-radius: 7px; border-radius: 7px;
box-shadow: 0 0 3px rgba(0, 0, 0, .1); box-shadow: 0 0 3px rgba(0, 0, 0, .1);

View File

@ -3,5 +3,10 @@
export default { export default {
failed: 'Action failed', failed: 'Action failed',
success: 'Action was successful' success: 'Action was successful',
internalServerError: 'Internal server error',
somethingWentWrong: 'Something went wrong',
loginFailed: 'Login failed',
authenticationRequired: 'Authentication required',
notFound: 'Not found'
} }

12
src/i18n/es-ES/index.js Normal file
View File

@ -0,0 +1,12 @@
// This is just an example,
// so you can safely delete all default props below
export default {
failed: 'Acción fallida',
success: 'Acción exitosa',
internalServerError: 'Error interno del servidor',
somethingWentWrong: 'Algo salió mal',
loginFailed: 'Usuario o contraseña incorrectos',
authenticationRequired: 'Autenticación requerida',
notFound: 'No encontrado'
}

View File

@ -1,5 +1,7 @@
import enUS from './en-US' import enUS from './en-US'
import esES from './es-ES'
export default { export default {
'en-US': enUS 'en-US': enUS,
'es-ES': esES
} }

View File

@ -9,6 +9,11 @@ export class JsonConnection extends VnObject {
_connected = false _connected = false
_requestsCount = 0 _requestsCount = 0
token = null token = null
interceptors = []
use (fn) {
this.interceptors.push(fn)
}
/** /**
* Executes the specified REST service with the given params and calls * Executes the specified REST service with the given params and calls
@ -70,8 +75,9 @@ export class JsonConnection extends VnObject {
const request = new XMLHttpRequest() const request = new XMLHttpRequest()
request.open(config.method, config.url, true) request.open(config.method, config.url, true)
const token = localStorage.getItem('vnToken') for (const fn of this.interceptors) {
if (token) { request.setRequestHeader('Authorization', token) } config = fn(config)
}
const headers = config.headers const headers = config.headers
if (headers) { if (headers) {

View File

@ -8,26 +8,22 @@
round round
icon="menu" icon="menu"
aria-label="Menu" aria-label="Menu"
@click="toggleLeftDrawer" @click="toggleLeftDrawer"/>
/>
<q-toolbar-title> <q-toolbar-title>
Home Home
</q-toolbar-title> </q-toolbar-title>
</q-toolbar> </q-toolbar>
</q-header> </q-header>
<q-drawer <q-drawer
v-model="leftDrawerOpen" v-model="leftDrawerOpen"
:width="250" :width="250"
show-if-above show-if-above>
>
<q-toolbar class="logo"> <q-toolbar class="logo">
<img src="statics/logo-dark.svg"> <img src="statics/logo-dark.svg">
</q-toolbar> </q-toolbar>
<div class="user-info"> <div class="user-info">
<div> <div>
<span id="user-name">{{user.nickname}}</span> <span id="user-name">{{(user.nickname)}}</span>
<q-btn flat icon="logout" alt="_Exit" @click="logout()"/> <q-btn flat icon="logout" alt="_Exit" @click="logout()"/>
</div> </div>
<div id="supplant" class="supplant"> <div id="supplant" class="supplant">
@ -63,7 +59,6 @@
</q-expansion-item> </q-expansion-item>
</q-list> </q-list>
</q-drawer> </q-drawer>
<q-page-container> <q-page-container>
<router-view /> <router-view />
</q-page-container> </q-page-container>
@ -156,6 +151,8 @@ export default defineComponent({
}, },
async mounted () { async mounted () {
await this.user.loadData()
await this.$app.loadConfig()
await this.fetchData() await this.fetchData()
}, },
@ -192,3 +189,10 @@ export default defineComponent({
} }
}) })
</script> </script>
<i18n lang="yaml">
en-US:
visitor: Visitor
es-ES:
visitor: Visitante
</i18n>

View File

@ -6,7 +6,7 @@
v-for="myNew in news" v-for="myNew in news"
:key="myNew.id"> :key="myNew.id">
<q-card> <q-card>
<q-img :src="`https://verdnatura.es/vn-image-data/news/full/${myNew.image}`"> <q-img :src="`${$app.imageUrl}/news/full/${myNew.image}`">
</q-img> </q-img>
<q-card-section> <q-card-section>
<div class="text-h5">{{ myNew.title }}</div> <div class="text-h5">{{ myNew.title }}</div>
@ -22,7 +22,7 @@
fab fab
icon="add_shopping_cart" icon="add_shopping_cart"
color="accent" color="accent"
:to="{name: 'catalog'}" to="/catalog"
:title="$t('startOrder')" :title="$t('startOrder')"
/> />
</q-page-sticky> </q-page-sticky>
@ -33,27 +33,18 @@
.new-card { .new-card {
width: 100%; width: 100%;
@media screen and (min-width: 1000px) and (max-width: 1399px) { @media screen and (min-width: 800px) and (max-width: 1400px) {
width: 50%; width: 50%;
} }
@media screen and (min-width: 1400px) and (max-width: 1699px) { @media screen and (min-width: 1401px) and (max-width: 1920px) {
width: 33.33%; width: 33.33%;
} }
@media screen and (min-width: 1700px) { @media screen and (min-width: 19021) {
width: 25%; width: 25%;
} }
} }
.new-body { .new-body {
font-family: 'Open Sans'; font-family: 'Open Sans';
a {
text-decoration: none;
color: #6a1;
&:hover {
text-decoration: underline;
}
}
} }
</style> </style>

View File

@ -0,0 +1,64 @@
<template>
<div class="vn-pp row justify-center">
<div
v-if="orders && !orders.length"
class="text-subtitle1 text-center text-grey-7 q-pa-md">
{{$t('noOrdersFound')}}
</div>
<q-card
v-if="orders && orders.length"
class="vn-w-md">
<q-list bordered separator>
<q-item
v-for="order in orders"
:key="order.id"
:to="`/order/${order.id}/`"
clickable
v-ripple>
<q-item-section>
<q-item-label>{{order.landed}}</q-item-label>
<q-item-label caption>#{{order.id}}</q-item-label>
<q-item-label caption>{{order.address.nickname}}</q-item-label>
<q-item-label caption>{{order.address.city}}</q-item-label>
</q-item-section>
<q-item-section side top>
{{order.taxableBase}}
</q-item-section>
</q-item>
</q-list>
</q-card>
<q-page-sticky position="bottom-right" :offset="[18, 18]">
<q-btn
fab
icon="add_shopping_cart"
color="accent"
to="/catalog"
:title="$t('startOrder')"/>
</q-page-sticky>
</div>
</template>
<script>
export default {
name: 'OrdersPendingIndex',
data () {
return {
orders: null
}
},
async mounted () {
this.orders = await this.$jApi.query(
'CALL myTicket_list(NULL, NULL)'
)
}
}
</script>
<i18n lang="yaml">
en-US:
startOrder: Start order
noOrdersFound: No orders found
es-ES:
startOrder: Empezar pedido
noOrdersFound: No se encontrado pedidos
</i18n>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center"> <div class="fullscreen bg-accent text-white text-center q-pa-md flex flex-center">
<div> <div>
<div style="font-size: 30vh"> <div style="font-size: 30vh">
404 404
@ -12,7 +12,7 @@
<q-btn <q-btn
class="q-mt-xl" class="q-mt-xl"
color="white" color="white"
text-color="blue" text-color="accent"
unelevated unelevated
to="/" to="/"
label="Go Home" label="Go Home"

View File

@ -49,7 +49,7 @@
</div> </div>
<div class="justify-center"> <div class="justify-center">
<q-btn <q-btn
type="submit" to="/"
:label="$t('logInAsGuest')" :label="$t('logInAsGuest')"
class="full-width" class="full-width"
color="primary" color="primary"
@ -58,7 +58,7 @@
outline outline
/> />
</div> </div>
<p class="password-forgotten text-center"> <p class="password-forgotten text-center q-mt-lg">
<router-link to="/remember-password" class="link"> <router-link to="/remember-password" class="link">
{{$t('haveForgottenPassword')}} {{$t('haveForgottenPassword')}}
</router-link> </router-link>
@ -67,7 +67,9 @@
<div class="footer text-center"> <div class="footer text-center">
<p> <p>
{{$t('notACustomerYet')}} {{$t('notACustomerYet')}}
<a href="//verdnatura.es/register/" target="_blank">{{$t('signUp')}}</a> <a href="//verdnatura.es/register/" target="_blank" class="link">
{{$t('signUp')}}
</a>
</p> </p>
<p class="contact"> <p class="contact">
{{$t('loginPhone')}} · {{$t('loginMail')}} {{$t('loginPhone')}} · {{$t('loginMail')}}
@ -84,12 +86,7 @@ $login-margin-between: 55px;
max-width: 280px; max-width: 280px;
} }
a { a {
text-decoration: none;
color: inherit; color: inherit;
&:hover {
text-decoration: underline;
}
} }
.header { .header {
margin-top: $login-margin-top; margin-top: $login-margin-top;
@ -110,7 +107,6 @@ a {
} }
.password-forgotten { .password-forgotten {
font-size: .8rem; font-size: .8rem;
margin-top: 30px;
} }
.footer { .footer {
margin-bottom: $login-margin-top; margin-bottom: $login-margin-top;
@ -159,7 +155,7 @@ export default {
methods: { methods: {
async onLogin () { async onLogin () {
await this.user.login(this.email, this.password) await this.user.login(this.email, this.password, this.remember)
this.$router.push('/') this.$router.push('/')
} }
} }

View File

@ -1,13 +1,13 @@
<template> <template>
<div class="text-center"> <div class="text-center">
<q-card-section> <div>
<q-icon <q-icon
name="contact_support" name="contact_support"
class="block q-mx-auto text-accent" class="block q-mx-auto text-accent"
style="font-size: 120px;" style="font-size: 120px;"
/> />
</q-card-section> </div>
<q-card-section> <div>
<q-form @submit="onSend" class="q-gutter-y-md text-grey-8"> <q-form @submit="onSend" class="q-gutter-y-md text-grey-8">
<div class="text-h5"> <div class="text-h5">
<div> <div>
@ -19,29 +19,31 @@
</div> </div>
<q-input <q-input
v-model="email" v-model="email"
:label="$t('email')" :label="$t('user')"
:rules="[ val => !!val || $t('inputEmail')]" :rules="[ val => !!val || $t('inputEmail')]"
autofocus autofocus
filled
/> />
<div> <div class="q-mt-lg">
{{$t('weSendEmail')}} {{$t('weSendEmail')}}
</div> </div>
<div> <div>
<q-btn <q-btn
type="submit" type="submit"
:label="$t('send')" :label="$t('send')"
class="full-width" class="full-width q-mt-md"
color="black" color="primary"
rounded
no-caps
unelevated
/> />
<div class="text-center q-mt-xs"> <div class="text-center q-mt-md">
<router-link to="/login" class="link"> <router-link to="/login" class="link">
{{$t('return')}} {{$t('return')}}
</router-link> </router-link>
</div> </div>
</div> </div>
</q-form> </q-form>
</q-card-section> </div>
</div> </div>
</template> </template>
@ -49,6 +51,13 @@
#image { #image {
height: 190px; height: 190px;
} }
.q-btn {
height: 50px;
}
a {
color: inherit;
font-size: .8rem;
}
</style> </style>
<script> <script>
@ -64,7 +73,7 @@ export default {
const params = { const params = {
email: this.email email: this.email
} }
await this.$axios.post('users/reset', params) await this.$axios.post('Users/reset', params)
this.$q.notify({ this.$q.notify({
message: this.$t('weHaveSentEmailToRecover'), message: this.$t('weHaveSentEmailToRecover'),
type: 'positive' type: 'positive'
@ -74,3 +83,26 @@ export default {
} }
} }
</script> </script>
<i18n lang="yaml">
en-US:
user: User
inputEmail: Input email
rememberPassword: Rememeber password
dontWorry: Don't worry!
fillData: Fill the data
weSendEmail: We will sent you an email to recover your password
weHaveSentEmailToRecover: We've sent you an email where you can recover your password
send: Send
return: Return
es-ES:
user: Usuario
inputEmail: Introduce el correo electrónico
rememberPassword: Recordar contraseña
dontWorry: ¡No te preocupes!
fillData: Rellena los datos
weSendEmail: Te enviaremos un correo para restablecer tu contraseña
weHaveSentEmailToRecover: Te hemos enviado un correo donde podrás recuperar tu contraseña
send: Enviar
return: Volver
</i18n>

View File

@ -25,11 +25,15 @@ const routes = [
{ {
name: '', name: '',
path: '', path: '',
component: () => import('pages/Home.vue') component: () => import('src/pages/Cms/Home.vue')
}, { }, {
name: 'home', name: 'home',
path: '/cms/home', path: '/cms/home',
component: () => import('pages/Home.vue') component: () => import('src/pages/Cms/Home.vue')
}, {
name: 'orders',
path: '/ecomerce/orders',
component: () => import('pages/Ecomerce/Orders.vue')
} }
] ]
}, },

17
src/stores/app.js Normal file
View File

@ -0,0 +1,17 @@
import { defineStore } from 'pinia'
import { jApi } from 'boot/axios'
export const appStore = defineStore('hedera', {
state: () => ({
imageUrl: ''
}),
actions: {
async loadConfig () {
const imageUrl = await jApi.getValue(
'SELECT url FROM imageConfig'
)
this.$patch({ imageUrl })
}
}
})

View File

@ -1,15 +0,0 @@
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
counter: 0
}),
getters: {
doubleCount: (state) => state.counter * 2
},
actions: {
increment () {
this.counter++
}
}
})

View File

@ -2,41 +2,58 @@ import { defineStore } from 'pinia'
import { api, jApi } from 'boot/axios' import { api, jApi } from 'boot/axios'
export const userStore = defineStore('user', { export const userStore = defineStore('user', {
state: () => ({ state: () => {
token: null, const token =
localStorage.getItem('vnToken') ||
sessionStorage.getItem('vnToken')
return {
token,
id: null, id: null,
name: null, name: null,
nickname: null nickname: null
}), }
},
getters: { getters: {
loggedIn: state => state.token != null loggedIn: state => state.token != null
}, },
actions: { actions: {
async login (user, password) { async login (user, password, remember) {
const params = { user, password } const params = { user, password }
const res = await api.post('Accounts/login', params) const res = await api.post('Accounts/login', params)
localStorage.setItem('vnToken', res.data.token)
if (remember) {
localStorage.setItem('vnToken', res.data.token)
} else {
sessionStorage.setItem('vnToken', res.data.token)
}
this.$patch({
token: res.data.token,
name: user
})
},
async logout () {
if (this.token != null) {
await api.post('Accounts/logout')
localStorage.removeItem('vnToken')
sessionStorage.removeItem('vnToken')
}
this.$reset()
},
async loadData () {
const userData = await jApi.getObject( const userData = await jApi.getObject(
'SELECT id, nickname FROM account.myUser' 'SELECT id, nickname FROM account.myUser'
) )
this.$patch({ this.$patch({
token: res.data.token,
name: user,
id: userData.id, id: userData.id,
nickname: userData.nickname nickname: userData.nickname
}) })
},
async logout () {
if (localStorage.getItem('vnToken') != null) {
await api.post('Accounts/logout')
}
localStorage.removeItem('vnToken')
this.$reset()
} }
} }
}) })