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": {
"front": "webpack serve --open",
"back": "cd ../salix && gulp backOnly",
"db": "cd ../vn-database && myvc run -d",
"back": "cd ../vn-database && myvc start && cd ../salix && gulp backOnly",
"build": "rm -rf build/ ; webpack",
"clean": "rm -rf build/",
"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
boot: [
'i18n',
'axios'
'axios',
'error-handler',
'app'
],
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
@ -129,7 +131,9 @@ module.exports = configure(function (ctx) {
// directives: [],
// Quasar plugins
plugins: []
plugins: [
'Notify'
]
},
// 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 { Connection } from '../js/db/connection'
import { userStore } from 'stores/user'
import axios from 'axios'
// Be careful when using SSR for cross-request state pollution
@ -11,17 +12,20 @@ import axios from 'axios'
const api = axios.create({
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()
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
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 }) => {
const i18n = createI18n({
locale: 'en-US',
locale: 'es-ES',
globalInjection: true,
silentTranslationWarn: true,
silentFallbackWarn: true,
messages
})

View File

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

View File

@ -3,5 +3,10 @@
export default {
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 esES from './es-ES'
export default {
'en-US': enUS
'en-US': enUS,
'es-ES': esES
}

View File

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

View File

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

View File

@ -6,7 +6,7 @@
v-for="myNew in news"
:key="myNew.id">
<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-card-section>
<div class="text-h5">{{ myNew.title }}</div>
@ -22,7 +22,7 @@
fab
icon="add_shopping_cart"
color="accent"
:to="{name: 'catalog'}"
to="/catalog"
:title="$t('startOrder')"
/>
</q-page-sticky>
@ -33,27 +33,18 @@
.new-card {
width: 100%;
@media screen and (min-width: 1000px) and (max-width: 1399px) {
@media screen and (min-width: 800px) and (max-width: 1400px) {
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%;
}
@media screen and (min-width: 1700px) {
@media screen and (min-width: 19021) {
width: 25%;
}
}
.new-body {
font-family: 'Open Sans';
a {
text-decoration: none;
color: #6a1;
&:hover {
text-decoration: underline;
}
}
}
</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>
<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 style="font-size: 30vh">
404
@ -12,7 +12,7 @@
<q-btn
class="q-mt-xl"
color="white"
text-color="blue"
text-color="accent"
unelevated
to="/"
label="Go Home"

View File

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

View File

@ -1,13 +1,13 @@
<template>
<div class="text-center">
<q-card-section>
<div>
<q-icon
name="contact_support"
class="block q-mx-auto text-accent"
style="font-size: 120px;"
/>
</q-card-section>
<q-card-section>
</div>
<div>
<q-form @submit="onSend" class="q-gutter-y-md text-grey-8">
<div class="text-h5">
<div>
@ -19,29 +19,31 @@
</div>
<q-input
v-model="email"
:label="$t('email')"
:label="$t('user')"
:rules="[ val => !!val || $t('inputEmail')]"
autofocus
filled
/>
<div>
<div class="q-mt-lg">
{{$t('weSendEmail')}}
</div>
<div>
<q-btn
type="submit"
:label="$t('send')"
class="full-width"
color="black"
class="full-width q-mt-md"
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">
{{$t('return')}}
</router-link>
</div>
</div>
</q-form>
</q-card-section>
</div>
</div>
</template>
@ -49,6 +51,13 @@
#image {
height: 190px;
}
.q-btn {
height: 50px;
}
a {
color: inherit;
font-size: .8rem;
}
</style>
<script>
@ -64,7 +73,7 @@ export default {
const params = {
email: this.email
}
await this.$axios.post('users/reset', params)
await this.$axios.post('Users/reset', params)
this.$q.notify({
message: this.$t('weHaveSentEmailToRecover'),
type: 'positive'
@ -74,3 +83,26 @@ export default {
}
}
</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: '',
path: '',
component: () => import('pages/Home.vue')
component: () => import('src/pages/Cms/Home.vue')
}, {
name: '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'
export const userStore = defineStore('user', {
state: () => ({
token: null,
id: null,
name: null,
nickname: null
}),
state: () => {
const token =
localStorage.getItem('vnToken') ||
sessionStorage.getItem('vnToken')
return {
token,
id: null,
name: null,
nickname: null
}
},
getters: {
loggedIn: state => state.token != null
},
actions: {
async login (user, password) {
async login (user, password, remember) {
const params = { user, password }
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(
'SELECT id, nickname FROM account.myUser'
)
this.$patch({
token: res.data.token,
name: user,
id: userData.id,
nickname: userData.nickname
})
},
async logout () {
if (localStorage.getItem('vnToken') != null) {
await api.post('Accounts/logout')
}
localStorage.removeItem('vnToken')
this.$reset()
}
}
})