Merge branch 'master' into 996-ReleasePlan_V1.6.0

This commit is contained in:
Ulf Gebhardt 2022-01-27 10:09:45 +01:00 committed by GitHub
commit 86bac9d415
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 692 additions and 6299 deletions

View File

@ -507,9 +507,17 @@ jobs:
# UNIT TESTS BACKEND #####################################################
##########################################################################
- name: backend | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
- name: Sleep for 30 seconds
run: sleep 30s
shell: bash
- name: backend | docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: Sleep for 30 seconds
run: sleep 30s
shell: bash
- name: backend Unit tests | test
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn CI_worklfow_test
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn CI_workflow_test
# run: docker-compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn test
##########################################################################
# COVERAGE CHECK BACKEND #################################################
@ -520,7 +528,7 @@ jobs:
report_name: Coverage Backend
type: lcov
result_path: ./backend/coverage/lcov.info
min_coverage: 45
min_coverage: 38
token: ${{ github.token }}
##############################################################################

View File

@ -15,7 +15,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
decayDuration: 0,
memo: 'Testing',
transactionId: 1,
name: 'Bibi',
name: 'Gradido Akademie',
email: 'bibi@bloxberg.de',
date: new Date(),
decay: {
@ -34,7 +34,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
decayDuration: 0,
memo: 'Testing 2',
transactionId: 2,
name: 'Bibi',
name: 'Gradido Akademie',
email: 'bibi@bloxberg.de',
date: new Date(),
decay: {
@ -53,6 +53,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
const toastedErrorMock = jest.fn()
const mocks = {
$d: jest.fn((t) => t),
$t: jest.fn((t) => t),
$apollo: {
query: apolloQueryMock,
@ -64,6 +65,7 @@ const mocks = {
const propsData = {
userId: 1,
fields: ['date', 'balance', 'name', 'memo', 'decay'],
}
describe('CreationTransactionListFormular', () => {

View File

@ -1,7 +1,7 @@
<template>
<div class="component-creation-transaction-list">
{{ $t('transactionlist.title') }}
<b-table striped hover :items="items"></b-table>
<b-table striped hover :fields="fields" :items="items"></b-table>
</div>
</template>
<script>
@ -13,6 +13,35 @@ export default {
},
data() {
return {
fields: [
{
key: 'date',
label: this.$t('transactionlist.date'),
formatter: (value, key, item) => {
return this.$d(new Date(value))
},
},
{
key: 'balance',
label: this.$t('transactionlist.amount'),
formatter: (value, key, item) => {
return `${value} GDD`
},
},
{ key: 'name', label: this.$t('transactionlist.community') },
{ key: 'memo', label: this.$t('transactionlist.memo') },
{
key: 'decay',
label: this.$t('transactionlist.decay'),
formatter: (value, key, item) => {
if (value && value.balance >= 0) {
return value.balance
} else {
return '0'
}
},
},
],
items: [],
}
},

View File

@ -12,6 +12,9 @@
"select_value": "Betrag auswählen",
"submit_creation": "Schöpfung einreichen",
"toasted": "Offene Schöpfung ({value} GDD) für {email} wurde gespeichert und liegt zur Bestätigung bereit",
"toasted_created": "Schöpfung wurde erfolgreich gespeichert",
"toasted_default": "`Fall {event} wird nicht unterstützt`",
"toasted_delete": "Offene Schöpfung wurde gelöscht",
"toasted_update": "`Offene Schöpfung {value} GDD) für {email} wurde geändert und liegt zur Bestätigung bereit",
"update_creation": "Schöpfung aktualisieren"
},
@ -52,6 +55,11 @@
"remove": "Entfernen",
"transaction": "Transaktion",
"transactionlist": {
"amount": "Betrag",
"community": "Gemeinschaft",
"date": "Datum",
"decay": "Vergänglichkeit",
"memo": "Nachricht",
"title": "Alle geschöpften Transaktionen für den Nutzer"
},
"unregistered_emails": "Nur unregistrierte Nutzer",

View File

@ -12,6 +12,9 @@
"select_value": "Select amount",
"submit_creation": "Submit creation",
"toasted": "Open creation ({value} GDD) for {email} has been saved and is ready for confirmation.",
"toasted_created": "Creation has been successfully saved",
"toasted_default": "`Case {event} is not supported`",
"toasted_delete": "Open creation has been deleted",
"toasted_update": "Open creation {value} GDD) for {email} has been changed and is ready for confirmation.",
"update_creation": "Creation update"
},
@ -52,6 +55,11 @@
"remove": "Remove",
"transaction": "Transaction",
"transactionlist": {
"amount": "Amount",
"community": "Community",
"date": "Date",
"decay": "Decay",
"memo": "Message",
"title": "All creation-transactions for the user"
},
"unregistered_emails": "Only unregistered users",

View File

@ -44,6 +44,9 @@ Vue.use(Toasted, {
addNavigationGuards(router, store, apolloProvider.defaultClient, i18n)
i18n.locale =
store.state.moderator && store.state.moderator.language ? store.state.moderator.language : 'en'
new Vue({
moment,
router,

View File

@ -15,7 +15,15 @@ jest.mock('vue-apollo')
jest.mock('vuex')
jest.mock('vue-i18n')
jest.mock('vue-moment')
jest.mock('./store/store')
jest.mock('./store/store', () => {
return {
state: {
moderator: {
language: 'es',
},
},
}
})
jest.mock('./i18n')
jest.mock('./router/router')
@ -101,4 +109,8 @@ describe('main', () => {
}),
)
})
it('sets the locale from store', () => {
expect(i18n.locale).toBe('es')
})
})

View File

@ -127,7 +127,7 @@ describe('CreationConfirm', () => {
})
it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('Pending Creation has been deleted')
expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_delete')
})
})
@ -189,7 +189,7 @@ describe('CreationConfirm', () => {
})
it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('Pending Creation has been deleted')
expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_created')
})
})
@ -201,7 +201,7 @@ describe('CreationConfirm', () => {
})
it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Case confirm is not supported')
expect(toastedErrorMock).toBeCalledWith('creation_form.toasted_default')
})
})

View File

@ -66,7 +66,7 @@ export default {
index = this.confirmResult.indexOf(findArr)
this.confirmResult.splice(index, 1)
this.$store.commit('openCreationsMinus', 1)
this.$toasted.success('Pending Creation has been deleted')
this.$toasted.success(this.$t('creation_form.toasted_delete'))
})
.catch((error) => {
this.$toasted.error(error.message)
@ -75,10 +75,10 @@ export default {
case 'confirmed':
this.confirmResult.splice(index, 1)
this.$store.commit('openCreationsMinus', 1)
this.$toasted.success('Pending Creation has been deleted')
this.$toasted.success(this.$t('creation_form.toasted_created'))
break
default:
this.$toasted.error('Case ' + event + ' is not supported')
this.$toasted.error(this.$t('creation_form.toasted_default', { event }))
}
},
getPendingCreations() {

View File

@ -2,7 +2,7 @@ PORT=4000
JWT_SECRET=$JWT_SECRET
JWT_EXPIRES_IN=10m
GRAPHIQL=false
GDT_API_URL=https://gdt.gradido.net
GDT_API_URL=$GDT_API_URL
DB_HOST=localhost
DB_PORT=3306
DB_USER=$DB_USER
@ -27,9 +27,9 @@ EMAIL_LINK_SETPASSWORD=$EMAIL_LINK_SETPASSWORD
#KLICKTIPP_APIKEY_DE=
#KLICKTIPP_APIKEY_EN=
#KLICKTIPP=true
COMMUNITY_NAME=
COMMUNITY_URL=
COMMUNITY_REGISTER_URL=
COMMUNITY_DESCRIPTION=
COMMUNITY_NAME=$COMMUNITY_NAME
COMMUNITY_URL=$COMMUNITY_URL
COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET

View File

@ -9,10 +9,13 @@ module.exports = async () => {
moduleNameMapper: {
'@entity/(.*)': '<rootDir>/../database/build/entity/$1',
// This is hack to fix a problem with the library `ts-mysql-migrate` which does differentiate between its ts/js state
'@dbTools/(.*)': '<rootDir>/../database/src/$1',
/*
'@dbTools/(.*)':
process.env.NODE_ENV === 'development'
? '<rootDir>/../database/src/$1'
: '<rootDir>/../database/build/src/$1',
*/
},
}
}

View File

@ -13,7 +13,7 @@
"start": "node build/index.js",
"dev": "nodemon -w src --ext ts --exec ts-node src/index.ts",
"lint": "eslint . --ext .js,.ts",
"CI_worklfow_test": "jest --runInBand --coverage ",
"CI_workflow_test": "jest --runInBand --coverage ",
"test": "NODE_ENV=development jest --runInBand --coverage "
},
"dependencies": {

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,16 +8,28 @@ import { RIGHTS } from '../../auth/RIGHTS'
import { ServerUserRepository } from '../../typeorm/repository/ServerUser'
import { getCustomRepository } from 'typeorm'
import { UserRepository } from '../../typeorm/repository/User'
import { INALIENABLE_RIGHTS } from '../../auth/INALIENABLE_RIGHTS'
const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
context.role = ROLE_UNAUTHORIZED // unauthorized user
// Do we have a token?
if (context.token) {
// Decode the token
const decoded = decode(context.token)
if (!decoded) {
// we always throw on an invalid token
throw new Error('403.13 - Client certificate revoked')
// Are all rights requested public?
const isInalienable = (<RIGHTS[]>rights).reduce(
(acc, right) => acc && INALIENABLE_RIGHTS.includes(right),
true,
)
if (isInalienable) {
// If public dont throw and permit access
return true
} else {
// Throw on a protected route
throw new Error('403.13 - Client certificate revoked')
}
}
// Set context pubKey
context.pubKey = Buffer.from(decoded.pubKey).toString('hex')

View File

@ -37,11 +37,11 @@ export class User {
@Field(() => String)
lastName: string
@Field(() => String)
username: string
@Field(() => String, { nullable: true })
username?: string
@Field(() => String)
description: string
@Field(() => String, { nullable: true })
description?: string
@Field(() => String)
pubkey: string

View File

@ -503,7 +503,7 @@ export class TransactionResolver {
email: userEntity.email,
})
if (!resultGDTSum.success) throw new Error(resultGDTSum.data)
transactions.gdtSum = Number(resultGDTSum.data.sum / 100) || 0
transactions.gdtSum = Number(resultGDTSum.data.sum) || 0
// get balance
const balanceRepository = getCustomRepository(BalanceRepository)
@ -584,6 +584,7 @@ export class TransactionResolver {
-centAmount,
queryRunner,
)
// Insert Transaction: recipient + amount
const recipiantUserTransactionBalance = await addUserTransaction(
recipiantUser,
@ -599,6 +600,7 @@ export class TransactionResolver {
transaction.received,
queryRunner,
)
// Update Balance: recipiant + amount
const recipiantStateBalance = await updateStateBalance(
recipiantUser,

View File

@ -49,7 +49,10 @@ const isLanguage = (language: string): boolean => {
}
const PHRASE_WORD_COUNT = 24
const WORDS = fs.readFileSync('src/config/mnemonic.english.txt').toString().split('\n')
const WORDS = fs
.readFileSync('src/config/mnemonic.uncompressed_buffer13116.txt')
.toString()
.split(',')
const PassphraseGenerate = (): string[] => {
const result = []
for (let i = 0; i < PHRASE_WORD_COUNT; i++) {
@ -418,7 +421,7 @@ export class UserResolver {
// Table: login_user_backups
const loginUserBackup = new LoginUserBackup()
loginUserBackup.userId = loginUserId
loginUserBackup.passphrase = passphrase.join(' ') + ' ' // login server saves trailing space
loginUserBackup.passphrase = passphrase.join(' ') // login server saves trailing space
loginUserBackup.mnemonicType = 2 // ServerConfig::MNEMONIC_BIP0039_SORTED_ORDER;
await queryRunner.manager.save(loginUserBackup).catch((error) => {
@ -585,15 +588,23 @@ export class UserResolver {
})
const loginUserBackupRepository = await getRepository(LoginUserBackup)
const loginUserBackup = await loginUserBackupRepository
.findOneOrFail({ userId: loginUser.id })
.catch(() => {
throw new Error('Could not find corresponding BackupUser')
})
let loginUserBackup = await loginUserBackupRepository.findOne({ userId: loginUser.id })
const passphrase = loginUserBackup.passphrase.slice(0, -1).split(' ')
// Generate Passphrase if needed
if (!loginUserBackup) {
const passphrase = PassphraseGenerate()
loginUserBackup = new LoginUserBackup()
loginUserBackup.userId = loginUser.id
loginUserBackup.passphrase = passphrase.join(' ') // login server saves trailing space
loginUserBackup.mnemonicType = 2 // ServerConfig::MNEMONIC_BIP0039_SORTED_ORDER;
loginUserBackupRepository.save(loginUserBackup)
}
const passphrase = loginUserBackup.passphrase.split(' ')
if (passphrase.length < PHRASE_WORD_COUNT) {
// TODO if this can happen we cannot recover from that
// this seem to be good on production data, if we dont
// make a coding mistake we do not have a problem here
throw new Error('Could not load a correct passphrase')
}

View File

@ -28,7 +28,7 @@ import { elopageWebhook } from '../webhook/elopage'
// TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
const DB_VERSION = '0006-login_users_collation'
const DB_VERSION = '0012-login_user_backups_unify_wordlist'
const createServer = async (context: any = serverContext): Promise<any> => {
// open mysql connection

View File

@ -37,8 +37,8 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
// eslint-disable-next-line no-console
console.log('Elopage Hook received', req.body)
res.status(200).end() // Responding is important
const loginElopgaeBuyRepository = await getCustomRepository(LoginElopageBuysRepository)
const loginElopgaeBuy = new LoginElopageBuys()
const loginElopageBuyRepository = await getCustomRepository(LoginElopageBuysRepository)
const loginElopageBuy = new LoginElopageBuys()
let firstName = ''
let lastName = ''
const entries = req.body.split('&')
@ -51,39 +51,39 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
const val = decodeURIComponent(keyVal[1]).replace('+', ' ').trim()
switch (key) {
case 'product[affiliate_program_id]':
loginElopgaeBuy.affiliateProgramId = parseInt(val)
loginElopageBuy.affiliateProgramId = parseInt(val)
break
case 'publisher[id]':
loginElopgaeBuy.publisherId = parseInt(val)
loginElopageBuy.publisherId = parseInt(val)
break
case 'order_id':
loginElopgaeBuy.orderId = parseInt(val)
loginElopageBuy.orderId = parseInt(val)
break
case 'product_id':
loginElopgaeBuy.productId = parseInt(val)
loginElopageBuy.productId = parseInt(val)
break
case 'product[price]':
// TODO: WHAT THE ACTUAL FUK? Please save this as float in the future directly in the database
loginElopgaeBuy.productPrice = Math.trunc(parseFloat(val) * 100)
loginElopageBuy.productPrice = Math.trunc(parseFloat(val) * 100)
break
case 'payer[email]':
loginElopgaeBuy.payerEmail = val
loginElopageBuy.payerEmail = val
break
case 'publisher[email]':
loginElopgaeBuy.publisherEmail = val
loginElopageBuy.publisherEmail = val
break
case 'payment_state':
loginElopgaeBuy.payed = val === 'paid'
loginElopageBuy.payed = val === 'paid'
break
case 'success_date':
loginElopgaeBuy.successDate = new Date(val)
loginElopageBuy.successDate = new Date(val)
break
case 'event':
loginElopgaeBuy.event = val
loginElopageBuy.event = val
break
case 'membership[id]':
// TODO this was never set on login_server - its unclear if this is the correct value
loginElopgaeBuy.elopageUserId = parseInt(val)
loginElopageBuy.elopageUserId = parseInt(val)
break
case 'payer[first_name]':
firstName = val
@ -100,14 +100,14 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
})
// Do not process certain events
if (['lesson.viewed', 'lesson.completed', 'lesson.commented'].includes(loginElopgaeBuy.event)) {
if (['lesson.viewed', 'lesson.completed', 'lesson.commented'].includes(loginElopageBuy.event)) {
// eslint-disable-next-line no-console
console.log('User viewed, completed or commented - not saving hook')
return
}
// Save the hook data
await loginElopgaeBuyRepository.save(loginElopgaeBuy)
await loginElopageBuyRepository.save(loginElopageBuy)
// create user for certain products
/*
@ -118,8 +118,8 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
Business-Mitgliedschaft, 43960
Förderbeitrag: 49106
*/
if ([36001, 43741, 43870, 43944, 43960, 49106].includes(loginElopgaeBuy.productId)) {
const email = loginElopgaeBuy.payerEmail
if ([36001, 43741, 43870, 43944, 43960, 49106].includes(loginElopageBuy.productId)) {
const email = loginElopageBuy.payerEmail
const VALIDATE_EMAIL = /^[a-zA-Z0-9.!#$%&?*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
const VALIDATE_NAME = /^<>&;]{2,}$/
@ -152,7 +152,7 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
email,
firstName,
lastName,
publisherId: loginElopgaeBuy.publisherId,
publisherId: loginElopageBuy.publisherId,
})
} catch (error) {
// eslint-disable-next-line no-console

View File

@ -98,6 +98,8 @@ COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
# COPY --from=build ${DOCKER_WORKDIR}/public ./public
# Copy package.json for script definitions (lock file should not be needed)
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
# Copy Mnemonic files
COPY --from=build ${DOCKER_WORKDIR}/src/config/*.txt ./src/config/
# Copy run scripts run/
# COPY --from=build ${DOCKER_WORKDIR}/run ./run
@ -112,7 +114,7 @@ CMD /bin/sh -c "yarn run up"
##################################################################################
# PRODUCTION RESET ###############################################################
##################################################################################
# FROM production as production_reset
FROM production as production_reset
# Run command
CMD /bin/sh -c "yarn run reset"

View File

@ -19,7 +19,7 @@ export class LoginUser extends BaseEntity {
@Column({ length: 255, default: '' })
username: string
@Column({ default: '' })
@Column({ default: '', nullable: true })
description: string
@Column({ type: 'bigint', default: 0, unsigned: true })

View File

@ -19,7 +19,7 @@ export class LoginUser extends BaseEntity {
@Column({ length: 255, default: '', collation: 'utf8mb4_unicode_ci' })
username: string
@Column({ default: '', collation: 'utf8mb4_unicode_ci' })
@Column({ default: '', collation: 'utf8mb4_unicode_ci', nullable: true })
description: string
@Column({ type: 'bigint', default: 0, unsigned: true })

View File

@ -0,0 +1,86 @@
/* MIGRATION TO CLEAN PRODUCTION DATA
*
* the way the passphrases are stored in login_user_backups is inconsistent.
* we need to try to detect which word list was used and transform it accordingly
*/
import fs from 'fs'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const sodium = require('sodium-native')
const PHRASE_WORD_COUNT = 24
const WORDS = fs
.readFileSync('src/config/mnemonic.uncompressed_buffer13116.txt')
.toString()
.split(',')
const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
if (!passphrase.length || passphrase.length < PHRASE_WORD_COUNT) {
throw new Error('passphrase empty or to short')
}
const state = Buffer.alloc(sodium.crypto_hash_sha512_STATEBYTES)
sodium.crypto_hash_sha512_init(state)
for (let i = 0; i < PHRASE_WORD_COUNT; i++) {
const value = Buffer.alloc(8)
const wordIndex = WORDS.indexOf(passphrase[i])
value.writeBigInt64LE(BigInt(wordIndex))
sodium.crypto_hash_sha512_update(state, value)
}
// trailing space is part of the login_server implementation
const clearPassphrase = passphrase.slice(0, PHRASE_WORD_COUNT).join(' ') + ' '
sodium.crypto_hash_sha512_update(state, Buffer.from(clearPassphrase))
const outputHashBuffer = Buffer.alloc(sodium.crypto_hash_sha512_BYTES)
sodium.crypto_hash_sha512_final(state, outputHashBuffer)
const pubKey = Buffer.alloc(sodium.crypto_sign_PUBLICKEYBYTES)
const privKey = Buffer.alloc(sodium.crypto_sign_SECRETKEYBYTES)
sodium.crypto_sign_seed_keypair(
pubKey,
privKey,
outputHashBuffer.slice(0, sodium.crypto_sign_SEEDBYTES),
)
return [pubKey, privKey]
}
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// Delete data with no reference in login_users table
// eslint-disable-next-line no-console
// 663 affected rows
const userBackups = await queryFn(
`SELECT passphrase, LOWER(HEX(pubkey)) as pubkey, user_id
FROM login_user_backups
LEFT JOIN login_users ON login_user_backups.user_id = login_users.id
WHERE user_id=1503`,
// WHERE pubkey is not null`, // todo fix this condition and regenerate
)
let i = 0
// eslint-disable-next-line no-console
userBackups.forEach(async (userBackup) => {
const passphrase = userBackup.passphrase.split(' ')
const keyPair = KeyPairEd25519Create(passphrase)
if (keyPair[0].toString('hex') !== userBackup.pubkey) {
i++
// eslint-disable-next-line no-console
console.log(
'Missmatch Pubkey',
i,
userBackup.user_id,
`"${userBackup.passphrase}"`,
`"${keyPair[0].toString('hex')}`,
`"${userBackup.pubkey}"`,
)
} else {
// eslint-disable-next-line no-console
// console.log('SUCCESS: ', `"${keyPair[0].toString('hex')}`, `"${userBackup.pubkey}"`)
}
})
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
return [] // cannot transform things back
}

View File

@ -0,0 +1,5 @@
This is a test to find if all passphrases evaluate to the saved public key.
You need `yarn add sodium-native` in order to make it work.
This could be the start of database integrity tests in oder to evaluate the correctness of the database

View File

@ -6,11 +6,13 @@
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// Explicitly change the charset and collate to the one used to then change it
await queryFn('ALTER TABLE `login_users` CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin;')
await queryFn(
'ALTER TABLE `login_users` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('ALTER TABLE `login_users` CONVERT TO CHARACTER SET utf8mb4;')
await queryFn('ALTER TABLE `login_users` CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin;')
}

View File

@ -0,0 +1,15 @@
/* MIGRATION TO CLEAN PRODUCTION DATA
*
* delete the pending tasks to not have any dead entries.
* the way we interact with the table is now differently
* and therefore we should clear it to avoid conflicts
* and dead entries
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('DELETE FROM `login_pending_tasks`;')
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
return [] // cannot undelete things
}

View File

@ -0,0 +1,23 @@
/* MIGRATION TO CLEAN PRODUCTION DATA
*
* some entries in the state_users table do not have an email.
* this is required tho to work with the new environment.
* to mitigate 38 out of 50 emails could be restored from
* login_users.
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// Fill in missing emails from login_users
await queryFn(
`UPDATE state_users
INNER JOIN login_users ON state_users.public_key = login_users.pubkey
SET state_users.email = login_users.email
WHERE state_users.email = '';`,
)
// Delete remaining ones
await queryFn(`DELETE FROM state_users WHERE email = ''`)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
return [] // cannot undelete things
}

View File

@ -0,0 +1,28 @@
/* MIGRATION TO CLEAN PRODUCTION DATA
*
* some entries in the login_users table are inconsistent.
* As solution the inconsistent data is purged and the corresponding
* account is set as not yet activated
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// Generate a random private key where the required data is present (pubkey + password + passphrase).
// Furthermore the email needs to be confirmed
await queryFn(
`UPDATE login_users SET privkey = UNHEX(SHA1(RAND()))
WHERE privkey IS NULL
AND pubkey IS NOT NULL
AND password != 0
AND email_checked = 1
AND id IN (SELECT user_id FROM login_user_backups);`,
)
// Remove incomplete data and set account as not activated yet.
await queryFn(
`UPDATE login_users SET password = 0, pubkey = NULL, email_checked = 0 WHERE privkey IS NULL;`,
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
return [] // cannot undelete things
}

View File

@ -0,0 +1,45 @@
/* MIGRATION TO CLEAN PRODUCTION DATA
*
* login_users and state_users are not in sync.
* Copy missing data from login_users to state_users.
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// Copy data with intact private key
await queryFn(
`INSERT INTO state_users
(public_key, email, first_name, last_name, username, disabled)
(SELECT pubkey as public_key, email, first_name, last_name, username, disabled
FROM login_users
WHERE email NOT IN (SELECT email from state_users)
AND privkey IS NOT NULL
)`,
)
// Copy data without intact private key, generate random pubkey
await queryFn(
`INSERT INTO state_users
(public_key, email, first_name, last_name, username, disabled)
(SELECT UNHEX(SHA1(RAND())) as public_key, email, first_name, last_name, username, disabled
FROM login_users
WHERE email NOT IN (SELECT email from state_users)
AND privkey IS NULL
)`,
)
// Remove duplicate data from state_users with dead pubkeys
// 18 entries
await queryFn(
`DELETE FROM state_users
WHERE id IN
(SELECT state_users.id FROM state_users
WHERE public_key NOT IN
(SELECT pubkey FROM login_users
WHERE pubkey IS NOT NULL)
AND email IN (SELECT email FROM state_users GROUP BY email HAVING COUNT(*) > 1
)
)`,
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
return [] // cannot undelete things
}

View File

@ -0,0 +1,18 @@
/* MIGRATION TO CLEAN PRODUCTION DATA
*
* cleanup the login_user_backups.
* Delete data with no reference in login_users table and
* delete the right one of the duplicate keys
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// Delete data with no reference in login_users table
await queryFn(`DELETE FROM login_user_backups WHERE user_id NOT IN (SELECT id FROM login_users)`)
// Delete duplicates which have changed for some reasons
await queryFn(`DELETE FROM login_user_backups WHERE id IN (21, 103, 313, 325, 726, 750, 1098)`)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
return [] // cannot undelete things
}

View File

@ -0,0 +1,66 @@
/* MIGRATION TO CLEAN PRODUCTION DATA
*
* the way the passphrases are stored in login_user_backups is inconsistent.
* we need to detect which word list was used and transform it accordingly.
* This also removes the trailing space
*/
import fs from 'fs'
const TARGET_MNEMONIC_TYPE = 2
const PHRASE_WORD_COUNT = 24
const WORDS_MNEMONIC_0 = fs
.readFileSync('src/config/mnemonic.uncompressed_buffer18112.txt')
.toString()
.split(',')
const WORDS_MNEMONIC_1 = fs
.readFileSync('src/config/mnemonic.uncompressed_buffer18113.txt')
.toString()
.split(',')
const WORDS_MNEMONIC_2 = fs
.readFileSync('src/config/mnemonic.uncompressed_buffer13116.txt')
.toString()
.split(',')
const WORDS_MNEMONIC = [WORDS_MNEMONIC_0, WORDS_MNEMONIC_1, WORDS_MNEMONIC_2]
const detectMnemonic = (passphrase: string[]): string[] => {
if (passphrase.length < PHRASE_WORD_COUNT) {
throw new Error(
`Passphrase is not long enough ${passphrase.length}/${PHRASE_WORD_COUNT}; passphrase: ${passphrase}`,
)
}
const passphraseSliced = passphrase.slice(0, PHRASE_WORD_COUNT)
// Loop through all word lists
for (let i = 0; i < WORDS_MNEMONIC.length; i++) {
// Does the wordlist contain all elements of the passphrase
if (passphraseSliced.every((word) => WORDS_MNEMONIC[i].includes(word))) {
if (i === TARGET_MNEMONIC_TYPE) {
return passphraseSliced
} else {
return passphraseSliced.map((word) => WORDS_MNEMONIC_2[WORDS_MNEMONIC[i].indexOf(word)])
}
}
}
throw new Error(`Could not find mnemonic type for passphrase: ${passphrase}`)
}
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// Loop through all user backups and update passphrase and mnemonic type if needed
const userBackups = await queryFn(`SELECT * FROM login_user_backups`)
userBackups.forEach(async (userBackup) => {
const passphrase = userBackup.passphrase.split(' ')
const newPassphrase = detectMnemonic(passphrase).join(' ')
if (newPassphrase !== userBackup.passphrase) {
await queryFn(
`UPDATE login_user_backups SET passphrase = ?, mnemonic_type = ? WHERE id = ?`,
[newPassphrase, TARGET_MNEMONIC_TYPE, userBackup.id],
)
}
})
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
return [] // cannot transform things back
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -30,6 +30,13 @@ TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log
WEBHOOK_ELOPAGE_SECRET=secret
GDT_API_URL=https://gdt.gradido.net
COMMUNITY_NAME=Gradido Development Stage1
COMMUNITY_URL=https://stage1.gradido.net/
COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register
COMMUNITY_DESCRIPTION=Gradido Development Stage1 Test Community
# frontend
GRAPHQL_URI=https://stage1.gradido.net/graphql
ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token}

View File

@ -22,7 +22,7 @@ fi
pm2 stop gradido-backend
# Backup data
mysqldump --databases --single-transaction --quick --lock-tables=false > ${SCRIPT_DIR}/backup/mariadb-backup-$(date +%d-%m-%Y_%H-%M-%S).sql -u ${DB_USER} -p${DB_PASSWORD} ${DB_DATABASE}
mysqldump --databases --single-transaction --quick --hex-blob --lock-tables=false > ${SCRIPT_DIR}/backup/mariadb-backup-$(date +%d-%m-%Y_%H-%M-%S).sql -u ${DB_USER} -p${DB_PASSWORD} ${DB_DATABASE}
# Start Services
pm2 start gradido-backend

View File

@ -0,0 +1,35 @@
#!/bin/bash
set -o allexport
SCRIPT_PATH=$(realpath $0)
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
PROJECT_ROOT=$SCRIPT_DIR/../..
set +o allexport
BACKUP_FILE_LOGIN=$1 #gradido_login_22-01-24.sql
BACKUP_FILE_COMMUNITY=$2 #gradido_node_22-01-24.sql
# Load backend .env for DB_USERNAME, DB_PASSWORD & DB_DATABASE
# NOTE: all config values will be in process.env when starting
# the services and will therefore take precedence over the .env
if [ -f "$PROJECT_ROOT/backend/.env" ]; then
export $(cat $PROJECT_ROOT/backend/.env | sed 's/#.*//g' | xargs)
else
export $(cat $PROJECT_ROOT/backend/.env.dist | sed 's/#.*//g' | xargs)
fi
# Delete whole database
sudo mysql -uroot -e "show databases" | grep -v Database | grep -v mysql| grep -v information_schema| gawk '{print "drop database `" $1 "`;select sleep(0.1);"}' | sudo mysql -uroot
BACKUP_LOGIN=$SCRIPT_DIR/backup/$BACKUP_FILE_LOGIN
BACKUP_COMMUNITY=$SCRIPT_DIR/backup/$BACKUP_FILE_COMMUNITY
# import backup login
mysql -u ${DB_USER} -p${DB_PASSWORD} <<EOFMYSQL
source $BACKUP_LOGIN
EOFMYSQL
# import backup community
mysql -u ${DB_USER} -p${DB_PASSWORD} <<EOFMYSQL
source $BACKUP_COMMUNITY
EOFMYSQL

View File

@ -50,12 +50,12 @@ sudo nano /etc/sudoers.d/gradido
sudo chmod a+rw /etc/nginx/sites-enabled
# Install node 16.x
sudo apt-get install -y curl
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt-get install -y nodejs
sudo apt-get install -y build-essential
# Install yarn
sudo apt-get install -y curl
sudo apt-get install -y gnupg
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
@ -75,9 +75,11 @@ sudo certbot
> Please read the Terms of Service at > Y
> Would you be willing, once your first certificate is successfully issued, to > N
> No names were found in your configuration files. Please enter in your domain > stage1.gradido.net
# Note: this will throw an error regarding not beeing able to identify the nginx corresponding
# config but produce the required certificate - thats perfectly fine this way
# Install logrotate
# sudo apt-get install -y logrotate
sudo apt-get install -y logrotate
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_DIR/logrotate/gradido.conf.template > $SCRIPT_DIR/logrotate/gradido.conf
sudo mv $SCRIPT_DIR/logrotate/gradido.conf /etc/logrotate.d/gradido.conf
sudo chown root:root /etc/logrotate.d/gradido.conf

View File

@ -28,7 +28,7 @@ fi
pm2 stop gradido-backend
# Backup data
mysqldump --databases --single-transaction --quick --lock-tables=false > ${SCRIPT_DIR}/backup/mariadb-restore-backup-$(date +%d-%m-%Y_%H-%M-%S).sql -u ${DB_USER} -p${DB_PASSWORD} ${DB_DATABASE}
mysqldump --databases --single-transaction --quick --hex-blob --lock-tables=false > ${SCRIPT_DIR}/backup/mariadb-restore-backup-$(date +%d-%m-%Y_%H-%M-%S).sql -u ${DB_USER} -p${DB_PASSWORD} ${DB_DATABASE}
# Restore Data
mysql -u ${DB_USER} -p${DB_PASSWORD} <<EOFMYSQL

View File

@ -73,9 +73,17 @@
> sudo /etc/init.d/fail2ban restart
# Install gradido
> sudo apt-get install git
> sudo apt-get install -y git
> cd ~
> git clone https://github.com/gradido/gradido.git
> cd gradido/deployment/bare_metal
# Timezone
# Note: This is not needed - UTC(default) is REQUIRED for production data
# > sudo timedatectl set-timezone UTC
# > sudo timedatectl set-ntp on
# > sudo apt purge ntp
# > sudo systemctl start systemd-timesyncd
# >> timedatectl to verify
# Adjust .env
# NOTE ';' can not be part of any value

View File

@ -20,24 +20,18 @@ services:
# DATABASE #############################################
########################################################
database:
restart: always # this is very dangerous, but worth a test for the delayed mariadb startup at first run
build:
context: ./database
target: test_up
# restart: always # this is very dangerous, but worth a test for the delayed mariadb startup at first run
#########################################################
## MARIADB ##############################################
#########################################################
mariadb:
build:
context: .
dockerfile: ./mariadb/Dockerfile
target: mariadb_server
environment:
- MARIADB_ALLOW_EMPTY_PASSWORD=1
- MARIADB_USER=root
networks:
- internal-net
- external-net
ports:
- 3306:3306
volumes:
- db_test_vol:/var/lib/mysql

View File

@ -108,7 +108,7 @@ services:
# DATABASE #############################################
########################################################
database:
image: gradido/database:production_up
#image: gradido/database:production_up
build:
context: ./database
target: production_up

View File

@ -24,8 +24,12 @@
<b-row class="mb-2">
<b-col>
<input-password
:rules="{ samePassword: value.password }"
:rules="{
required: true,
samePassword: value.password,
}"
:label="register ? $t('form.passwordRepeat') : $t('form.password_new_repeat')"
:immediate="true"
:name="createId(register ? $t('form.passwordRepeat') : $t('form.password_new_repeat'))"
:placeholder="register ? $t('form.passwordRepeat') : $t('form.password_new_repeat')"
v-model="passwordRepeat"

View File

@ -12,7 +12,6 @@
</div>
</template>
<script>
import { localeChanged } from 'vee-validate'
import locales from '../locales/'
import { updateUserInfos } from '../graphql/mutations'
@ -26,10 +25,8 @@ export default {
},
methods: {
setLocale(locale) {
this.$i18n.locale = locale
this.$store.commit('language', this.$i18n.locale)
this.$store.commit('language', locale)
this.currentLanguage = this.getLocaleObject(locale)
localeChanged(locale)
},
async saveLocale(locale) {
// if (this.$i18n.locale === locale) return

View File

@ -7,6 +7,7 @@ const propsData = {
balance: 1234,
visible: false,
elopageUri: 'https://elopage.com',
pending: false,
}
const mocks = {
@ -20,6 +21,7 @@ const mocks = {
isAdmin: true,
},
},
$n: jest.fn((n) => n),
}
describe('Navbar', () => {

View File

@ -10,7 +10,7 @@
</div>
<b-navbar-nav class="ml-auto" is-nav>
<b-nav-item>{{ balance }} GDD</b-nav-item>
<b-nav-item>{{ pending ? '—' : $n(balance, 'decimal') }} GDD</b-nav-item>
<b-nav-item to="/profile" right class="d-none d-sm-none d-md-none d-lg-flex shadow-lg">
<small>
{{ $store.state.firstName }} {{ $store.state.lastName }},
@ -87,6 +87,10 @@ export default {
type: String,
required: false,
},
pending: {
type: Boolean,
required: true,
},
},
data() {
return {

View File

@ -4,7 +4,7 @@
<p></p>
<div class="mb-6">
<b-nav vertical class="w-200">
<b-nav-item to="/overview" class="mb-3" active>
<b-nav-item to="/overview" class="mb-3">
<b-icon icon="house" aria-hidden="true"></b-icon>
{{ $t('overview') }}
</b-nav-item>
@ -52,3 +52,9 @@ export default {
},
}
</script>
<style>
#component-sidebar .active,
.component-navbar .active {
font-weight: bold;
}
</style>

View File

@ -1,5 +1,6 @@
{
"admin_area": "Adminbereich",
"advanced-calculation": "Vorausberechnung",
"back": "Zurück",
"community": {
"choose-another-community": "Eine andere Gemeinschaft auswählen",
@ -197,7 +198,7 @@
},
"thx": {
"activateEmail": "Dein Konto wurde noch nicht aktiviert. Bitte überprüfe deine E-Mail und klicke den Aktivierungslink!",
"checkEmail": "Deine E-Mail wurde erfolgreich verifiziert.",
"checkEmail": "Deine E-Mail wurde erfolgreich verifiziert. Du kannst dich jetzt anmelden.",
"email": "Wir haben dir eine E-Mail gesendet.",
"emailActivated": "Danke dass Du deine E-Mail bestätigt hast.",
"errorTitle": "Achtung!",

View File

@ -1,5 +1,6 @@
{
"admin_area": "Admin Area",
"advanced-calculation": "Advanced calculation",
"back": "Back",
"community": {
"choose-another-community": "Choose another community",
@ -197,7 +198,7 @@
},
"thx": {
"activateEmail": "Your account has not been activated yet, please check your emails and click the activation link!",
"checkEmail": "Your email has been successfully verified.",
"checkEmail": "Your email has been successfully verified. You can sign in now.",
"email": "We have sent you an email.",
"emailActivated": "Thank you your email has been activated.",
"errorTitle": "Attention!",

View File

@ -28,7 +28,7 @@ Vue.toasted.register(
loadAllRules(i18n)
addNavigationGuards(router, store, apolloProvider.defaultClient, i18n)
addNavigationGuards(router, store, apolloProvider.defaultClient)
if (!store) {
setTimeout(

View File

@ -1,6 +1,6 @@
import { verifyLogin } from '../graphql/queries'
const addNavigationGuards = (router, store, apollo, i18n) => {
const addNavigationGuards = (router, store, apollo) => {
// handle publisherId
router.beforeEach((to, from, next) => {
const publisherId = to.query.pid
@ -21,7 +21,6 @@ const addNavigationGuards = (router, store, apollo, i18n) => {
fetchPolicy: 'network-only',
})
.then((result) => {
i18n.locale = result.data.verifyLogin.language
store.dispatch('login', result.data.verifyLogin)
next({ path: '/overview' })
})

View File

@ -1,11 +1,15 @@
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import { localeChanged } from 'vee-validate'
import i18n from '../i18n.js'
Vue.use(Vuex)
export const mutations = {
language: (state, language) => {
i18n.locale = language
localeChanged(language)
state.language = language
},
email: (state, email) => {

View File

@ -1,8 +1,18 @@
import { mutations, actions } from './store'
import Vuex from 'vuex'
import Vue from 'vue'
import i18n from '../i18n.js'
import { localeChanged } from 'vee-validate'
jest.mock('vuex')
jest.mock('../i18n.js')
jest.mock('vee-validate', () => {
return {
localeChanged: jest.fn(),
}
})
i18n.locale = 'blubb'
const {
language,
@ -29,6 +39,14 @@ describe('Vuex store', () => {
language(state, 'de')
expect(state.language).toEqual('de')
})
it('sets the i18n locale', () => {
expect(i18n.locale).toBe('de')
})
it('calls localChanged of vee-validate', () => {
expect(localeChanged).toBeCalledWith('de')
})
})
describe('email', () => {

View File

@ -4,6 +4,7 @@
class="main-navbar"
:balance="balance"
:visible="visible"
:pending="pending"
:elopageUri="elopageUri"
@set-visible="setVisible"
@admin="admin"

View File

@ -214,21 +214,55 @@ describe('Register', () => {
})
*/
describe('API calls', () => {
describe('API calls when form is missing input', () => {
beforeEach(() => {
wrapper.find('#registerFirstname').setValue('Max')
wrapper.find('#registerLastname').setValue('Mustermann')
wrapper.find('.language-switch-select').findAll('option').at(1).setSelected()
wrapper.find('#publisherid').setValue('12345')
})
it('has disabled submit button when missing input checked box', () => {
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled')
})
it('has disabled submit button when missing email input', () => {
wrapper.find('#registerCheckbox').setChecked()
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled')
})
})
describe('API calls when completely filled and missing publisherid', () => {
beforeEach(() => {
wrapper.find('#registerFirstname').setValue('Max')
wrapper.find('#registerLastname').setValue('Mustermann')
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
wrapper.find('.language-switch-select').findAll('option').at(1).setSelected()
wrapper.find('#registerCheckbox').setChecked()
})
it('has enabled submit button when completely filled', async () => {
await wrapper.vm.$nextTick()
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe(undefined)
})
})
describe('API calls when completely filled', () => {
beforeEach(() => {
wrapper.find('#registerFirstname').setValue('Max')
wrapper.find('#registerLastname').setValue('Mustermann')
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
wrapper.find('.language-switch-select').findAll('option').at(1).setSelected()
wrapper.find('#publisherid').setValue('12345')
wrapper.find('#registerCheckbox').setChecked()
})
it('commits publisherId to store', () => {
expect(mockStoreCommit).toBeCalledWith('publisherId', 12345)
})
it('has enabled submit button when completely filled', () => {
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled')
it('has enabled submit button when completely filled', async () => {
await wrapper.vm.$nextTick()
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe(undefined)
})
describe('server sends back error', () => {

View File

@ -161,9 +161,9 @@
</b-button>
</router-link>
<b-button
:disabled="!(namesFilled && emailFilled && form.agree && !!language)"
:disabled="disabled"
type="submit"
variant="primary"
:variant="disabled ? 'outline-light' : 'primary'"
>
{{ $t('signup') }}
</b-button>
@ -191,7 +191,6 @@
import InputEmail from '../../components/Inputs/InputEmail.vue'
import LanguageSwitchSelect from '../../components/LanguageSwitchSelect.vue'
import { createUser } from '../../graphql/mutations'
import { localeChanged } from 'vee-validate'
import { getCommunityInfoMixin } from '../../mixins/getCommunityInfo'
export default {
@ -218,8 +217,6 @@ export default {
updateLanguage(e) {
this.language = e
this.$store.commit('language', this.language)
this.$i18n.locale = this.language
localeChanged(this.language)
},
getValidationState({ dirty, validated, valid = null }) {
return dirty || validated ? valid : null
@ -267,6 +264,9 @@ export default {
emailFilled() {
return this.form.email !== ''
},
disabled() {
return !(this.namesFilled && this.emailFilled && this.form.agree && !!this.language)
},
},
}
</script>

View File

@ -15,54 +15,48 @@ const stubs = {
RouterLink: RouterLinkStub,
}
const createMockObject = (comingFrom) => {
return {
localVue,
mocks: {
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$route: {
params: {
optin: '123',
comingFrom,
},
path: {
includes: (t) => t,
},
},
$toasted: {
global: {
error: toasterMock,
},
},
$router: {
push: routerPushMock,
},
$loading: {
show: jest.fn(() => {
return { hide: jest.fn() }
}),
},
$apollo: {
mutate: apolloMutationMock,
},
const mocks = {
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$route: {
params: {
optin: '123',
},
stubs,
}
path: {
mock: 'checkEmail',
includes: jest.fn((t) => t === mocks.$route.path.mock),
},
},
$toasted: {
global: {
error: toasterMock,
},
},
$router: {
push: routerPushMock,
},
$loading: {
show: jest.fn(() => {
return { hide: jest.fn() }
}),
},
$apollo: {
mutate: apolloMutationMock,
},
}
describe('ResetPassword', () => {
let wrapper
const Wrapper = (functionName) => {
return mount(ResetPassword, functionName)
const Wrapper = () => {
return mount(ResetPassword, { localVue, mocks, stubs })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper(createMockObject())
wrapper = Wrapper()
})
describe('No valid optin', () => {
@ -86,11 +80,32 @@ describe('ResetPassword', () => {
})
describe('Register header', () => {
it('has a welcome message', async () => {
expect(wrapper.find('div.header').text()).toContain('settings.password.reset')
expect(wrapper.find('div.header').text()).toContain(
'settings.password.reset-password.text',
)
describe('from reset', () => {
beforeEach(() => {
mocks.$route.path.mock = 'reset'
wrapper = Wrapper()
})
it('has a welcome message', async () => {
expect(wrapper.find('div.header').text()).toContain('settings.password.reset')
expect(wrapper.find('div.header').text()).toContain(
'settings.password.reset-password.text',
)
})
})
describe('from checkEmail', () => {
beforeEach(() => {
mocks.$route.path.mock = 'checkEmail'
wrapper = Wrapper()
})
it('has a welcome message', async () => {
expect(wrapper.find('div.header').text()).toContain('settings.password.set')
expect(wrapper.find('div.header').text()).toContain(
'settings.password.set-password.text',
)
})
})
})
@ -128,7 +143,6 @@ describe('ResetPassword', () => {
describe('submit form', () => {
beforeEach(async () => {
// wrapper = Wrapper(createMockObject())
await wrapper.findAll('input').at(0).setValue('Aa123456_')
await wrapper.findAll('input').at(1).setValue('Aa123456_')
await flushPromises()
@ -164,14 +178,14 @@ describe('ResetPassword', () => {
})
})
describe('server response with success', () => {
describe('server response with success on /checkEmail', () => {
beforeEach(async () => {
mocks.$route.path.mock = 'checkEmail'
apolloMutationMock.mockResolvedValue({
data: {
resetPassword: 'success',
},
})
wrapper = Wrapper(createMockObject('checkEmail'))
await wrapper.findAll('input').at(0).setValue('Aa123456_')
await wrapper.findAll('input').at(1).setValue('Aa123456_')
await wrapper.find('form').trigger('submit')
@ -189,6 +203,26 @@ describe('ResetPassword', () => {
)
})
it('redirects to "/thx/checkEmail"', () => {
expect(routerPushMock).toHaveBeenCalledWith('/thx/checkEmail')
})
})
describe('server response with success on /reset', () => {
beforeEach(async () => {
mocks.$route.path.mock = 'reset'
wrapper = Wrapper()
apolloMutationMock.mockResolvedValue({
data: {
resetPassword: 'success',
},
})
await wrapper.findAll('input').at(0).setValue('Aa123456_')
await wrapper.findAll('input').at(1).setValue('Aa123456_')
await wrapper.find('form').trigger('submit')
await flushPromises()
})
it('redirects to "/thx/reset"', () => {
expect(routerPushMock).toHaveBeenCalledWith('/thx/reset')
})

View File

@ -92,7 +92,11 @@ export default {
})
.then(() => {
this.form.password = ''
this.$router.push('/thx/reset')
if (this.$route.path.includes('checkEmail')) {
this.$router.push('/thx/checkEmail')
} else {
this.$router.push('/thx/reset')
}
})
.catch((error) => {
this.$toasted.global.error(error.message)

View File

@ -34,6 +34,7 @@
</b-row>
<b-container class="bv-example-row mt-3 gray-background p-2">
<p>{{ $t('advanced-calculation') }}</p>
<b-row class="pr-3">
<b-col class="text-right">{{ $t('form.current_balance') }}</b-col>
<b-col class="text-right">{{ $n(balance, 'decimal') }}</b-col>
@ -42,18 +43,13 @@
<b-col class="text-right">
<strong>{{ $t('form.your_amount') }}</strong>
</b-col>
<b-col class="text-right">
<b-col class="text-right borderbottom">
<strong>- {{ $n(amount, 'decimal') }}</strong>
</b-col>
</b-row>
<b-row class="pr-3">
<b-col class="text-right">{{ $t('decay.decay') }}</b-col>
<b-col class="text-right" style="border-bottom: double">- {{ $n(decay, 'decimal') }}</b-col>
</b-row>
<b-row class="pr-3">
<b-col class="text-right">{{ $t('form.new_balance') }}</b-col>
<b-col class="text-right">~ {{ $n(balance - amount - decay, 'decimal') }}</b-col>
<b-col class="text-right">~ {{ $n(balance - amount, 'decimal') }}</b-col>
</b-row>
</b-container>
@ -95,5 +91,6 @@ export default {
}
.borderbottom {
border-bottom: 1px solid rgb(70, 65, 65);
border-bottom-style: double;
}
</style>

View File

@ -138,10 +138,6 @@ describe('UserCard_Language', () => {
expect(storeCommitMock).toBeCalledWith('language', 'en')
})
it('changes the i18n locale', () => {
expect(mocks.$i18n.locale).toBe('en')
})
it('has no select field anymore', () => {
expect(wrapper.find('select').exists()).toBeFalsy()
})

View File

@ -60,7 +60,6 @@
</b-card>
</template>
<script>
import { localeChanged } from 'vee-validate'
import LanguageSwitchSelect from '../../../components/LanguageSwitchSelect.vue'
import { updateUserInfos } from '../../../graphql/mutations'
@ -97,8 +96,6 @@ export default {
})
.then(() => {
this.$store.commit('language', this.language)
this.$i18n.locale = this.language
localeChanged(this.language)
this.cancelEdit()
this.$toasted.success(this.$t('settings.language.success'))
})