mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 996-ReleasePlan_V1.6.0
This commit is contained in:
commit
86bac9d415
14
.github/workflows/test.yml
vendored
14
.github/workflows/test.yml
vendored
@ -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 }}
|
||||
|
||||
##############################################################################
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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: [],
|
||||
}
|
||||
},
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
@ -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',
|
||||
*/
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
1
backend/src/config/mnemonic.uncompressed_buffer13116.txt
Normal file
1
backend/src/config/mnemonic.uncompressed_buffer13116.txt
Normal file
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
@ -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')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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')
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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 })
|
||||
|
||||
86
database/integrity/0013-test.ts.keep
Normal file
86
database/integrity/0013-test.ts.keep
Normal 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
|
||||
}
|
||||
5
database/integrity/README.md
Normal file
5
database/integrity/README.md
Normal 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
|
||||
@ -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;')
|
||||
}
|
||||
|
||||
15
database/migrations/0007-login_pending_tasks_delete.ts
Normal file
15
database/migrations/0007-login_pending_tasks_delete.ts
Normal 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
|
||||
}
|
||||
23
database/migrations/0008-state_users_plug_holes.ts
Normal file
23
database/migrations/0008-state_users_plug_holes.ts
Normal 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
|
||||
}
|
||||
28
database/migrations/0009-login_users_plug_holes.ts
Normal file
28
database/migrations/0009-login_users_plug_holes.ts
Normal 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
|
||||
}
|
||||
45
database/migrations/0010-login_users_state_users_sync.ts
Normal file
45
database/migrations/0010-login_users_state_users_sync.ts
Normal 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
|
||||
}
|
||||
18
database/migrations/0011-login_user_backups_plug_holes.ts
Normal file
18
database/migrations/0011-login_user_backups_plug_holes.ts
Normal 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
|
||||
}
|
||||
@ -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
@ -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}
|
||||
|
||||
@ -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
|
||||
35
deployment/bare_metal/import_old_production.sh
Executable file
35
deployment/bare_metal/import_old_production.sh
Executable 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -108,7 +108,7 @@ services:
|
||||
# DATABASE #############################################
|
||||
########################################################
|
||||
database:
|
||||
image: gradido/database:production_up
|
||||
#image: gradido/database:production_up
|
||||
build:
|
||||
context: ./database
|
||||
target: production_up
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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!",
|
||||
|
||||
@ -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!",
|
||||
|
||||
@ -28,7 +28,7 @@ Vue.toasted.register(
|
||||
|
||||
loadAllRules(i18n)
|
||||
|
||||
addNavigationGuards(router, store, apolloProvider.defaultClient, i18n)
|
||||
addNavigationGuards(router, store, apolloProvider.defaultClient)
|
||||
|
||||
if (!store) {
|
||||
setTimeout(
|
||||
|
||||
@ -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' })
|
||||
})
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
class="main-navbar"
|
||||
:balance="balance"
|
||||
:visible="visible"
|
||||
:pending="pending"
|
||||
:elopageUri="elopageUri"
|
||||
@set-visible="setVisible"
|
||||
@admin="admin"
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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')
|
||||
})
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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()
|
||||
})
|
||||
|
||||
@ -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'))
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user