Merge master in the branch.

This commit is contained in:
elweyn 2021-09-05 10:45:50 +02:00
commit 021952831f
51 changed files with 385 additions and 209 deletions

21
backend/src/auth/auth.ts Normal file
View File

@ -0,0 +1,21 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { AuthChecker } from 'type-graphql'
import decode from '../jwt/decode'
import { apiGet } from '../apis/loginAPI'
import CONFIG from '../config'
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
export const isAuthorized: AuthChecker<any> = async ({ root, args, context, info }, roles) => {
if (context.token) {
const decoded = decode(context.token)
if (decoded.sessionId && decoded.sessionId !== 0) {
const result = await apiGet(
`${CONFIG.LOGIN_API_URL}checkSessionState?session_id=${decoded.sessionId}`,
)
context.sessionId = decoded.sessionId
return result.success
}
}
return false
}

View File

@ -17,9 +17,6 @@ export class GdtTransactionInput {
@ArgsType()
export class GdtTransactionSessionIdInput {
@Field(() => Number)
sessionId: number
@Field(() => Int, { nullable: true })
currentPage?: number

View File

@ -41,9 +41,6 @@ export class ChangePasswordArgs {
@ArgsType()
export class UpdateUserInfosArgs {
@Field(() => Number)
sessionId!: number
@Field(() => String)
email!: string

View File

@ -2,9 +2,6 @@ import { ArgsType, Field, Int } from 'type-graphql'
@ArgsType()
export class TransactionListInput {
@Field(() => Number)
sessionId: number
@Field(() => Int)
firstPage: number
@ -17,9 +14,6 @@ export class TransactionListInput {
@ArgsType()
export class TransactionSendArgs {
@Field(() => Number)
sessionId: number
@Field(() => String)
email: string

View File

@ -15,6 +15,7 @@ export enum GdtEntryType {
@ObjectType()
export class GdtEntry {
constructor(json: any) {
this.id = json.id
this.amount = json.amount
this.date = json.date
this.email = json.email
@ -27,6 +28,9 @@ export class GdtEntry {
this.gdt = json.gdt
}
@Field(() => Number)
id: number
@Field(() => Number)
amount: number

View File

@ -1,20 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql'
import { User } from './User'
// temporaray solution until we have JWT implemented
@ObjectType()
export class LoginResponse {
constructor(json: any) {
this.sessionId = json.session_id
this.user = new User(json.user)
}
@Field(() => Number)
sessionId: number
@Field(() => User)
user: User
}

View File

@ -1,13 +1,17 @@
import { Resolver, Query, /* Mutation, */ Arg } from 'type-graphql'
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
import CONFIG from '../../config'
import { Balance } from '../models/Balance'
import { apiGet } from '../../apis/HttpRequest'
@Resolver()
export class BalanceResolver {
@Authorized()
@Query(() => Balance)
async balance(@Arg('sessionId') sessionId: number): Promise<Balance> {
const result = await apiGet(CONFIG.COMMUNITY_API_URL + 'getBalance/' + sessionId)
async balance(@Ctx() context: any): Promise<Balance> {
const result = await apiGet(CONFIG.COMMUNITY_API_URL + 'getBalance/' + context.sessionId)
if (!result.success) throw new Error(result.data)
return new Balance(result.data)
}

View File

@ -1,5 +1,7 @@
// import jwt from 'jsonwebtoken'
import { Resolver, Query, /* Mutation, */ Args } from 'type-graphql'
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Args, Ctx, Authorized } from 'type-graphql'
import CONFIG from '../../config'
import { GdtEntryList } from '../models/GdtEntryList'
import { GdtTransactionSessionIdInput } from '../inputs/GdtInputs'
@ -7,14 +9,16 @@ import { apiGet } from '../../apis/HttpRequest'
@Resolver()
export class GdtResolver {
@Authorized()
@Query(() => GdtEntryList)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async listGDTEntries(
@Args()
{ currentPage = 1, pageSize = 5, order = 'DESC', sessionId }: GdtTransactionSessionIdInput,
{ currentPage = 1, pageSize = 5, order = 'DESC' }: GdtTransactionSessionIdInput,
@Ctx() context: any,
): Promise<GdtEntryList> {
const result = await apiGet(
`${CONFIG.COMMUNITY_API_URL}listGDTTransactions/${currentPage}/${pageSize}/${order}/${sessionId}`,
`${CONFIG.COMMUNITY_API_URL}listGDTTransactions/${currentPage}/${pageSize}/${order}/${context.sessionId}`,
)
if (!result.success) {
throw new Error(result.data)

View File

@ -1,4 +1,7 @@
import { Resolver, Query, Args } from 'type-graphql'
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Args, Authorized, Ctx } from 'type-graphql'
import CONFIG from '../../config'
import { TransactionList } from '../models/Transaction'
import { TransactionListInput, TransactionSendArgs } from '../inputs/TransactionInput'
@ -6,23 +9,27 @@ import { apiGet, apiPost } from '../../apis/HttpRequest'
@Resolver()
export class TransactionResolver {
@Authorized()
@Query(() => TransactionList)
async transactionList(
@Args() { sessionId, firstPage = 1, items = 25, order = 'DESC' }: TransactionListInput,
@Args() { firstPage = 1, items = 25, order = 'DESC' }: TransactionListInput,
@Ctx() context: any,
): Promise<TransactionList> {
const result = await apiGet(
`${CONFIG.COMMUNITY_API_URL}listTransactions/${firstPage}/${items}/${order}/${sessionId}`,
`${CONFIG.COMMUNITY_API_URL}listTransactions/${firstPage}/${items}/${order}/${context.sessionId}`,
)
if (!result.success) throw new Error(result.data)
return new TransactionList(result.data)
}
@Authorized()
@Query(() => String)
async sendCoins(
@Args() { sessionId, email, amount, memo }: TransactionSendArgs,
@Args() { email, amount, memo }: TransactionSendArgs,
@Ctx() context: any,
): Promise<string> {
const payload = {
session_id: sessionId,
session_id: context.sessionId,
target_email: email,
amount: amount * 10000,
memo,

View File

@ -1,8 +1,9 @@
// import jwt from 'jsonwebtoken'
import { Resolver, Query, Args, Arg, UseMiddleware } from 'type-graphql'
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware } from 'type-graphql'
import CONFIG from '../../config'
import { CheckUsernameResponse } from '../models/CheckUsernameResponse'
import { LoginResponse } from '../models/LoginResponse'
import { User } from '../models/User'
import { LoginViaVerificationCode } from '../models/LoginViaVerificationCode'
import { SendPasswordResetEmailResponse } from '../models/SendPasswordResetEmailResponse'
import { UpdateUserInfosResponse } from '../models/UpdateUserInfosResponse'
@ -16,13 +17,14 @@ import {
import { apiPost, apiGet } from '../../apis/HttpRequest'
import { KlicktippController } from '../../apis/KlicktippController'
import { registerMiddleware } from '../../middleware/registerMiddleware'
import encode from '../../jwt/encode'
@Resolver()
export class UserResolver {
private connector: KlicktippController = new KlicktippController(CONFIG.KLICKTTIPP_API_URL)
@Query(() => LoginResponse)
async login(@Args() { email, password }: UnsecureLoginArgs): Promise<LoginResponse> {
@Query(() => String)
async login(@Args() { email, password }: UnsecureLoginArgs): Promise<string> {
email = email.trim().toLowerCase()
const result = await apiPost(CONFIG.LOGIN_API_URL + 'unsecureLogin', { email, password })
@ -31,21 +33,10 @@ export class UserResolver {
throw new Error(result.data)
}
// temporary solution until we have JWT implemented
return new LoginResponse(result.data)
// create and return the json web token
// The expire doesn't help us here. The client needs to track when the token expires on its own,
// since every action prolongs the time the session is valid.
/*
return jwt.sign(
{ result, role: 'todo' },
CONFIG.JWT_SECRET, // * , { expiresIn: CONFIG.JWT_EXPIRES_IN } ,
)
*/
// return (await apiPost(CONFIG.LOGIN_API_URL + 'unsecureLogin', login)).result.data
// const loginResult: LoginResult = await loginAPI.login(data)
// return loginResult.user ? loginResult.user : new User()
const data = result.data
const sessionId = data.session_id
delete data.session_id
return encode({ sessionId, user: new User(data.user) })
}
@Query(() => LoginViaVerificationCode)
@ -63,9 +54,10 @@ export class UserResolver {
return new LoginViaVerificationCode(result.data)
}
@Authorized()
@Query(() => String)
async logout(@Arg('sessionId') sessionId: number): Promise<string> {
const payload = { session_id: sessionId }
async logout(@Ctx() context: any): Promise<string> {
const payload = { session_id: context.sessionId }
const result = await apiPost(CONFIG.LOGIN_API_URL + 'logout', payload)
if (!result.success) {
throw new Error(result.data)
@ -128,11 +120,11 @@ export class UserResolver {
return 'sucess'
}
@Authorized()
@Query(() => UpdateUserInfosResponse)
async updateUserInfos(
@Args()
{
sessionId,
email,
firstName,
lastName,
@ -142,9 +134,10 @@ export class UserResolver {
password,
passwordNew,
}: UpdateUserInfosArgs,
@Ctx() context: any,
): Promise<UpdateUserInfosResponse> {
const payload = {
session_id: sessionId,
session_id: context.sessionId,
email,
update: {
'User.first_name': firstName || undefined,

View File

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import 'reflect-metadata'
import express from 'express'
import { buildSchema } from 'type-graphql'
@ -8,17 +10,30 @@ import connection from './database/connection'
import CONFIG from './config'
// TODO move to extern
// import { BookResolver } from './graphql/resolvers/BookResolver'
import { UserResolver } from './graphql/resolvers/UserResolver'
import { BalanceResolver } from './graphql/resolvers/BalanceResolver'
import { GdtResolver } from './graphql/resolvers/GdtResolver'
import { TransactionResolver } from './graphql/resolvers/TransactionResolver'
import { isAuthorized } from './auth/auth'
// TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
const DB_VERSION = '0001-init_db'
const context = (req: any) => {
const authorization = req.req.headers.authorization
let token = null
if (authorization) {
token = req.req.headers.authorization.replace(/^Bearer /, '')
}
const context = {
token,
}
return context
}
async function main() {
// check for correct database version
const con = await connection()
@ -34,6 +49,7 @@ async function main() {
// const connection = await createConnection()
const schema = await buildSchema({
resolvers: [UserResolver, BalanceResolver, TransactionResolver, GdtResolver],
authChecker: isAuthorized,
})
// Graphiql interface
@ -46,7 +62,7 @@ async function main() {
const server = express()
// Apollo Server
const apollo = new ApolloServer({ schema, playground })
const apollo = new ApolloServer({ schema, playground, context })
apollo.applyMiddleware({ app: server })
// Start Server

23
backend/src/jwt/decode.ts Normal file
View File

@ -0,0 +1,23 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import jwt from 'jsonwebtoken'
import CONFIG from '../config/'
export default (token: string): any => {
if (!token) return null
let sessionId = null
const email = null
try {
const decoded = jwt.verify(token, CONFIG.JWT_SECRET)
sessionId = decoded.sub
// email = decoded.email
return {
token,
sessionId,
email,
}
} catch (err) {
return null
}
}

18
backend/src/jwt/encode.ts Normal file
View File

@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import jwt from 'jsonwebtoken'
import CONFIG from '../config/'
// Generate an Access Token
export default function encode(data: any): string {
const { user, sessionId } = data
const { email, language, firstName, lastName } = user
const token = jwt.sign({ email, language, firstName, lastName, sessionId }, CONFIG.JWT_SECRET, {
expiresIn: CONFIG.JWT_EXPIRES_IN,
// issuer: CONFIG.GRAPHQL_URI,
// audience: CONFIG.CLIENT_URI,
subject: sessionId.toString(),
})
return token
}

View File

@ -21,7 +21,7 @@ $this->assign('title', __('GDT Kontoübersicht'));
$header = '<h3>' . __('Zur Verfügung: ') . '</h3>';
if($gdtSum > 0){
$header .= '<h2>'.$this->element('printGDT', ['number' => $gdtSum]).'</h2>';
$header .= '<h2>'.$this->element('printGDT', ['number' => $gdtSum*100.0]).'</h2>';
}
if($moreEntrysAsShown) {
$header .= '<span>'. __('Nur die letzten 100 Einträge werden angezeigt!') . '</span>';
@ -56,7 +56,7 @@ $this->assign('header', $header);
<?= $this->Number->format($entry['factor2']) ?>
<?php endif; ?>
</div>
<div class="cell c3"><?= $this->element('printGDT', ['number' => $entry['gdt']]) ?></div>
<div class="cell c3"><?= $this->element('printGDT', ['number' => $entry['gdt']*100.0]) ?></div>
</div>
<?php endforeach; ?>
</div>
@ -98,7 +98,7 @@ $this->assign('header', $header);
<?= $this->Number->format($gdtEntry['factor2']) ?>
<?php endif; ?>
</div>
<div class="cell c3"><?= $this->element('printGDT', ['number' => $gdtEntry['gdt']]) ?></div>
<div class="cell c3"><?= $this->element('printGDT', ['number' => $gdtEntry['gdt'] * 100.0]) ?></div>
</div>
<?php endforeach; ?>
</div>

View File

@ -0,0 +1,53 @@
# Roles
User Roles also handled by blockchain and node servers
My Goal is to save not only the gradido transactions in blockchain but also
who is allowed to create new gradidos,
who allow joining new user to the group,
who allow connect to another group and
how the community decide which one is allowed to do this things.
## Why?
If this would be handled only by community-server everyone could be easly
overwrite this rules by using a modified client to send his transactions direct over
hedera or iota bypassing the community-server rules.
With hedera it is possible to only allow sending messages to a topic with the correct admin key,
but then is the admin the single point of failure. Also must the key saved on server to allow everyone
sending gradidos or the transactions will only be proccessed, when admin is logging in.
If we don't use blockchain technologie at all, we have a big single point of failure.
The Community-Server and everyone who has direct access to server and the admins of course.
But it would be much much simpler of course :)
In summary it is to make sure that the community is in power and no one can take over.
## How?
There is a special type of transactions with which users determine who can determine what.
This transaction control which signatures are neccessary for things like creation and so one.
For this I think different types are needed.
- *one*: The founder of group (or someone other choosen) decide everything, this is default from start
- *some*: a number of user must sign, set absolute count or relative count
- *most*: more than 1/2 or 3/4 must sign
- *all*: all member must sign, default for choose which mode will be used
- *one-council*: one member of council
- *some-council*: absolute or relative number of council members must sign
- *most-council*: more than 1/2 or 3/4 from council members must sign
- *all-council*: all members of council must sign
this configuration can be done for different types of action,
so the voting-mode for creation may differ from voting mode for
add new members to community. Also the council members for different actions
may differ.
Also how to vote for council members is an extra type of action.
## Veto
Especially for *some* and *some-council* maybe also for other types.
The users there could vote but haven't yet and not all need to vote,
can make a Veto with Explanation which reset all existing signs of current vote,
and is needed to sign by all which again vote for the case.
## Summary
With that setup all community consense models should be possible except Democracy.
Democracy needs a secret ballot. The votes on blockchain are open (at least if someone knows which
public-key belongs to which user). A secret ballot on blockchain is really hard. By my last
recherche I haven't found on. But maybe this can do the trick: https://secure.vote/

View File

@ -0,0 +1,42 @@
# Wie Colored Coins in Iota funktionieren
## Schöpfung
- Colored Coins werden bei Iota mit einer speziellen Transaktion erzeugt
- Die Farbe des neuen Coins wird durch den Transaktionshash beschrieben
- Die einmal erzeugte Menge von Colored Coins ist fest
- Um die Menge zu erhöhen müssten neue Colored Coin erzeugt werden und die alten damit ausgetauscht werden (z.B. mittels SmartContract)
## Geldmenge
- Colored Coins basierend auf den normalen Iota-Coins somit werden soviele Iota-Coins benötigt wie man Colored-Coins braucht
- 2.779.530.283.000.000 Iota maximale Coinmenge
- Weltbevölkerung 2021: 7.915.559.780
- Pro Kopf Geldmenge Gradido: 53.476
- Benötigte Iota Coins für Gradido mit 4 Nachkommastellen mit 25% Puffer:
- 7.915.559.780 * 534.760.000 * 1.25 = 5.291.155.935.000.000.000
- Vorhandene Iota coins: 2.779.530.283.000.000
- Es sind nicht genügend Iota Coins vorhanden im die ganze Welt mit Gradido auf Basis von Iota versorgen zu können
## Kosten
- Kurs am 30.08.2021: 1 Miota = 0,84
- Bei Verwendung von 4 Nachkommastellen braucht es 10 Miota für 1.000 Colored Coins (Gradido) also Miota im Wert von 8,40€
- Aktuell (30.08.2021) geschöpfte Gradido Cent: 17.001.990.500
- Notwendige Miota: 17.002.0, Wert: 14.286,73 €
- Solange die Benutzer Kontrolle über ihre Keys haben können sie mit einem regulärem Iota Wallet die Gradidos wieder in Iota umwandeln (um z.B. der Vergänglichkeit zu entgehen)
- Mit 2 Nachkommastellen wird die Vergänglichkeit schon bei 100 Gradido und 1 Stunde ungenau
- 1 Stunde, 100 Gradido, Vergänglichkeit: 0,00576, Gradidos nach einer Stunde: 99,99424 GDD
- 1 Minute, 100 Gradido, Vergänglichkeit: 0,000096, Gradidos nach einer Minute: 99,999904 GDD
## Dust-Protection
- Iota erlaubt bei leeren Adressen nur Transaktionen von mindestens 1 Miota
- Nachdem 1 Miota da ist sind bis zu 10 Transaktione < 1 Miota erlaubt, bevor ein weitere Transaktion mit mindestens 1 Miota eingehen muss
- Bei Verwendung von 4 Nachkommastellen entspricht das 100 GDD, bei 2 Nachkommastellen 10.000 GDD
## Lösung
Wir können nur 3 Nachkommastellen verwenden.
### Kosten
- 0,84 € für 1.000 GDD
- 1.428 Euro für alle bisherigen geschöpften Gradidos
### Dust-Protection
- 1.000 GDD entspricht 1 Miota, die erste Schöpfung muss also zwangsläufig 1k Gradido betragen
- Jeder kann nur maximal 10 Transaktionen < 1k GDD empfangen bevor er die GDD weitergesendet haben muss um neue erhalten zu können oder eine neue Schöpfung von 1k GDD bekommen hat

View File

@ -0,0 +1 @@
<mxfile host="7d619c52-f8a9-4b9d-82fe-1aa45d350dea" modified="2021-08-07T11:49:26.921Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.58.2 Chrome/89.0.4389.128 Electron/12.0.13 Safari/537.36" etag="5tat3B3yItVGc6mcvaQ1" version="12.2.4" pages="1"><diagram id="1SuIEZF2QCnWeueF3Zz_" name="Page-1">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</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

View File

@ -0,0 +1 @@
<mxfile host="11f51089-224b-4c51-8025-2a587ac1ee54" modified="2021-08-08T15:21:15.636Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.58.2 Chrome/89.0.4389.128 Electron/12.0.13 Safari/537.36" etag="Ph0quYraKNcgZ1LpaOqw" version="12.2.4" pages="1"><diagram id="svTVmLC50Oi8Ao2eC_wi" name="Page-1">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</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

View File

@ -0,0 +1 @@
<mxfile host="c8c9d8b1-9d45-405b-9350-947d02163aae" modified="2021-08-13T14:14:22.645Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.59.0 Chrome/91.0.4472.124 Electron/13.1.7 Safari/537.36" etag="M5WsFcVCZVYdDRxEgPr7" version="12.2.4" pages="1"><diagram id="4VYGDKFA-XGvTikjH5B0" name="Page-1">zVlLc+MoEP41Os6U9bRz9GMym9qaSmqTqp3sjUjYYoOEF6HYmV+/DQJLCHnieJRkLjK0oNV099cP7IXLYv+Vo23+jWWYesEk23vhyguCaRLAUxKeNWEaN4QNJ1lD8lvCLfmBNXGiqTXJcGUtFIxRQbY2MWVliVNh0RDnbGcvWzNqf3WLNtgh3KaIutS/SSbyhjoLpi39D0w2ufmyn1w0bwpkFuuTVDnK2K5DCr944ZIzJppRsV9iKnVn9NLsuzzy9iAYx6U4aYNW/BOitT7c1fXdXAsnns2JYRcoFyYLkHgriSllNTBZ7HIi8O0WpZK4A3MDLRcFhZkPQ1cgLeMT5gLvOyQt4FfMCiz4MyzRb8MLrSztLWa6a1XvG33mHbXPNA1pa28OnFuFwEDr5Ih+Ekc/4NIZydgdR2WFUkFY6WiLs7rMsGQxeRcNzWwF+ZMTNZSMoaGpoyHXe8psLmGn3AZVFUltFeA9Ed8743upuc+xnq32WpFq8mwmJQj6XS0EI+m53hibebtVzay9N5gTOC3mmnjUDhWreYptfxCIb7BZpjGEMytquNbqmCMesIahcUyRIE92rBkykf7CDSMg8TFnCKKekZvj6E3d0NDj44c9r7roMWp04DBSDnM49Wk+NHNR9td8dbW6/uwFCSokYihoaYEoga8b0kYczNZxNsCMsN2L44r8QA9qgbT0VoqsDhEvvHgl+daCVU2a8ZvPbEoYU7yWrCQQCUT/uSYLJr9eAaJJubmTk9WnaBwkB7OoB+WZC2V/wHmCEaBseHTMQEklcKnyKzxeYZREvXrgMFIUi5FeWj5U8qcgFFeClZDRg0tI2WvCCwiefbuaxLPlLMVV9XJgfUDp40aF4utaUFIa646RknrYCEI34gZDZhoj4poA+ysR97Rg91sHtrCf308NbFEQfp5Mkwswmnr6w6YcP8wFoWO4GzCUDCJtMVEddXwQAPMS0VvBuCpQPxAA8cv+7ydvVJMF0Xv5/0CyNzb8TTDhJOnpudm+Xza8IQzcnsPki9w3+eKfGlY8Um8ZevOIy8zRrADm7SJD3BrCn6woVJbJCIbnN8gTgJRKfkyW6ZMrJpDsHykmaQ6DEqmfDBfS9GoPYKAC78FSN7uaZ4odQEgeAJhzxYsfNh2SlxcsFZEfOD3Ai0Z+QTaSXY7kkzVrHo2ozWJUK0kKpBplpAQLLzun3raHfrHgqQRnj3jJKJPFbSmlCxdrQmmPpEsYIMROQRMca10YaGhNFbRykkn9QIVlNTxjxJfIdsckdPPrJHZh5ffd9qwA45ajxhCgo9LSfvJfLXv1RlOfqkZVc+lajCtjmgXGfGvpEouldIF/cSZNrxtKGFk95WSHaZpj5XfSl1tHcz1LOWrHWRoxj/jLGd2pY7gB8x4vluLYTtzRicnCH6OoDd2i9tXZwvSnbU9633kz3J8OpokXU445sZVyZh+ZYXqZPj67nZzYjKK3aydNRfczk787CCIbBLE/AILZgBnH6BjCETqGzn3L4Xrl3uvergzftZyJg5mLg/BDK60eDpJ+bDoXB3G/YhsRB0P9BmSlkqm8s8w5K0kqHeByxKLCXJ6kuFSXa871SQFVAz3WvgxVEr8EvCjqV8ixC7zJT/zmFcCDaXt13xis/f8j/PI/</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -72,6 +72,7 @@
"vue-good-table": "^2.21.3",
"vue-i18n": "^8.22.4",
"vue-jest": "^3.0.7",
"vue-jwt-decode": "^0.1.0",
"vue-loading-overlay": "^3.4.2",
"vue-moment": "^4.1.0",
"vue-qrcode": "^0.3.5",

View File

@ -15,7 +15,6 @@ describe('LanguageSwitch', () => {
let wrapper
const state = {
sessionId: 1234,
email: 'he@ho.he',
language: null,
}
@ -123,7 +122,6 @@ describe('LanguageSwitch', () => {
expect(updateUserInfosQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
sessionId: 1234,
email: 'he@ho.he',
locale: 'en',
},
@ -136,7 +134,6 @@ describe('LanguageSwitch', () => {
expect(updateUserInfosQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
sessionId: 1234,
email: 'he@ho.he',
locale: 'de',
},

View File

@ -32,13 +32,13 @@ export default {
localeChanged(locale)
},
async saveLocale(locale) {
// if (this.$i18n.locale === locale) return
this.setLocale(locale)
if (this.$store.state.sessionId && this.$store.state.email) {
if (this.$store.state.email) {
this.$apollo
.query({
query: updateUserInfos,
variables: {
sessionId: this.$store.state.sessionId,
email: this.$store.state.email,
locale: locale,
},

View File

@ -2,23 +2,13 @@ import gql from 'graphql-tag'
export const login = gql`
query($email: String!, $password: String!) {
login(email: $email, password: $password) {
sessionId
user {
email
firstName
lastName
language
username
description
}
}
login(email: $email, password: $password)
}
`
export const logout = gql`
query($sessionId: Float!) {
logout(sessionId: $sessionId)
query {
logout
}
`
@ -39,7 +29,6 @@ export const loginViaEmailVerificationCode = gql`
export const updateUserInfos = gql`
query(
$sessionId: Float!
$email: String!
$firstName: String
$lastName: String
@ -50,7 +39,6 @@ export const updateUserInfos = gql`
$locale: String
) {
updateUserInfos(
sessionId: $sessionId
email: $email
firstName: $firstName
lastName: $lastName
@ -66,8 +54,8 @@ export const updateUserInfos = gql`
`
export const transactionsQuery = gql`
query($sessionId: Float!, $firstPage: Int = 1, $items: Int = 25, $order: String = "DESC") {
transactionList(sessionId: $sessionId, firstPage: $firstPage, items: $items, order: $order) {
query($firstPage: Int = 1, $items: Int = 25, $order: String = "DESC") {
transactionList(firstPage: $firstPage, items: $items, order: $order) {
gdtSum
count
balance
@ -115,8 +103,8 @@ export const resgisterUserQuery = gql`
`
export const sendCoins = gql`
query($sessionId: Float!, $email: String!, $amount: Float!, $memo: String!) {
sendCoins(sessionId: $sessionId, email: $email, amount: $amount, memo: $memo)
query($email: String!, $amount: Float!, $memo: String!) {
sendCoins(email: $email, amount: $amount, memo: $memo)
}
`
@ -137,10 +125,11 @@ export const checkUsername = gql`
`
export const listGDTEntriesQuery = gql`
query($currentPage: Int!, $pageSize: Int!, $sessionId: Float!) {
listGDTEntries(currentPage: $currentPage, pageSize: $pageSize, sessionId: $sessionId) {
query($currentPage: Int!, $pageSize: Int!) {
listGDTEntries(currentPage: $currentPage, pageSize: $pageSize) {
count
gdtEntries {
id
amount
date
comment

View File

@ -18,6 +18,7 @@
"de": "Deutsch",
"en": "English"
},
"select_language": "Bitte wähle eine Sprache für die App und Newsletter",
"decay": {
"decay": "Vergänglichkeit",
"decay_since_last_transaction":"Vergänglichkeit seit der letzten Transaktion",

View File

@ -18,6 +18,7 @@
"de": "Deutsch",
"en": "English"
},
"select_language": "Please choose a language for the app and newsletter",
"decay": {
"decay": "Decay",
"decay_since_last_transaction":"Decay since the last transaction",

View File

@ -3,7 +3,7 @@ import DashboardPlugin from './plugins/dashboard-plugin'
import App from './App.vue'
import i18n from './i18n.js'
import { loadAllRules } from './validation-rules'
import ApolloClient from 'apollo-boost'
import { ApolloClient, ApolloLink, InMemoryCache, HttpLink } from 'apollo-boost'
import VueApollo from 'vue-apollo'
import CONFIG from './config'
@ -11,7 +11,21 @@ import { store } from './store/store'
import router from './routes/router'
const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI })
const authLink = new ApolloLink((operation, forward) => {
const token = store.state.token
operation.setContext({
headers: {
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
},
})
return forward(operation)
})
const apolloClient = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
uri: CONFIG.GRAPHQL_URI,
})
@ -26,7 +40,7 @@ Vue.config.productionTip = false
loadAllRules(i18n)
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !store.state.sessionId) {
if (to.meta.requiresAuth && !store.state.token) {
next({ path: '/login' })
} else {
next()

View File

@ -1,6 +1,8 @@
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import VueJwtDecode from 'vue-jwt-decode'
Vue.use(Vuex)
export const mutations = {
@ -10,9 +12,6 @@ export const mutations = {
email: (state, email) => {
state.email = email
},
sessionId: (state, sessionId) => {
state.sessionId = sessionId
},
username: (state, username) => {
state.username = username
},
@ -25,43 +24,47 @@ export const mutations = {
description: (state, description) => {
state.description = description
},
token: (state, token) => {
state.token = token
},
}
export const actions = {
login: ({ dispatch, commit }, data) => {
commit('sessionId', data.sessionId)
commit('email', data.user.email)
commit('language', data.user.language)
commit('username', data.user.username)
commit('firstName', data.user.firstName)
commit('lastName', data.user.lastName)
commit('description', data.user.description)
login: ({ dispatch, commit }, token) => {
const decoded = VueJwtDecode.decode(token)
commit('token', token)
commit('email', decoded.email)
commit('language', decoded.language)
commit('username', decoded.username)
commit('firstName', decoded.firstName)
commit('lastName', decoded.lastName)
commit('description', decoded.description)
},
logout: ({ commit, state }) => {
commit('sessionId', null)
commit('token', null)
commit('email', null)
commit('username', '')
commit('firstName', '')
commit('lastName', '')
commit('description', '')
sessionStorage.clear()
localStorage.clear()
},
}
export const store = new Vuex.Store({
plugins: [
createPersistedState({
storage: window.sessionStorage,
storage: window.localStorage,
}),
],
state: {
sessionId: null,
email: '',
language: null,
firstName: '',
lastName: '',
username: '',
description: '',
token: null,
},
getters: {},
// Syncronous mutation of the state

View File

@ -1,6 +1,17 @@
import { mutations, actions } from './store'
import VueJwtDecode from 'vue-jwt-decode'
const { language, email, sessionId, username, firstName, lastName, description } = mutations
jest.mock('vue-jwt-decode')
VueJwtDecode.decode.mockReturnValue({
email: 'user@example.org',
language: 'de',
username: 'peter',
firstName: 'Peter',
lastName: 'Lustig',
description: 'Nickelbrille',
})
const { language, email, token, username, firstName, lastName, description } = mutations
const { login, logout } = actions
describe('Vuex store', () => {
@ -21,11 +32,11 @@ describe('Vuex store', () => {
})
})
describe('sessionId', () => {
it('sets the state of sessionId', () => {
const state = { sessionId: null }
sessionId(state, '1234')
expect(state.sessionId).toEqual('1234')
describe('token', () => {
it('sets the state of token', () => {
const state = { token: null }
token(state, '1234')
expect(state.token).toEqual('1234')
})
})
@ -66,41 +77,31 @@ describe('Vuex store', () => {
describe('login', () => {
const commit = jest.fn()
const state = {}
const commitedData = {
sessionId: 1234,
user: {
email: 'someone@there.is',
language: 'en',
username: 'user',
firstName: 'Peter',
lastName: 'Lustig',
description: 'Nickelbrille',
},
}
const commitedData = 'token'
it('calls seven commits', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenCalledTimes(7)
})
it('commits sessionId', () => {
it('commits token', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(1, 'sessionId', 1234)
expect(commit).toHaveBeenNthCalledWith(1, 'token', 'token')
})
it('commits email', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(2, 'email', 'someone@there.is')
expect(commit).toHaveBeenNthCalledWith(2, 'email', 'user@example.org')
})
it('commits language', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(3, 'language', 'en')
expect(commit).toHaveBeenNthCalledWith(3, 'language', 'de')
})
it('commits username', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(4, 'username', 'user')
expect(commit).toHaveBeenNthCalledWith(4, 'username', 'peter')
})
it('commits firstName', () => {
@ -128,9 +129,9 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenCalledTimes(6)
})
it('commits sessionId', () => {
it('commits token', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(1, 'sessionId', null)
expect(commit).toHaveBeenNthCalledWith(1, 'token', null)
})
it('commits email', () => {
@ -159,7 +160,7 @@ describe('Vuex store', () => {
})
// how to get this working?
it.skip('calls sessionStorage.clear()', () => {
it.skip('calls localStorage.clear()', () => {
const clearStorageMock = jest.fn()
global.sessionStorage = jest.fn(() => {
return {

View File

@ -41,7 +41,6 @@ describe('DashboardLayoutGdd', () => {
},
$store: {
state: {
sessionId: 1,
email: 'user@example.org',
},
dispatch: storeDispatchMock,
@ -128,16 +127,17 @@ describe('DashboardLayoutGdd', () => {
describe('logout', () => {
beforeEach(async () => {
await apolloMock.mockResolvedValue({
data: {
logout: 'success',
},
})
await wrapper.findComponent({ name: 'sidebar' }).vm.$emit('logout')
await flushPromises()
})
it('calls the API', async () => {
expect(apolloMock).toBeCalledWith(
expect.objectContaining({
variables: { sessionId: 1 },
}),
)
await expect(apolloMock).toBeCalled()
})
it('dispatches logout to store', () => {
@ -196,7 +196,6 @@ describe('DashboardLayoutGdd', () => {
expect(apolloMock).toBeCalledWith(
expect.objectContaining({
variables: {
sessionId: 1,
firstPage: 2,
items: 5,
},

View File

@ -92,7 +92,6 @@ export default {
this.$apollo
.query({
query: logout,
variables: { sessionId: this.$store.state.sessionId },
})
.then(() => {
this.$sidebar.displaySidebar(false)
@ -111,7 +110,6 @@ export default {
.query({
query: transactionsQuery,
variables: {
sessionId: this.$store.state.sessionId,
firstPage: pagination.firstPage,
items: pagination.items,
},

View File

@ -16,12 +16,12 @@ describe('AccountOverview', () => {
const mocks = {
$t: jest.fn((t) => t),
$n: jest.fn((n) => String(n)),
$store: {
state: {
sessionId: 1,
email: 'sender@example.org',
},
},
$n: jest.fn((n) => String(n)),
$apollo: {
query: sendMock,
},
@ -93,7 +93,6 @@ describe('AccountOverview', () => {
expect(sendMock).toBeCalledWith(
expect.objectContaining({
variables: {
sessionId: 1,
email: 'user@example.org',
amount: 23.45,
memo: 'Make the best of it!',

View File

@ -107,10 +107,7 @@ export default {
this.$apollo
.query({
query: sendCoins,
variables: {
sessionId: this.$store.state.sessionId,
...this.transactionData,
},
variables: this.transactionData,
})
.then(() => {
this.error = false

View File

@ -8,11 +8,6 @@ describe('GddSend', () => {
const mocks = {
$t: jest.fn((t) => t),
$store: {
state: {
sessionId: 1234,
},
},
$i18n: {
locale: jest.fn(() => 'en'),
},

View File

@ -57,11 +57,6 @@ describe('GdtTransactionList', () => {
$t: jest.fn((t) => t),
$n: jest.fn((n) => n),
$d: jest.fn((d) => d),
$store: {
state: {
sessionId: 1,
},
},
$toasted: {
error: toastErrorMock,
},
@ -89,7 +84,6 @@ describe('GdtTransactionList', () => {
expect(apolloMock).toBeCalledWith(
expect.objectContaining({
variables: {
sessionId: 1,
currentPage: 1,
pageSize: 25,
},

View File

@ -163,10 +163,19 @@
import { listGDTEntriesQuery } from '../../../graphql/queries'
import PaginationButtons from '../../../components/PaginationButtons'
const iconsByType = {
1: { icon: 'heart', classes: 'gradido-global-color-accent' },
4: { icon: 'person-check', classes: 'gradido-global-color-accent' },
7: { icon: 'gift', classes: 'gradido-global-color-accent' },
function iconByType(typeId) {
switch (typeId) {
case 1:
case 2:
case 3:
case 5:
case 6:
return { icon: 'heart', classes: 'gradido-global-color-accent' }
case 4:
return { icon: 'person-check', classes: 'gradido-global-color-accent' }
case 7:
return { icon: 'gift', classes: 'gradido-global-color-accent' }
}
}
export default {
@ -199,7 +208,6 @@ export default {
.query({
query: listGDTEntriesQuery,
variables: {
sessionId: this.$store.state.sessionId,
currentPage: this.currentPage,
pageSize: this.pageSize,
},
@ -216,7 +224,7 @@ export default {
})
},
getIcon(givenType) {
const type = iconsByType[givenType]
const type = iconByType(givenType)
if (type)
return {
icon: type.icon,

View File

@ -6,12 +6,7 @@ const localVue = global.localVue
const loginQueryMock = jest.fn().mockResolvedValue({
data: {
login: {
sessionId: 1,
user: {
name: 'Peter Lustig',
},
},
login: 'token',
},
})
@ -159,10 +154,7 @@ describe('Login', () => {
describe('login success', () => {
it('dispatches server response to store', () => {
expect(mockStoreDispach).toBeCalledWith('login', {
sessionId: 1,
user: { name: 'Peter Lustig' },
})
expect(mockStoreDispach).toBeCalledWith('login', 'token')
})
it('redirects to overview page', () => {

View File

@ -85,6 +85,13 @@ describe('Register', () => {
it('has password repeat input fields', () => {
expect(wrapper.find('input[name="form.passwordRepeat"]').exists()).toBeTruthy()
})
it('has Language selected field', () => {
expect(wrapper.find('#selectedLanguage').exists()).toBeTruthy()
})
it('selected Language value de', async () => {
wrapper.find('#selectedLanguage').findAll('option').at(1).setSelected()
expect(wrapper.find('#selectedLanguage').element.value).toBe('de')
})
it('has 1 checkbox input fields', () => {
expect(wrapper.find('#registerCheckbox').exists()).toBeTruthy()
@ -126,9 +133,16 @@ describe('Register', () => {
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
wrapper.find('input[name="form.password"]').setValue('Aa123456')
wrapper.find('input[name="form.passwordRepeat"]').setValue('Aa123456')
wrapper.find('#selectedLanguage').findAll('option').at(1).setSelected()
wrapper.find('input[name="site.signup.agree"]').setChecked(true)
})
it('reset selected value language', async () => {
await wrapper.find('button.ml-2').trigger('click')
await flushPromises()
expect(wrapper.find('#selectedLanguage').element.value).toBe('')
})
it('resets the firstName field after clicking the reset button', async () => {
await wrapper.find('button.ml-2').trigger('click')
await flushPromises()
@ -173,6 +187,7 @@ describe('Register', () => {
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
wrapper.find('input[name="form.password"]').setValue('Aa123456')
wrapper.find('input[name="form.passwordRepeat"]').setValue('Aa123456')
wrapper.find('#selectedLanguage').findAll('option').at(1).setSelected()
})
describe('server sends back error', () => {

View File

@ -84,6 +84,18 @@
:register="register"
></input-password-confirmation>
<b-row>
<b-col cols="12">
{{ $t('language') }}
<b-form-select
id="selectedLanguage"
v-model="selected"
:options="options"
class="mb-3"
></b-form-select>
</b-col>
</b-row>
<b-row class="my-4">
<b-col cols="12">
<b-form-checkbox
@ -109,7 +121,10 @@
</span>
</b-alert>
<div class="text-center" v-if="namesFilled && emailFilled && form.agree">
<div
class="text-center"
v-if="namesFilled && emailFilled && form.agree && languageFilled"
>
<div class="text-center">
<b-button class="ml-2" @click="resetForm()">{{ $t('form.reset') }}</b-button>
<b-button type="submit" variant="primary">{{ $t('signup') }}</b-button>
@ -147,6 +162,12 @@ export default {
passwordRepeat: '',
},
},
selected: null,
options: [
{ value: null, text: this.$t('select_language') },
{ value: 'de', text: this.$t('languages.de') },
{ value: 'en', text: this.$t('languages.en') },
],
submitted: false,
showError: false,
messageError: '',
@ -168,8 +189,7 @@ export default {
},
agree: false,
}
this.form.password.password = ''
this.form.password.passwordRepeat = ''
this.selected = null
this.$nextTick(() => {
this.$refs.observer.reset()
})
@ -183,7 +203,7 @@ export default {
firstName: this.form.firstname,
lastName: this.form.lastname,
password: this.form.password.password,
language: this.$store.state.language,
language: this.selected,
},
})
.then(() => {
@ -192,6 +212,7 @@ export default {
this.form.lastname = ''
this.form.password.password = ''
this.form.password.passwordRepeat = ''
this.selected = null
this.$router.push('/thx/register')
})
.catch((error) => {
@ -207,6 +228,7 @@ export default {
this.form.lastname = ''
this.form.password.password = ''
this.form.password.passwordRepeat = ''
this.selected = null
},
},
computed: {
@ -221,6 +243,9 @@ export default {
emailFilled() {
return this.form.email !== ''
},
languageFilled() {
return this.selected !== null
},
},
}
</script>

View File

@ -17,7 +17,6 @@ describe('UserCard_FormUserData', () => {
$t: jest.fn((t) => t),
$store: {
state: {
sessionId: 1,
email: 'user@example.org',
firstName: 'Peter',
lastName: 'Lustig',
@ -118,7 +117,6 @@ describe('UserCard_FormUserData', () => {
expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({
variables: {
sessionId: 1,
email: 'user@example.org',
firstName: 'Petra',
lastName: 'Lustiger',
@ -167,7 +165,6 @@ describe('UserCard_FormUserData', () => {
expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({
variables: {
sessionId: 1,
email: 'user@example.org',
firstName: 'Petra',
lastName: 'Lustiger',

View File

@ -85,7 +85,6 @@ export default {
data() {
return {
showUserData: true,
sessionId: this.$store.state.sessionId,
form: {
firstName: this.$store.state.firstName,
lastName: this.$store.state.lastName,
@ -118,7 +117,6 @@ export default {
.query({
query: updateUserInfos,
variables: {
sessionId: this.$store.state.sessionId,
email: this.$store.state.email,
firstName: this.form.firstName,
lastName: this.form.lastName,

View File

@ -14,7 +14,6 @@ describe('UserCard_FormUserMail', () => {
$t: jest.fn((t) => t),
$store: {
state: {
sessionId: 1,
email: 'user@example.org',
firstName: 'Peter',
lastName: 'Lustig',
@ -76,7 +75,6 @@ describe('UserCard_FormUserMail', () => {
expect(mockAPIcall).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
sessionId: 1,
email: 'user@example.org',
newEmail: 'test@example.org',
},
@ -106,7 +104,6 @@ describe('UserCard_FormUserMail', () => {
expect(mockAPIcall).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
sessionId: 1,
email: 'user@example.org',
newEmail: 'test@example.org',
},

View File

@ -48,7 +48,6 @@ export default {
.query({
query: updateUserInfos,
variables: {
sessionId: this.$store.state.sessionId,
email: this.$store.state.email,
newEmail: this.newEmail,
},

View File

@ -17,7 +17,6 @@ describe('UserCard_FormUserPasswort', () => {
$t: jest.fn((t) => t),
$store: {
state: {
sessionId: 1,
email: 'user@example.org',
},
},
@ -175,7 +174,6 @@ describe('UserCard_FormUserPasswort', () => {
expect(changePasswordProfileMock).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
sessionId: 1,
email: 'user@example.org',
password: '1234',
passwordNew: 'Aa123456',

View File

@ -81,7 +81,6 @@ export default {
.query({
query: updateUserInfos,
variables: {
sessionId: this.$store.state.sessionId,
email: this.$store.state.email,
password: this.form.password,
passwordNew: this.form.newPassword.password,

View File

@ -25,7 +25,6 @@ describe('UserCard_FormUsername', () => {
$t: jest.fn((t) => t),
$store: {
state: {
sessionId: 1,
email: 'user@example.org',
username: '',
},
@ -111,7 +110,6 @@ describe('UserCard_FormUsername', () => {
expect.objectContaining({
variables: {
email: 'user@example.org',
sessionId: 1,
username: 'username',
},
}),
@ -151,7 +149,6 @@ describe('UserCard_FormUsername', () => {
expect.objectContaining({
variables: {
email: 'user@example.org',
sessionId: 1,
username: 'username',
},
}),

View File

@ -90,7 +90,6 @@ export default {
.query({
query: updateUserInfos,
variables: {
sessionId: this.$store.state.sessionId,
email: this.$store.state.email,
username: this.form.username,
},

View File

@ -13,11 +13,6 @@ describe('UserProfileTransactionList', () => {
$i18n: {
locale: jest.fn(() => 'en'),
},
$store: {
state: {
sessionId: 1,
},
},
}
const stubs = {

View File

@ -13576,6 +13576,13 @@ vue-jest@^3.0.5, vue-jest@^3.0.7:
tsconfig "^7.0.0"
vue-template-es2015-compiler "^1.6.0"
vue-jwt-decode@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/vue-jwt-decode/-/vue-jwt-decode-0.1.0.tgz#f9caf7b9030d5459cc567b1c3117d9d1f291458f"
integrity sha512-4iP0NzYHkAF7G13tYPc/nudk4oNpB8GCVZupc7lekxXok1XKEgefNaGTpDT14g7RKe5H9GaMphPduDj4UVfZwQ==
dependencies:
vue "^2.3.3"
vue-loader@^15.7.0:
version "15.9.6"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.6.tgz#f4bb9ae20c3a8370af3ecf09b8126d38ffdb6b8b"
@ -13655,6 +13662,11 @@ vue@^2.2.6, vue@^2.5.17, vue@^2.6.11:
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.12.tgz#f5ebd4fa6bd2869403e29a896aed4904456c9123"
integrity sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==
vue@^2.3.3:
version "2.6.14"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"
integrity sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==
vuex-persistedstate@^4.0.0-beta.3:
version "4.0.0-beta.3"
resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-4.0.0-beta.3.tgz#89dd712de72d28e85cc95467d066002c1405f277"