merge master, merge conflict fixed

This commit is contained in:
ogerly 2021-10-13 16:34:29 +02:00
commit 907a9fe696
68 changed files with 1221 additions and 166 deletions

View File

@ -344,7 +344,7 @@ jobs:
report_name: Coverage Frontend
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 73
min_coverage: 76
token: ${{ github.token }}
##############################################################################

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ messages.pot
nbproject
.metadata
/.env
package-lock.json

View File

@ -14,4 +14,8 @@ DB_DATABASE=gradido_community
#KLICKTIPP_PASSWORD=
#KLICKTIPP_APIKEY_DE=
#KLICKTIPP_APIKEY_EN=
#KLICKTIPP=true
#KLICKTIPP=true
COMMUNITY_NAME=
COMMUNITY_URL=
COMMUNITY_REGISTER_URL=
COMMUNITY_DESCRIPTION=

View File

@ -26,6 +26,7 @@
"graphql": "^15.5.1",
"jest": "^27.2.4",
"jsonwebtoken": "^8.5.1",
"libsodium-wrappers": "^0.7.9",
"mysql2": "^2.3.0",
"reflect-metadata": "^0.1.13",
"ts-jest": "^27.0.5",
@ -35,6 +36,7 @@
"devDependencies": {
"@types/express": "^4.17.12",
"@types/jsonwebtoken": "^8.5.2",
"@types/libsodium-wrappers": "^0.7.9",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"eslint": "^7.29.0",

View File

@ -30,9 +30,17 @@ const klicktipp = {
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN',
}
const community = {
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/vue/',
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/vue/register',
COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
}
// This is needed by graphql-directive-auth
process.env.APP_SECRET = server.JWT_SECRET
const CONFIG = { ...server, ...database, ...klicktipp }
const CONFIG = { ...server, ...database, ...klicktipp, ...community }
export default CONFIG

View File

@ -0,0 +1,31 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql'
@ObjectType()
export class Community {
constructor(json?: any) {
if (json) {
this.id = Number(json.id)
this.name = json.name
this.url = json.url
this.description = json.description
this.registerUrl = json.registerUrl
}
}
@Field(() => Number)
id: number
@Field(() => String)
name: string
@Field(() => String)
url: string
@Field(() => String)
description: string
@Field(() => String)
registerUrl: string
}

View File

@ -19,6 +19,7 @@ export class User {
this.pubkey = json.public_hex
this.language = json.language
this.publisherId = json.publisher_id
if (json.hasElopage) this.hasElopage = json.hasElopage
}
@Field(() => String)
@ -74,4 +75,7 @@ export class User {
@Field(() => KlickTipp)
klickTipp: KlickTipp
@Field(() => Boolean)
hasElopage?: boolean
}

View File

@ -0,0 +1,49 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query } from 'type-graphql'
import CONFIG from '../../config'
import { Community } from '../model/Community'
@Resolver()
export class CommunityResolver {
@Query(() => Community)
async getCommunityInfo(): Promise<Community> {
return new Community({
name: CONFIG.COMMUNITY_NAME,
description: CONFIG.COMMUNITY_DESCRIPTION,
url: CONFIG.COMMUNITY_URL,
registerUrl: CONFIG.COMMUNITY_REGISTER_URL,
})
}
@Query(() => [Community])
async communities(): Promise<Community[]> {
const communities: Community[] = []
communities.push(
new Community({
id: 1,
name: 'Gradido Entwicklung',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
url: 'http://localhost/vue/',
registerUrl: 'http://localhost/vue/register-community',
}),
new Community({
id: 2,
name: 'Gradido Staging',
description: 'Der Testserver der Gradido-Akademie.',
url: 'https://stage1.gradido.net/vue/',
registerUrl: 'https://stage1.gradido.net/vue/register-community',
}),
new Community({
id: 3,
name: 'Gradido-Akademie',
description: 'Freies Institut für Wirtschaftsbionik.',
url: 'https://gradido.net',
registerUrl: 'https://gdd1.gradido.com/vue/register-community',
}),
)
return communities
}
}

View File

@ -1,7 +1,7 @@
/* 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 { Resolver, Query, Args, Ctx, Authorized, Arg } from 'type-graphql'
import { getCustomRepository } from 'typeorm'
import CONFIG from '../../config'
import { GdtEntryList } from '../model/GdtEntryList'
@ -32,4 +32,16 @@ export class GdtResolver {
}
return new GdtEntryList(resultGDT.data)
}
@Authorized()
@Query(() => Number)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async existPid(@Arg('pid') pid: number): Promise<number> {
// load user
const resultPID = await apiGet(`${CONFIG.GDT_API_URL}/publishers/checkPidApi/${pid}`)
if (!resultPID.success) {
throw new Error(resultPID.data)
}
return resultPID.data.pid
}
}

View File

@ -23,7 +23,7 @@ import { User as dbUser } from '../../typeorm/entity/User'
import { UserTransaction as dbUserTransaction } from '../../typeorm/entity/UserTransaction'
import { Transaction as dbTransaction } from '../../typeorm/entity/Transaction'
import { apiGet, apiPost } from '../../apis/HttpRequest'
import { apiPost } from '../../apis/HttpRequest'
import { roundFloorFrom4 } from '../../util/round'
import { calculateDecay, calculateDecayWithInterval } from '../../util/decay'
import { TransactionTypeId } from '../enum/TransactionTypeId'
@ -216,13 +216,9 @@ export class TransactionResolver {
@Args() { currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
@Ctx() context: any,
): Promise<TransactionList> {
// get public key for current logged in user
const result = await apiGet(CONFIG.LOGIN_API_URL + 'login?session_id=' + context.sessionId)
if (!result.success) throw new Error(result.data)
// load user
const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(result.data.user.public_hex)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
const transactions = await listTransactions(currentPage, pageSize, order, userEntity)

View File

@ -2,12 +2,14 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
import { from_hex as fromHex } from 'libsodium-wrappers'
import CONFIG from '../../config'
import { CheckUsernameResponse } from '../model/CheckUsernameResponse'
import { LoginViaVerificationCode } from '../model/LoginViaVerificationCode'
import { SendPasswordResetEmailResponse } from '../model/SendPasswordResetEmailResponse'
import { UpdateUserInfosResponse } from '../model/UpdateUserInfosResponse'
import { User } from '../model/User'
import { User as DbUser } from '../../typeorm/entity/User'
import encode from '../../jwt/encode'
import ChangePasswordArgs from '../arg/ChangePasswordArgs'
import CheckUsernameArgs from '../arg/CheckUsernameArgs'
@ -45,7 +47,22 @@ export class UserResolver {
const user = new User(result.data.user)
// read additional settings from settings table
const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(user.pubkey)
let userEntity: void | DbUser
userEntity = await userRepository.findByPubkeyHex(user.pubkey).catch(() => {
userEntity = new DbUser()
userEntity.firstName = user.firstName
userEntity.lastName = user.lastName
userEntity.username = user.username
userEntity.email = user.email
userEntity.pubkey = Buffer.from(fromHex(user.pubkey))
userEntity.save().catch(() => {
throw new Error('error by save userEntity')
})
})
if (!userEntity) {
throw new Error('error with cannot happen')
}
const userSettingRepository = getCustomRepository(UserSettingRepository)
const coinanimation = await userSettingRepository
@ -102,6 +119,18 @@ export class UserResolver {
throw new Error(result.data)
}
const user = new User(result.data.user)
const dbuser = new DbUser()
dbuser.pubkey = Buffer.from(fromHex(user.pubkey))
dbuser.email = user.email
dbuser.firstName = user.firstName
dbuser.lastName = user.lastName
dbuser.username = user.username
dbuser.save().catch(() => {
throw new Error('error saving user')
})
return 'success'
}
@ -228,4 +257,13 @@ export class UserResolver {
}
return new CheckEmailResponse(result.data)
}
@Query(() => Boolean)
async hasElopage(@Ctx() context: any): Promise<boolean> {
const result = await apiGet(CONFIG.LOGIN_API_URL + 'hasElopage?session_id=' + context.sessionId)
if (!result.success) {
throw new Error(result.data)
}
return result.data.hasElopage
}
}

View File

@ -10,6 +10,7 @@ export class UserRepository extends Repository<User> {
}
async getUsersIndiced(userIds: number[]): Promise<User[]> {
if (!userIds.length) return []
const users = await this.createQueryBuilder('user')
.select(['user.id', 'user.firstName', 'user.lastName', 'user.email'])
.where('user.id IN (:...users)', { users: userIds })

View File

@ -14,6 +14,8 @@ async function calculateDecay(amount: number, from: Date, to: Date): Promise<num
// if decay hasn't started yet we return input amount
if (!decayStartBlock) return amount
// what happens when from > to
// Do we want to have negative decay?
const decayDuration = (to.getTime() - from.getTime()) / 1000
return decayFormula(amount, decayDuration)
}

View File

@ -888,6 +888,11 @@
"@types/koa-compose" "*"
"@types/node" "*"
"@types/libsodium-wrappers@^0.7.9":
version "0.7.9"
resolved "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz"
integrity sha512-LisgKLlYQk19baQwjkBZZXdJL0KbeTpdEnrAfz5hQACbklCY0gVFnsKUyjfNWF1UQsCSjw93Sj5jSbiO8RPfdw==
"@types/long@^4.0.0":
version "4.0.1"
resolved "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz"
@ -914,9 +919,9 @@
integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
"@types/node@^14.11.2":
version "14.17.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.4.tgz#218712242446fc868d0e007af29a4408c7765bc0"
integrity sha512-8kQ3+wKGRNN0ghtEn7EGps/B8CzuBz1nXZEIGGLP2GnwbqYn4dbTs7k+VKLTq1HvZLRCIDtN3Snx1Ege8B7L5A==
version "14.17.21"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.21.tgz#6359d8cf73481e312a43886fa50afc70ce5592c6"
integrity sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA==
"@types/prettier@^2.1.5":
version "2.4.1"
@ -1686,7 +1691,7 @@ chalk@^3.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^4.0.0:
chalk@^4.0.0, chalk@^4.1.0:
version "4.1.1"
resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz"
integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
@ -1694,14 +1699,6 @@ chalk@^4.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
char-regex@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
@ -3874,6 +3871,18 @@ libphonenumber-js@^1.9.7:
resolved "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.22.tgz"
integrity sha512-nE0aF0wrNq09ewF36s9FVqRW73hmpw6cobVDlbexmsu1432LEfuN24BCudNuRx4t2rElSeK/N0JbedzRW/TC4A==
libsodium-wrappers@^0.7.9:
version "0.7.9"
resolved "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz"
integrity sha512-9HaAeBGk1nKTRFRHkt7nzxqCvnkWTjn1pdjKgcUnZxj0FyOP4CnhgFhMdrFfgNsukijBGyBLpP2m2uKT1vuWhQ==
dependencies:
libsodium "^0.7.0"
libsodium@^0.7.0:
version "0.7.9"
resolved "https://registry.npmjs.org/libsodium/-/libsodium-0.7.9.tgz"
integrity sha512-gfeADtR4D/CM0oRUviKBViMGXZDgnFdMKMzHsvBdqLBHd9ySi6EtYnmuhHVDDYgYpAO8eU8hEY+F8vIUAPh08A==
load-json-file@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz"

View File

@ -67,6 +67,7 @@ In case of success returns:
"username": ""
},
"session_id": -127182,
"hasElopage": true,
"clientIP":"123.123.123.123"
}
```
@ -86,6 +87,7 @@ In case of success returns:
- `role`: role of user currently only "none" or "admin"
- `username`: not used yet
- `clientIP`: should be the same as where the js-client is running, else maybe a man-in-the-middle attacks is happening or
- `hasElopage`: only present if hasElopage was set to true in request, true if user has an elopage account
nginx was wrong configured.
- `session_id`: can be also negative
@ -593,3 +595,29 @@ or:
"msg": "session not found"
}
```
## Check if User has an Elopage Account
Check if logged in user has already an elopage account
### Request
`GET http://localhost/login_api/hasElopage?session_id=-127182`
### Response
In case of success returns:
```json
{
"state":"success",
"hasElopage": true
}
```
or:
```json
{
"state":"not found",
"msg": "session not found"
}
```

View File

@ -122,7 +122,6 @@ describe('LanguageSwitch', () => {
expect(updateUserInfosMutationMock).toBeCalledWith(
expect.objectContaining({
variables: {
email: 'he@ho.he',
locale: 'en',
},
}),
@ -134,7 +133,6 @@ describe('LanguageSwitch', () => {
expect(updateUserInfosMutationMock).toBeCalledWith(
expect.objectContaining({
variables: {
email: 'he@ho.he',
locale: 'de',
},
}),

View File

@ -39,7 +39,6 @@ export default {
.mutate({
mutation: updateUserInfos,
variables: {
email: this.$store.state.email,
locale: locale,
},
})

View File

@ -14,8 +14,8 @@ export default {
return {
selected: null,
options: [
{ value: 'de', text: this.$t('languages.de') },
{ value: 'en', text: this.$t('languages.en') },
{ value: 'de', text: this.$t('settings.language.de') },
{ value: 'en', text: this.$t('settings.language.en') },
],
}
},

View File

@ -105,3 +105,26 @@ export const checkEmailQuery = gql`
}
}
`
export const communityInfo = gql`
query {
getCommunityInfo {
name
description
registerUrl
url
}
}
`
export const communities = gql`
query {
communities {
id
name
url
description
registerUrl
}
}
`

View File

@ -1,14 +1,22 @@
{
"back": "Zurück",
"community": "Gemeinschaft",
"communitys": {
"form": {
"date_period": "Datum / Zeitraum",
"hours": "Stunden",
"hours_report": "Stundenbericht",
"more_hours": "weitere Stunden",
"submit": "Einreichen"
}
"community": {
"choose-another-community": "Eine andere Gemeinschaft auswählen",
"communities": {
"form": {
"date_period": "Datum / Zeitraum",
"hours": "Stunden",
"hours_report": "Stundenbericht",
"more_hours": "weitere Stunden",
"submit": "Einreichen"
}
},
"community": "Gemeinschaft",
"continue-to-registration": "Weiter zur Registrierung",
"current-community": "Aktuelle Gemeinschaft",
"location": "Ort:",
"other-communities": "Weitere Gemeinschaften",
"switch-to-this-community": "zu dieser Gemeinschaft wechseln"
},
"decay": {
"calculation_decay": "Berechnung der Vergänglichkeit",
@ -19,7 +27,6 @@
"decayStart": " - Startblock für Vergänglichkeit am: ",
"decay_introduced": "Die Vergänglichkeit wurde Eingeführt am ",
"decay_since_last_transaction": "Vergänglichkeit seit der letzten Transaktion",
"fromCommunity": "Aus der Gemeinschaft",
"hours": "Stunden",
"last_transaction": "Letzte Transaktion",
"minutes": "Minuten",

View File

@ -1,14 +1,22 @@
{
"back": "Back",
"community": "Community",
"communitys": {
"form": {
"date_period": "Date / Period",
"hours": "hours",
"hours_report": "Hourly report",
"more_hours": "more hours",
"submit": "submit"
}
"community": {
"choose-another-community": "Choose another community",
"communities": {
"form": {
"date_period": "Date / Period",
"hours": "hours",
"hours_report": "Hourly report",
"more_hours": "more hours",
"submit": "submit"
}
},
"community": "Community",
"continue-to-registration": "Continue to registration",
"current-community": "Current community",
"location": "Location:",
"other-communities": "Other communities",
"switch-to-this-community": "switch to this community"
},
"decay": {
"calculation_decay": "Calculation of Decay",
@ -19,7 +27,6 @@
"decayStart": " - Starting block for decay at: ",
"decay_introduced": "Decay was Introduced on",
"decay_since_last_transaction": "Decay since the last transaction",
"fromCommunity": "From the community",
"hours": "Hours",
"last_transaction": "Last transaction:",
"minutes": "Minutes",

View File

@ -6,7 +6,11 @@ import { loadAllRules } from './validation-rules'
import { ApolloClient, ApolloLink, InMemoryCache, HttpLink } from 'apollo-boost'
import VueApollo from 'vue-apollo'
import CONFIG from './config'
import VueApexCharts from 'vue-apexcharts'
import addNavigationGuards from './routes/guards'
import { store } from './store/store'
import router from './routes/router'
@ -49,13 +53,7 @@ Vue.config.productionTip = false
loadAllRules(i18n)
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !store.state.token) {
next({ path: '/login' })
} else {
next()
}
})
addNavigationGuards(router, store)
Vue.use(VueApexCharts)
Vue.component('apexchart', VueApexCharts)

View File

@ -1,4 +1,4 @@
import clickOutside from '@/directives/click-ouside.js'
// import clickOutside from '@/directives/click-ouside.js'
import { focus } from 'vue-focus'
/**
@ -7,7 +7,7 @@ import { focus } from 'vue-focus'
const GlobalDirectives = {
install(Vue) {
Vue.directive('click-outside', clickOutside)
// Vue.directive('click-outside', clickOutside)
Vue.directive('focus', focus)
},
}

View File

@ -0,0 +1,18 @@
const addNavigationGuards = (router, store) => {
router.beforeEach((to, from, next) => {
// handle publisherId
const publisherId = to.query.pid
if (publisherId) {
store.commit('publisherId', publisherId)
delete to.query.pid
}
// handle authentication
if (to.meta.requiresAuth && !store.state.token) {
next({ path: '/login' })
} else {
next()
}
})
}
export default addNavigationGuards

View File

@ -0,0 +1,47 @@
import addNavigationGuards from './guards'
import router from './router'
const storeCommitMock = jest.fn()
const store = {
commit: storeCommitMock,
state: {
token: null,
},
}
addNavigationGuards(router, store)
describe('navigation guards', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('publisher ID', () => {
it('commits the pid to the store when present', async () => {
await router.push({ path: 'register', query: { pid: 42 } })
expect(storeCommitMock).toBeCalledWith('publisherId', '42')
})
it('does not commit the pid when not present', async () => {
await router.push({ path: 'password' })
expect(storeCommitMock).not.toBeCalled()
})
})
describe('authorization', () => {
const navGuard = router.beforeHooks[0]
const next = jest.fn()
it('redirects to login when not authorized', () => {
navGuard({ meta: { requiresAuth: true }, query: {} }, {}, next)
expect(next).toBeCalledWith({ path: '/login' })
})
it('does not redirect to login when authorized', () => {
store.state.token = 'valid token'
navGuard({ meta: { requiresAuth: true }, query: {} }, {}, next)
expect(next).toBeCalledWith()
})
})
})

View File

@ -5,10 +5,9 @@ import CONFIG from '../config'
Vue.use(VueRouter)
// configure router
const router = new VueRouter({
base: '/vue',
routes, // short for routes: routes
routes,
linkActiveClass: 'active',
mode: 'history',
scrollBehavior: (to, from, savedPosition) => {

View File

@ -55,8 +55,8 @@ describe('router', () => {
expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' })
})
it('has ten routes defined', () => {
expect(routes).toHaveLength(10)
it('has twelve routes defined', () => {
expect(routes).toHaveLength(12)
})
describe('overview', () => {
@ -131,6 +131,20 @@ describe('router', () => {
})
})
describe('register-community', () => {
it('loads the "registerCommunity" component', async () => {
const component = await routes.find((r) => r.path === '/register-community').component()
expect(component.default.name).toBe('registerCommunity')
})
})
describe('select-community', () => {
it('loads the "registerSelectCommunity" component', async () => {
const component = await routes.find((r) => r.path === '/select-community').component()
expect(component.default.name).toBe('registerSelectCommunity')
})
})
describe('reset', () => {
it('loads the "ResetPassword" component', async () => {
const component = await routes.find((r) => r.path === '/reset/:optin').component()

View File

@ -55,6 +55,14 @@ const routes = [
path: '/password',
component: () => import('../views/Pages/ForgotPassword.vue'),
},
{
path: '/register-community',
component: () => import('../views/Pages/RegisterCommunity.vue'),
},
{
path: '/select-community',
component: () => import('../views/Pages/RegisterSelectCommunity.vue'),
},
{
path: '/reset/:optin',
component: () => import('../views/Pages/ResetPassword.vue'),

View File

@ -29,6 +29,15 @@ export const mutations = {
newsletterState: (state, newsletterState) => {
state.newsletterState = newsletterState
},
publisherId: (state, publisherId) => {
state.publisherId = publisherId
},
community: (state, community) => {
state.community = community
},
coinanimation: (state, coinanimation) => {
state.coinanimation = coinanimation
},
}
export const actions = {
@ -39,6 +48,7 @@ export const actions = {
commit('firstName', data.firstName)
commit('lastName', data.lastName)
commit('description', data.description)
commit('coinanimation', data.coinanimation)
commit('newsletterState', data.klickTipp.newsletterState)
},
logout: ({ commit, state }) => {
@ -48,6 +58,7 @@ export const actions = {
commit('firstName', '')
commit('lastName', '')
commit('description', '')
commit('coinanimation', true)
commit('newsletterState', null)
localStorage.clear()
},
@ -69,6 +80,7 @@ export const store = new Vuex.Store({
token: null,
coinanimation: true,
newsletterState: null,
community: null,
},
getters: {},
// Syncronous mutation of the state

View File

@ -8,7 +8,10 @@ const {
firstName,
lastName,
description,
coinanimation,
newsletterState,
publisherId,
community,
} = mutations
const { login, logout } = actions
@ -70,6 +73,14 @@ describe('Vuex store', () => {
})
})
describe('coinanimation', () => {
it('sets the state of coinanimation', () => {
const state = { coinanimation: true }
coinanimation(state, false)
expect(state.coinanimation).toEqual(false)
})
})
describe('newsletterState', () => {
it('sets the state of newsletterState', () => {
const state = { newsletterState: null }
@ -77,6 +88,32 @@ describe('Vuex store', () => {
expect(state.newsletterState).toEqual(true)
})
})
describe('publisherId', () => {
it('sets the state of publisherId', () => {
const state = {}
publisherId(state, 42)
expect(state.publisherId).toEqual(42)
})
})
describe('community', () => {
it('sets the state of community', () => {
const state = {}
community(state, {
name: 'test12',
description: 'test community 12',
url: 'http://test12.test12/',
registerUrl: 'http://test12.test12/vue/register',
})
expect(state.community).toEqual({
name: 'test12',
description: 'test community 12',
url: 'http://test12.test12/',
registerUrl: 'http://test12.test12/vue/register',
})
})
})
})
describe('actions', () => {
@ -90,14 +127,15 @@ describe('Vuex store', () => {
firstName: 'Peter',
lastName: 'Lustig',
description: 'Nickelbrille',
coinanimation: false,
klickTipp: {
newsletterState: true,
},
}
it('calls seven commits', () => {
it('calls eight commits', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenCalledTimes(7)
expect(commit).toHaveBeenCalledTimes(8)
})
it('commits email', () => {
@ -130,9 +168,14 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenNthCalledWith(6, 'description', 'Nickelbrille')
})
it('commits coinanimation', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(7, 'coinanimation', false)
})
it('commits newsletterState', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(7, 'newsletterState', true)
expect(commit).toHaveBeenNthCalledWith(8, 'newsletterState', true)
})
})
@ -140,9 +183,9 @@ describe('Vuex store', () => {
const commit = jest.fn()
const state = {}
it('calls six commits', () => {
it('calls eight commits', () => {
logout({ commit, state })
expect(commit).toHaveBeenCalledTimes(7)
expect(commit).toHaveBeenCalledTimes(8)
})
it('commits token', () => {
@ -175,9 +218,14 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenNthCalledWith(6, 'description', '')
})
it('commits coinanimation', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(7, 'coinanimation', true)
})
it('commits newsletterState', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(7, 'newsletterState', null)
expect(commit).toHaveBeenNthCalledWith(8, 'newsletterState', null)
})
// how to get this working?

View File

@ -4,14 +4,20 @@ import Login from './Login'
const localVue = global.localVue
const loginQueryMock = jest.fn().mockResolvedValue({
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
login: 'token',
getCommunityInfo: {
name: 'test12',
description: 'test community 12',
url: 'http://test12.test12/',
registerUrl: 'http://test12.test12/vue/register',
},
},
})
const toastErrorMock = jest.fn()
const mockStoreDispach = jest.fn()
const mockStoreCommit = jest.fn()
const mockRouterPush = jest.fn()
const spinnerHideMock = jest.fn()
const spinnerMock = jest.fn(() => {
@ -30,6 +36,15 @@ describe('Login', () => {
$t: jest.fn((t) => t),
$store: {
dispatch: mockStoreDispach,
commit: mockStoreCommit,
state: {
community: {
name: 'Gradido Entwicklung',
url: 'http://localhost/vue/',
registerUrl: 'http://localhost/vue/register',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
},
},
},
$loading: {
show: spinnerMock,
@ -41,7 +56,7 @@ describe('Login', () => {
error: toastErrorMock,
},
$apollo: {
query: loginQueryMock,
query: apolloQueryMock,
},
}
@ -62,20 +77,54 @@ describe('Login', () => {
expect(wrapper.find('div.login-form').exists()).toBeTruthy()
})
it('commits the community info to the store', () => {
expect(mockStoreCommit).toBeCalledWith('community', {
name: 'test12',
description: 'test community 12',
url: 'http://test12.test12/',
registerUrl: 'http://test12.test12/vue/register',
})
})
describe('communities gives back error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({
message: 'Failed to get communities',
})
wrapper = Wrapper()
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('Failed to get communities')
})
})
describe('Login header', () => {
it('has a welcome message', () => {
expect(wrapper.find('div.header').text()).toBe('Gradido site.login.community')
})
})
describe('Community Data', () => {
it('has a Community name', () => {
expect(wrapper.find('.test-communitydata b').text()).toBe('Gradido Entwicklung')
})
it('has a Community description', () => {
expect(wrapper.find('.test-communitydata p').text()).toBe(
'Die lokale Entwicklungsumgebung von Gradido.',
)
})
})
describe('links', () => {
it('has a link "Forgot Password?"', () => {
it('has a link "Forgot Password"', () => {
expect(wrapper.findAllComponents(RouterLinkStub).at(0).text()).toEqual(
'settings.password.forgot_pwd',
)
})
it('links to /password when clicking "Forgot Password?"', () => {
it('links to /password when clicking "Forgot Password"', () => {
expect(wrapper.findAllComponents(RouterLinkStub).at(0).props().to).toBe('/password')
})
@ -86,7 +135,9 @@ describe('Login', () => {
})
it('links to /register when clicking "Create new account"', () => {
expect(wrapper.findAllComponents(RouterLinkStub).at(1).props().to).toBe('/register')
expect(wrapper.findAllComponents(RouterLinkStub).at(1).props().to).toBe(
'/register-community',
)
})
})
@ -135,10 +186,15 @@ describe('Login', () => {
await flushPromises()
await wrapper.find('form').trigger('submit')
await flushPromises()
apolloQueryMock.mockResolvedValue({
data: {
login: 'token',
},
})
})
it('calls the API with the given data', () => {
expect(loginQueryMock).toBeCalledWith(
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
email: 'user@example.org',
@ -168,7 +224,7 @@ describe('Login', () => {
describe('login fails', () => {
beforeEach(() => {
loginQueryMock.mockRejectedValue({
apolloQueryMock.mockRejectedValue({
message: 'Ouch!',
})
})

View File

@ -18,9 +18,14 @@
<b-col lg="5" md="7">
<b-card no-body class="border-0 mb-0" style="background-color: #ebebeba3 !important">
<b-card-body class="p-4">
<div class="text-center text-muted mb-4">
<small>{{ $t('login') }}</small>
<div class="text-center text-muted mb-4 test-communitydata">
<b>{{ $store.state.community.name }}</b>
<p class="text-lead">
{{ $store.state.community.description }}
</p>
{{ $t('login') }}
</div>
<validation-observer ref="observer" v-slot="{ handleSubmit }">
<b-form @submit.stop.prevent="handleSubmit(onSubmit)">
<input-email v-model="form.email"></input-email>
@ -38,13 +43,17 @@
</b-card-body>
</b-card>
<b-row class="mt-3">
<b-col cols="6">
<router-link to="/password">
<b-col cols="6" class="text-center text-sm-left col-12 col-sm-6 pb-5">
<router-link to="/password" class="mt-3">
{{ $t('settings.password.forgot_pwd') }}
</router-link>
</b-col>
<b-col cols="6" class="text-right" v-show="allowRegister">
<router-link to="/register">
<b-col
cols="6"
class="text-center text-sm-right col-12 col-sm-6"
v-show="allowRegister"
>
<router-link to="/register-community" class="mt-3">
{{ $t('site.login.new_wallet') }}
</router-link>
</b-col>
@ -58,7 +67,7 @@
import CONFIG from '../../config'
import InputPassword from '../../components/Inputs/InputPassword'
import InputEmail from '../../components/Inputs/InputEmail'
import { login } from '../../graphql/queries'
import { login, communityInfo } from '../../graphql/queries'
export default {
name: 'login',
@ -103,6 +112,21 @@ export default {
this.$toasted.error(this.$t('error.no-account'))
})
},
async onCreated() {
this.$apollo
.query({
query: communityInfo,
})
.then((result) => {
this.$store.commit('community', result.data.getCommunityInfo)
})
.catch((error) => {
this.$toasted.error(error.message)
})
},
},
created() {
this.onCreated()
},
}
</script>

View File

@ -26,6 +26,12 @@ describe('Register', () => {
state: {
email: 'peter@lustig.de',
language: 'en',
community: {
name: 'Gradido Entwicklung',
url: 'http://localhost/vue/',
registerUrl: 'http://localhost/vue/register',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
},
},
},
}
@ -53,6 +59,18 @@ describe('Register', () => {
})
})
describe('Community Data', () => {
it('has a Community name?', () => {
expect(wrapper.find('.test-communitydata b').text()).toBe('Gradido Entwicklung')
})
it('has a Community description?', () => {
expect(wrapper.find('.test-communitydata p').text()).toBe(
'Die lokale Entwicklungsumgebung von Gradido.',
)
})
})
describe('links', () => {
it('has a link "Back"', () => {
expect(wrapper.find('.test-button-back').text()).toEqual('back')
@ -127,6 +145,18 @@ describe('Register', () => {
})
})
describe('link Choose another community', () => {
it('has a link "Choose another community"', () => {
expect(wrapper.find('.test-button-another-community').text()).toEqual(
'community.choose-another-community',
)
})
it('links to /select-community when clicking "Choose another community"', () => {
expect(wrapper.find('.test-button-another-community').props().to).toBe('/select-community')
})
})
describe('API calls', () => {
beforeEach(() => {
wrapper.find('#registerFirstname').setValue('Max')

View File

@ -13,15 +13,21 @@
</div>
</b-container>
</div>
<!-- Page content -->
<b-container class="mt--8 p-1">
<!-- Table -->
<b-row class="justify-content-center">
<b-col lg="6" md="8">
<b-card no-body class="border-0" style="background-color: #ebebeba3 !important">
<b-card-body class="p-4">
<div class="text-center text-muted mb-4">
<small>{{ $t('signup') }}</small>
<div class="text-center text-muted mb-4 test-communitydata">
<b>{{ $store.state.community.name }}</b>
<p class="text-lead">
{{ $store.state.community.description }}
</p>
<div>{{ $t('signup') }}</div>
</div>
<validation-observer ref="observer" v-slot="{ handleSubmit }">
@ -118,12 +124,13 @@
<div class="text-center">
<div class="text-center">
<b-button class="ml-2 test-button-back" to="/login">
{{ $t('back') }}
</b-button>
<router-link class="test-button-back" to="/login">
<b-button variant="outline-secondary">
{{ $t('back') }}
</b-button>
</router-link>
<b-button
:disabled="!(namesFilled && emailFilled && form.agree && languageFilled)"
:disabled="!(namesFilled && emailFilled && form.agree && !!language)"
type="submit"
variant="primary"
>
@ -138,6 +145,13 @@
</b-col>
</b-row>
</b-container>
<div class="text-center pt-4">
<router-link class="test-button-another-community" to="/select-community">
<b-button variant="outline-secondary">
{{ $t('community.choose-another-community') }}
</b-button>
</router-link>
</div>
</div>
</template>
<script>
@ -224,9 +238,6 @@ export default {
emailFilled() {
return this.form.email !== ''
},
languageFilled() {
return !!this.language
},
},
}
</script>

View File

@ -0,0 +1,82 @@
import { mount, RouterLinkStub } from '@vue/test-utils'
import RegisterCommunity from './RegisterCommunity'
const localVue = global.localVue
describe('RegisterCommunity', () => {
let wrapper
const mocks = {
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$store: {
state: {
community: {
name: 'Gradido Entwicklung',
url: 'http://localhost/vue/',
registerUrl: 'http://localhost/vue/register',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
},
},
},
}
const stubs = {
RouterLink: RouterLinkStub,
}
const Wrapper = () => {
return mount(RegisterCommunity, { localVue, mocks, stubs })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the Div Element "#register-community"', () => {
expect(wrapper.find('div#register-community').exists()).toBeTruthy()
})
describe('Displaying the current community info', () => {
it('has a current community name', () => {
expect(wrapper.find('.header h1').text()).toBe('Gradido Entwicklung')
})
it('has a current community description', () => {
expect(wrapper.find('.header p').text()).toBe(
'Die lokale Entwicklungsumgebung von Gradido.',
)
})
it('has a current community location', () => {
expect(wrapper.find('.header p.community-location').text()).toBe('http://localhost/vue/')
})
})
describe('buttons and links', () => {
it('has a button "Continue to registration?"', () => {
expect(wrapper.findAll('a').at(0).text()).toEqual('community.continue-to-registration')
})
it('button links to /register when clicking "Continue to registration"', () => {
expect(wrapper.findAll('a').at(0).props().to).toBe('/register')
})
it('has a button "Choose another community?"', () => {
expect(wrapper.findAll('a').at(1).text()).toEqual('community.choose-another-community')
})
it('button links to /select-community when clicking "Choose another community"', () => {
expect(wrapper.findAll('a').at(1).props().to).toBe('/select-community')
})
it('has a button "Back to Login?"', () => {
expect(wrapper.findAll('a').at(2).text()).toEqual('back')
})
it('button links to /login when clicking "Back to Login"', () => {
expect(wrapper.findAll('a').at(2).props().to).toBe('/login')
})
})
})
})

View File

@ -0,0 +1,60 @@
<template>
<div id="register-community">
<!-- Header -->
<div class="p-3">
<b-container>
<div class="text-center mb-7 header">
<b-row class="justify-content-center">
<b-col xl="5" lg="6" md="8" class="px-2">
<h1>{{ $store.state.community.name }}</h1>
<p class="text-lead">
{{ $store.state.community.description }}
</p>
<p class="text-lead community-location">
{{ $store.state.community.url }}
</p>
</b-col>
</b-row>
<b-row>
<b-col class="text-center">
<router-link to="/register">
<b-button variant="outline-secondary">
{{ $t('community.continue-to-registration') }}
</b-button>
</router-link>
</b-col>
</b-row>
<hr />
<b-row>
<b-col class="text-center">
<router-link to="/select-community">
<b-button variant="outline-secondary">
{{ $t('community.choose-another-community') }}
</b-button>
</router-link>
</b-col>
</b-row>
<hr />
<b-row>
<b-col class="text-center">
<router-link to="/login">
<b-button variant="outline-secondary">{{ $t('back') }}</b-button>
</router-link>
</b-col>
</b-row>
</div>
</b-container>
</div>
</div>
</template>
<script>
export default {
name: 'registerCommunity',
data() {
return {}
},
methods: {},
}
</script>
<style></style>

View File

@ -0,0 +1,127 @@
import { mount, RouterLinkStub } from '@vue/test-utils'
import RegisterSelectCommunity from './RegisterSelectCommunity'
const localVue = global.localVue
const spinnerHideMock = jest.fn()
const spinnerMock = jest.fn(() => {
return {
hide: spinnerHideMock,
}
})
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
communities: [
{
id: 1,
name: 'Gradido Entwicklung',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
url: 'http://localhost/vue/',
registerUrl: 'http://localhost/vue/register-community',
},
{
id: 2,
name: 'Gradido Staging',
description: 'Der Testserver der Gradido-Akademie.',
url: 'https://stage1.gradido.net/vue/',
registerUrl: 'https://stage1.gradido.net/vue/register-community',
},
{
id: 3,
name: 'Gradido-Akademie',
description: 'Freies Institut für Wirtschaftsbionik.',
url: 'https://gradido.net',
registerUrl: 'https://gdd1.gradido.com/vue/register-community',
},
],
},
})
const toasterMock = jest.fn()
describe('RegisterSelectCommunity', () => {
let wrapper
const mocks = {
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$store: {
state: {
community: {
name: 'Gradido Entwicklung',
url: 'http://localhost/vue/',
registerUrl: 'http://localhost/vue/register',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
},
},
},
$apollo: {
query: apolloQueryMock,
},
$loading: {
show: spinnerMock,
},
$toasted: {
error: toasterMock,
},
}
const stubs = {
RouterLink: RouterLinkStub,
}
const Wrapper = () => {
return mount(RegisterSelectCommunity, { localVue, mocks, stubs })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the Div Element "#register-select-community"', () => {
expect(wrapper.find('div#register-select-community').exists()).toBeTruthy()
})
it('starts with a spinner', () => {
expect(spinnerMock).toBeCalled()
})
describe('calls the apollo query', () => {
describe('server returns data', () => {
it('calls the API to get the data', () => {
expect(apolloQueryMock).toBeCalled()
})
it('shows two other communities', () => {
expect(wrapper.findAll('div.bg-secondary')).toHaveLength(2)
})
it('hides the spinner', () => {
expect(spinnerHideMock).toBeCalled()
})
})
describe('server response is error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({
message: 'Wrong thing',
})
wrapper = Wrapper()
})
it('toast an error', () => {
expect(toasterMock).toBeCalledWith('Wrong thing')
})
it('hides the spinner', () => {
expect(spinnerHideMock).toBeCalled()
})
})
})
})
})

View File

@ -0,0 +1,104 @@
<template>
<div id="register-select-community">
<b-container class="text-center">
<div class="pb-3">{{ $t('community.current-community') }}</div>
<div v-if="!pending">
<b-card class="border-0 mb-0" bg-variant="primary">
<b>{{ $store.state.community.name }}</b>
<br />
{{ $store.state.community.description }}
<br />
<router-link to="/register">
<b-button variant="outline-secondary">
{{ $t('community.continue-to-registration') }}
</b-button>
</router-link>
</b-card>
<hr />
<div>{{ $t('community.other-communities') }}</div>
<div v-for="community in communities" :key="community.id" class="pb-3">
<b-card bg-variant="secondary">
<b>{{ community.name }}</b>
<br />
{{ community.description }}
<br />
<b>
<small>
<b-link :href="community.url">{{ community.url }}</b-link>
</small>
</b>
<br />
<b-button variant="outline-secondary" :href="community.registerUrl">
{{ $t('community.switch-to-this-community') }}
</b-button>
</b-card>
</div>
</div>
<div class="text-center py-lg-4">
<router-link to="/login">
<b-button variant="outline-secondary">{{ $t('back') }}</b-button>
</router-link>
</div>
</b-container>
</div>
</template>
<script>
import { communities } from '../../graphql/queries'
export default {
name: 'registerSelectCommunity',
data() {
return {
communities: [],
pending: true,
}
},
methods: {
async getCommunities() {
const loader = this.$loading.show({
container: this.$refs.header,
})
this.$apollo
.query({
query: communities,
fetchPolicy: 'network-only',
})
.then((response) => {
this.communities = response.data.communities.filter(
(c) => c.name !== this.$store.state.community.name,
)
})
.catch((error) => {
this.$toasted.error(error.message)
})
loader.hide()
this.pending = false
},
},
created() {
this.getCommunities()
},
}
</script>
<style>
.alert-light {
color: #424543;
background-color: #bac1c84a;
border-color: #ffffff00;
}
.bg-primary {
background-color: #5e72e41f !important;
}
.bg-secondary {
background-color: #525f7f0f !important;
}
.card-footer {
background-color: #ffffff5e !important;
}
</style>

View File

@ -67,12 +67,13 @@ describe('GddSend', () => {
it('trims the email after blur', async () => {
await wrapper.find('#input-group-1').find('input').setValue(' valid@email.com ')
await wrapper.find('#input-group-1').find('input').trigger('blur')
await flushPromises()
expect(wrapper.vm.form.email).toBe('valid@email.com')
})
})
describe('ammount field', () => {
describe('amount field', () => {
it('has an input field of type text', () => {
expect(wrapper.find('#input-group-2').find('input').attributes('type')).toBe('text')
})
@ -91,6 +92,13 @@ describe('GddSend', () => {
)
})
it('does not update form amount when invalid', async () => {
await wrapper.find('#input-group-2').find('input').setValue('invalid')
await wrapper.find('#input-group-2').find('input').trigger('blur')
await flushPromises()
expect(wrapper.vm.form.amountValue).toBe(0)
})
it('flushes an error message when no valid amount is given', async () => {
await wrapper.find('#input-group-2').find('input').setValue('a')
await flushPromises()
@ -150,11 +158,11 @@ describe('GddSend', () => {
it('clears all fields on click', async () => {
await wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv')
await wrapper.find('#input-group-2').find('input').setValue('87.23')
await wrapper.find('#input-group-3').find('textarea').setValue('Long enugh')
await wrapper.find('#input-group-3').find('textarea').setValue('Long enough')
await flushPromises()
expect(wrapper.vm.form.email).toBe('someone@watches.tv')
expect(wrapper.vm.form.amount).toBe('87.23')
expect(wrapper.vm.form.memo).toBe('Long enugh')
expect(wrapper.vm.form.memo).toBe('Long enough')
await wrapper.find('button[type="reset"]').trigger('click')
await flushPromises()
expect(wrapper.vm.form.email).toBe('')
@ -167,7 +175,7 @@ describe('GddSend', () => {
beforeEach(async () => {
await wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv')
await wrapper.find('#input-group-2').find('input').setValue('87.23')
await wrapper.find('#input-group-3').find('textarea').setValue('Long enugh')
await wrapper.find('#input-group-3').find('textarea').setValue('Long enough')
await wrapper.find('form').trigger('submit')
await flushPromises()
})
@ -179,7 +187,7 @@ describe('GddSend', () => {
{
email: 'someone@watches.tv',
amount: 87.23,
memo: 'Long enugh',
memo: 'Long enough',
},
],
])

View File

@ -0,0 +1,36 @@
import { mount } from '@vue/test-utils'
import UserCard from './UserCard'
const localVue = global.localVue
describe('UserCard', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$n: jest.fn((n) => String(n)),
$store: {
state: {
email: 'user@example.org',
},
},
}
const Wrapper = () => {
return mount(UserCard, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the Div Element ".userdata-card"', () => {
expect(wrapper.find('div.userdata-card').exists()).toBeTruthy()
})
it('renders the Div Element "vue-qrcode"', () => {
expect(wrapper.find('vue-qrcode'))
})
})
})

View File

@ -1,30 +1,32 @@
<template>
<b-card class="bg-transparent border-0">
<div class="w-100 text-center">
<vue-qrcode
v-if="$store.state.email"
:value="$store.state.email"
type="image/png"
></vue-qrcode>
</div>
<div class="userdata-card">
<b-card class="bg-transparent border-0">
<div class="w-100 text-center">
<vue-qrcode
v-if="$store.state.email"
:value="$store.state.email"
type="image/png"
></vue-qrcode>
</div>
<div class="card-profile-stats d-flex justify-content-center mt-md-5">
<div>
<span class="heading">
{{ $n(balance, 'decimal') }}
</span>
<span class="description">GDD</span>
<div class="card-profile-stats d-flex justify-content-center mt-md-5">
<div>
<span class="heading">
{{ $n(balance, 'decimal') }}
</span>
<span class="description">GDD</span>
</div>
<div>
<span class="heading">{{ transactionCount }}</span>
<span class="description">{{ $t('transactions') }}</span>
</div>
<div>
<span class="heading">--</span>
<span class="description">{{ $t('community.community') }}</span>
</div>
</div>
<div>
<span class="heading">{{ transactionCount }}</span>
<span class="description">{{ $t('transactions') }}</span>
</div>
<div>
<span class="heading">--</span>
<span class="description">{{ $t('community') }}</span>
</div>
</div>
</b-card>
</b-card>
</div>
</template>
<script>
import VueQrcode from 'vue-qrcode'

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils'
import UserCardCoinAnimation from './UserCard_CoinAnimation'
import { updateUserInfos } from '../../../graphql/mutations'
const localVue = global.localVue
@ -17,6 +18,7 @@ describe('UserCard_CoinAnimation', () => {
$store: {
state: {
language: 'de',
coinanimation: true,
},
commit: storeCommitMock,
},
@ -25,7 +27,7 @@ describe('UserCard_CoinAnimation', () => {
error: toastErrorMock,
},
$apollo: {
query: mockAPIcall,
mutate: mockAPIcall,
},
}
@ -35,6 +37,7 @@ describe('UserCard_CoinAnimation', () => {
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
@ -45,5 +48,84 @@ describe('UserCard_CoinAnimation', () => {
it('has an edit BFormCheckbox switch', () => {
expect(wrapper.find('.Test-BFormCheckbox').exists()).toBeTruthy()
})
describe('enable with success', () => {
beforeEach(async () => {
await wrapper.setData({ CoinAnimationStatus: false })
mockAPIcall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 1,
},
},
})
await wrapper.find('input').setChecked()
})
it('calls the updateUserInfos mutation', () => {
expect(mockAPIcall).toBeCalledWith({
mutation: updateUserInfos,
variables: {
coinanimation: true,
},
})
})
it('updates the store', () => {
expect(storeCommitMock).toBeCalledWith('coinanimation', true)
})
it('toasts a success message', () => {
expect(toastSuccessMock).toBeCalledWith('settings.coinanimation.True')
})
})
describe('disable with success', () => {
beforeEach(async () => {
await wrapper.setData({ CoinAnimationStatus: true })
mockAPIcall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 1,
},
},
})
await wrapper.find('input').setChecked(false)
})
it('calls the subscribe mutation', () => {
expect(mockAPIcall).toBeCalledWith({
mutation: updateUserInfos,
variables: {
coinanimation: false,
},
})
})
it('updates the store', () => {
expect(storeCommitMock).toBeCalledWith('coinanimation', false)
})
it('toasts a success message', () => {
expect(toastSuccessMock).toBeCalledWith('settings.coinanimation.False')
})
})
describe('disable with server error', () => {
beforeEach(() => {
mockAPIcall.mockRejectedValue({
message: 'Ouch',
})
wrapper.find('input').trigger('change')
})
it('resets the CoinAnimationStatus', () => {
expect(wrapper.vm.CoinAnimationStatus).toBeTruthy()
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('Ouch')
})
})
})
})

View File

@ -36,12 +36,9 @@ export default {
name: 'FormUserCoinAnimation',
data() {
return {
CoinAnimationStatus: true,
CoinAnimationStatus: this.$store.state.coinanimation,
}
},
created() {
this.CoinAnimationStatus = this.$store.state.coinanimation /* existiert noch nicht im store */
},
methods: {
async onSubmit() {
this.$apollo
@ -52,7 +49,7 @@ export default {
},
})
.then(() => {
this.$store.state.coinanimation = this.CoinAnimationStatus
this.$store.commit('coinanimation', this.CoinAnimationStatus)
this.$toasted.success(
this.CoinAnimationStatus
? this.$t('settings.coinanimation.True')
@ -60,6 +57,7 @@ export default {
)
})
.catch((error) => {
this.CoinAnimationStatus = this.$store.state.coinanimation
this.$toasted.error(error.message)
})
},

View File

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

View File

@ -111,7 +111,6 @@ export default {
.mutate({
mutation: updateUserInfos,
variables: {
email: this.$store.state.email,
firstName: this.form.firstName,
lastName: this.form.lastName,
description: this.form.description,

View File

@ -75,7 +75,6 @@ describe('UserCard_FormUserMail', () => {
expect(mockAPIcall).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
email: 'user@example.org',
newEmail: 'test@example.org',
},
}),
@ -104,7 +103,6 @@ describe('UserCard_FormUserMail', () => {
expect(mockAPIcall).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
email: 'user@example.org',
newEmail: 'test@example.org',
},
}),

View File

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

View File

@ -15,11 +15,6 @@ describe('UserCard_FormUserPasswort', () => {
const mocks = {
$t: jest.fn((t) => t),
$store: {
state: {
email: 'user@example.org',
},
},
$toasted: {
success: toastSuccessMock,
error: toastErrorMock,
@ -191,7 +186,6 @@ describe('UserCard_FormUserPasswort', () => {
expect(changePasswordProfileMock).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
email: 'user@example.org',
password: '1234',
passwordNew: 'Aa123456_',
},

View File

@ -76,7 +76,6 @@ export default {
.mutate({
mutation: updateUserInfos,
variables: {
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: {
email: 'user@example.org',
username: '',
},
commit: storeCommitMock,
@ -109,7 +108,6 @@ describe('UserCard_FormUsername', () => {
expect(mockAPIcall).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
email: 'user@example.org',
username: 'username',
},
}),
@ -148,7 +146,6 @@ describe('UserCard_FormUsername', () => {
expect(mockAPIcall).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
email: 'user@example.org',
username: 'username',
},
}),

View File

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

View File

@ -23,7 +23,6 @@ describe('UserCard_Language', () => {
$store: {
state: {
language: 'de',
email: 'peter@lustig.de',
},
commit: storeCommitMock,
},
@ -127,7 +126,6 @@ describe('UserCard_Language', () => {
expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({
variables: {
email: 'peter@lustig.de',
locale: 'en',
},
}),

View File

@ -89,7 +89,6 @@ export default {
.mutate({
mutation: updateUserInfos,
variables: {
email: this.$store.state.email,
locale: this.language,
},
})
@ -106,7 +105,7 @@ export default {
})
},
buildTagFromLanguageString() {
return 'languages.' + this.$store.state.language
return 'settings.language.' + this.$store.state.language
},
},
}

View File

@ -38,6 +38,7 @@ describe('UserCard_Newsletter', () => {
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
@ -51,13 +52,13 @@ describe('UserCard_Newsletter', () => {
describe('unsubscribe with success', () => {
beforeEach(async () => {
await wrapper.setData({ newsletterState: false })
await wrapper.setData({ newsletterState: true })
mockAPIcall.mockResolvedValue({
data: {
unsubscribeNewsletter: true,
},
})
await wrapper.find('input').trigger('change')
await wrapper.find('input').setChecked(false)
})
it('calls the unsubscribe mutation', () => {
@ -80,13 +81,13 @@ describe('UserCard_Newsletter', () => {
describe('subscribe with success', () => {
beforeEach(async () => {
await wrapper.setData({ newsletterState: true })
await wrapper.setData({ newsletterState: false })
mockAPIcall.mockResolvedValue({
data: {
subscribeNewsletter: true,
},
})
wrapper.find('input').trigger('change')
await wrapper.find('input').setChecked()
})
it('calls the subscribe mutation', () => {
@ -104,7 +105,7 @@ describe('UserCard_Newsletter', () => {
})
it('toasts a success message', () => {
expect(toastSuccessMock).toBeCalledWith('settings.newsletter.newsletterFalse')
expect(toastSuccessMock).toBeCalledWith('settings.newsletter.newsletterTrue')
})
})

View File

@ -14,7 +14,7 @@ import VueQrcode from 'vue-qrcode'
import VueMoment from 'vue-moment'
import VueApexCharts from 'vue-apexcharts'
import clickOutside from '@/directives/click-ouside.js'
// import clickOutside from '@/directives/click-ouside.js'
import { focus } from 'vue-focus'
import { loadAllRules } from '../src/validation-rules'
@ -48,7 +48,7 @@ global.localVue.use(VueMoment)
global.localVue.use(VueApexCharts)
global.localVue.component('validation-provider', ValidationProvider)
global.localVue.component('validation-observer', ValidationObserver)
global.localVue.directive('click-outside', clickOutside)
// global.localVue.directive('click-outside', clickOutside)
global.localVue.directive('focus', focus)
// throw errors for vue warnings to force the programmers to take care about warnings

View File

@ -328,6 +328,7 @@ const Poco::UInt16* Passphrase::getWordIndices() const
bool Passphrase::checkIfValid()
{
if (!mWordSource) return false;
std::istringstream iss(mPassphraseString);
std::vector<std::string> results(std::istream_iterator<std::string>{iss},
std::istream_iterator<std::string>());

View File

@ -150,16 +150,16 @@ Poco::JSON::Object* JsonCreateUser::handle(Poco::Dynamic::Var params)
emailOptIn->setBaseUrl(user->getGroupBaseUrl() + ServerConfig::g_frontend_checkEmailPath);
em->addEmail(new model::Email(emailOptIn, user, model::Email::convertTypeFromInt(emailType)));
Poco::JSON::Object* result = stateSuccess();
result->set("user", user->getJson());
if (login_after_register && session) {
Poco::JSON::Object* result = stateSuccess();
if(group_was_not_set) {
Poco::JSON::Array infos;
infos.add("group_id was not set, use 1 as default!");
result->set("info", infos);
}
result->set("session_id", session->getHandle());
return result;
}
return stateSuccess();
return result;
}

View File

@ -0,0 +1,16 @@
#include "JsonHasElopage.h"
#include "../model/table/ElopageBuy.h"
Poco::JSON::Object* JsonHasElopage::handle(Poco::Dynamic::Var params)
{
auto result = checkAndLoadSession(params);
if (result) {
return result;
}
auto elopage_buy = Poco::AutoPtr<model::table::ElopageBuy>(new model::table::ElopageBuy);
result = stateSuccess();
result->set("hasElopage", elopage_buy->isExistInDB("email", mSession->getNewUser()->getModel()->getEmail()));
return result;
}

View File

@ -0,0 +1,15 @@
#ifndef __JSON_INTERFACE_JSON_HAS_ELOPAGE_
#define __JSON_INTERFACE_JSON_HAS_ELOPAGE_
#include "JsonRequestHandler.h"
class JsonHasElopage : public JsonRequestHandler
{
public:
Poco::JSON::Object* handle(Poco::Dynamic::Var params);
protected:
};
#endif // __JSON_INTERFACE_JSON_HAS_ELOPAGE_

View File

@ -81,22 +81,30 @@ void JsonRequestHandler::handleRequest(Poco::Net::HTTPServerRequest& request, Po
}
if (json_result) {
NotificationList errors;
if (!json_result->isNull("session_id")) {
int session_id = 0;
try {
json_result->get("session_id").convert(session_id);
}
catch (Poco::Exception& e) {
NotificationList erros;
erros.addError(new Error("json request", "invalid session_id"));
erros.sendErrorsAsEmail();
errors.addError(new Error("json request", "invalid session_id"));
}
if (session_id) {
auto session = SessionManager::getInstance()->getSession(session_id);
response.addCookie(session->getLoginCookie());
}
}
json_result->stringify(responseStream);
try {
json_result->stringify(responseStream);
}
catch (Poco::Exception& e) {
errors.addError(new ParamError("json request", "error on stringify from json result:", e.message()));
errors.addError(new ParamError("json request", "caller url", request.getURI()));
}
if (errors.errorCount()) {
errors.sendErrorsAsEmail();
}
delete json_result;
}

View File

@ -16,6 +16,7 @@
#include "JsonUnknown.h"
#include "JsonGetRunningUserTasks.h"
#include "JsonGetUsers.h"
#include "JsonHasElopage.h"
#include "JsonLoginViaEmailVerificationCode.h"
#include "JsonLogout.h"
#include "JsonNetworkInfos.h"
@ -140,6 +141,9 @@ Poco::Net::HTTPRequestHandler* JsonRequestHandlerFactory::createRequestHandler(c
else if (url_first_part == "/logout") {
return new JsonLogout(client_host);
}
else if (url_first_part == "/hasElopage") {
return new JsonHasElopage;
}
return new JsonUnknown;
}

View File

@ -8,6 +8,8 @@
#include "../lib/DataTypeConverter.h"
#include "../model/table/ElopageBuy.h"
Poco::JSON::Object* JsonUnsecureLogin::handle(Poco::Dynamic::Var params)
{
@ -105,9 +107,21 @@ Poco::JSON::Object* JsonUnsecureLogin::handle(Poco::Dynamic::Var params)
USER_COMPLETE,
USER_DISABLED
*/
// run query for checking if user has already an account async
Poco::AutoPtr<model::table::UserHasElopageTask> hasElopageTask = new model::table::UserHasElopageTask(email);
hasElopageTask->scheduleTask(hasElopageTask);
auto user_state = session->loadUser(email, password);
auto user_model = session->getNewUser()->getModel();
Poco::JSON::Array infos;
// AUTOMATIC ERROR CORRECTION
// if something went wrong by initial key generation for user, generate keys again
if (user_state >= USER_LOADED_FROM_DB && !user_model->getPublicKey()) {
if (session->generateKeys(true, true)) {
user_state = session->getNewUser()->getUserState();
}
}
switch (user_state) {
case USER_EMPTY:
@ -140,7 +154,9 @@ Poco::JSON::Object* JsonUnsecureLogin::handle(Poco::Dynamic::Var params)
session->setClientIp(mClientIP);
if(infos.size() > 0) {
result->set("info", infos);
}
}
AWAIT(hasElopageTask)
result->set("hasElopage", hasElopageTask->hasElopage());
return result;
default:
result->set("state", "error");

View File

@ -746,7 +746,7 @@ void Session::detectSessionState()
bool cryptedPassphrase = userBackups.size() > 0;
for (auto it = userBackups.begin(); it != userBackups.end(); it++) {
auto passphrase = (*it)->getModel()->getPassphrase();
Mnemonic* wordSource = nullptr;
const Mnemonic* wordSource = Passphrase::detectMnemonic(passphrase);
auto passphrase_obj = Passphrase::create(passphrase, wordSource);
if (!passphrase_obj.isNull() && passphrase_obj->checkIfValid()) {
auto key_pair = KeyPairEd25519::create(passphrase_obj);

View File

@ -9,6 +9,11 @@ namespace model {
"product[affiliate_program_id]", "publisher[id]", "order_id", "product_id",
"product[price]", "payer[email]", "publisher[email]", "payment_state", "success_date", "event" };
ElopageBuy::ElopageBuy()
{
}
ElopageBuy::ElopageBuy(const Poco::Net::NameValueCollection& elopage_webhook_requestData)
: mPayed(false)
{
@ -103,6 +108,14 @@ namespace model {
return select;
}
// --------------------------- Tasks --------------------------------------------
int UserHasElopageTask::run()
{
auto elopage_buy = Poco::AutoPtr<model::table::ElopageBuy>(new model::table::ElopageBuy);
bool hasElopage = elopage_buy->isExistInDB("payer_email", mEmail);
return 0;
}
}
}

View File

@ -31,6 +31,7 @@ namespace model {
{
public:
ElopageBuy(const Poco::Net::NameValueCollection& elopage_webhook_requestData);
ElopageBuy();
// generic db operations
const char* getTableName() const { return "elopage_buys"; }
@ -51,6 +52,22 @@ namespace model {
Poco::DateTime mSuccessDate;
std::string mEvent;
};
// check for user existing
class UserHasElopageTask : public UniLib::controller::CPUTask
{
public:
UserHasElopageTask(std::string email) : mEmail(email), mHasElopage(false) {}
int run();
const char* getResourceType() const { return "UserHasElopageTask"; };
bool hasElopage() const { return mHasElopage; }
protected:
std::string mEmail;
bool mHasElopage;
};
}
}

View File

@ -171,7 +171,7 @@ namespace model {
<< " WHERE " << fieldName << " = ?"
, Poco::Data::Keywords::into(id), Poco::Data::Keywords::useRef(fieldValue);
try {
if (select.execute() == 1) {
if (select.execute() >= 1) {
return true;
}
}

View File

@ -68,5 +68,7 @@ namespace UniLib {
}
}
#define AWAIT(task) while (!hasElopageTask->isTaskFinished()) { Poco::Thread::sleep(10); }
#endif //__DR_UNIVERSUM_LIB_CONTROLLER_CPU_TASK_H__