Merge master in here.

This commit is contained in:
elweyn 2023-01-13 11:24:11 +01:00
commit df4e2b419a
45 changed files with 365 additions and 224 deletions

View File

@ -437,7 +437,7 @@ jobs:
report_name: Coverage Frontend
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 89
min_coverage: 91
token: ${{ github.token }}
##############################################################################
@ -527,7 +527,7 @@ jobs:
report_name: Coverage Backend
type: lcov
result_path: ./backend/coverage/lcov.info
min_coverage: 76
min_coverage: 78
token: ${{ github.token }}
##########################################################################

View File

@ -3,6 +3,10 @@
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"hediet.vscode-drawio"
"hediet.vscode-drawio",
"streetsidesoftware.code-spell-checker-german",
"mtxr.sqltools",
"mtxr.sqltools-driver-mysql",
"jcbuisson.vue"
]
}

17
.vscode/settings.json vendored
View File

@ -1,3 +1,18 @@
{
"git.ignoreLimitWarning": true
"git.ignoreLimitWarning": true,
"sqltools.connections": [
{
"mysqlOptions": {
"authProtocol": "default"
},
"previewLimit": 50,
"server": "localhost",
"port": 3306,
"driver": "MariaDB",
"name": "localhost",
"database": "gradido_community",
"username": "root",
"password": ""
}
],
}

View File

@ -4141,9 +4141,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001271:
version "1.0.30001354"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001354.tgz"
integrity sha512-mImKeCkyGDAHNywYFA4bqnLAzTUvVkqPvhY4DV47X+Gl2c5Z8c3KNETnXp14GQt11LvxE8AwjzGxJ+rsikiOzg==
version "1.0.30001442"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz"
integrity sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow==
capture-exit@^2.0.0:
version "2.0.0"

View File

@ -31,7 +31,6 @@
"express": "^4.17.1",
"graphql": "^15.5.1",
"i18n": "^0.15.1",
"jest": "^27.2.4",
"jsonwebtoken": "^8.5.1",
"lodash.clonedeep": "^4.5.0",
"log4js": "^6.4.6",

View File

@ -35,6 +35,7 @@ export enum RIGHTS {
SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS',
CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE',
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
OPEN_CREATIONS = 'OPEN_CREATIONS',
// Admin
SEARCH_USERS = 'SEARCH_USERS',
SET_USER_ROLE = 'SET_USER_ROLE',

View File

@ -33,6 +33,7 @@ export const ROLE_USER = new Role('user', [
RIGHTS.COMMUNITY_STATISTICS,
RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
RIGHTS.OPEN_CREATIONS,
])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights

View File

@ -70,11 +70,13 @@ const email = {
EMAIL: process.env.EMAIL === 'true' || false,
EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || false,
EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'stage1@gradido.net',
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
EMAIL_USERNAME: process.env.EMAIL_USERNAME || '',
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx',
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'gmail.com',
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587',
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || '',
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'mailserver',
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '1025',
// eslint-disable-next-line no-unneeded-ternary
EMAIL_TLS: process.env.EMAIL_TLS === 'false' ? false : true,
EMAIL_LINK_VERIFICATION:
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}',
EMAIL_LINK_SETPASSWORD:

View File

@ -41,7 +41,7 @@ export const sendEmailTranslated = async (params: {
host: CONFIG.EMAIL_SMTP_URL,
port: Number(CONFIG.EMAIL_SMTP_PORT),
secure: false, // true for 465, false for other ports
requireTLS: true,
requireTLS: CONFIG.EMAIL_TLS,
auth: {
user: CONFIG.EMAIL_USERNAME,
pass: CONFIG.EMAIL_PASSWORD,

View File

@ -0,0 +1,14 @@
import { ObjectType, Field, Int } from 'type-graphql'
import Decimal from 'decimal.js-light'
@ObjectType()
export class OpenCreation {
@Field(() => Int)
month: number
@Field(() => Int)
year: number
@Field(() => Decimal)
amount: Decimal
}

View File

@ -1,13 +1,11 @@
import { ObjectType, Field } from 'type-graphql'
import { KlickTipp } from './KlickTipp'
import { User as dbUser } from '@entity/User'
import Decimal from 'decimal.js-light'
import { FULL_CREATION_AVAILABLE } from '../resolver/const/const'
import { UserContact } from './UserContact'
@ObjectType()
export class User {
constructor(user: dbUser, creation: Decimal[] = FULL_CREATION_AVAILABLE) {
constructor(user: dbUser) {
this.id = user.id
this.gradidoID = user.gradidoID
this.alias = user.alias
@ -26,7 +24,6 @@ export class User {
this.isAdmin = user.isAdmin
this.klickTipp = null
this.hasElopage = null
this.creation = creation
this.hideAmountGDD = user.hideAmountGDD
this.hideAmountGDT = user.hideAmountGDT
}
@ -34,9 +31,6 @@ export class User {
@Field(() => Number)
id: number
// `public_key` binary(32) DEFAULT NULL,
// `privkey` binary(80) DEFAULT NULL,
@Field(() => String)
gradidoID: string
@ -62,9 +56,6 @@ export class User {
@Field(() => Date, { nullable: true })
deletedAt: Date | null
// `password` bigint(20) unsigned DEFAULT 0,
// `email_hash` binary(32) DEFAULT NULL,
@Field(() => Date)
createdAt: Date
@ -84,8 +75,6 @@ export class User {
@Field(() => Number, { nullable: true })
publisherId: number | null
// `passphrase` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
@Field(() => Date, { nullable: true })
isAdmin: Date | null
@ -94,7 +83,4 @@ export class User {
@Field(() => Boolean, { nullable: true })
hasElopage: boolean | null
@Field(() => [Decimal])
creation: Decimal[]
}

View File

@ -32,7 +32,7 @@ export class BalanceResolver {
const lastTransaction = context.lastTransaction
? context.lastTransaction
: await dbTransaction.findOne({ userId: user.id }, { order: { balanceDate: 'DESC' } })
: await dbTransaction.findOne({ userId: user.id }, { order: { id: 'DESC' } })
logger.debug(`lastTransaction=${lastTransaction}`)

View File

@ -1947,6 +1947,23 @@ describe('ContributionResolver', () => {
}),
)
})
describe('confirm same contribution again', () => {
it('throws an error', async () => {
await expect(
mutate({
mutation: confirmContribution,
variables: {
id: creation ? creation.id : -1,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Contribution already confirmd.')],
}),
)
})
})
})
describe('confirm two creations one after the other quickly', () => {

View File

@ -11,8 +11,9 @@ import { Transaction as DbTransaction } from '@entity/Transaction'
import { AdminCreateContributions } from '@model/AdminCreateContributions'
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
import { Contribution, ContributionListResult } from '@model/Contribution'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import { Decay } from '@model/Decay'
import { OpenCreation } from '@model/OpenCreation'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import { TransactionTypeId } from '@enum/TransactionTypeId'
import { Order } from '@enum/Order'
import { ContributionType } from '@enum/ContributionType'
@ -27,6 +28,7 @@ import { RIGHTS } from '@/auth/RIGHTS'
import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
import { backendLogger as logger } from '@/server/logger'
import {
getCreationDates,
getUserCreation,
getUserCreations,
validateContribution,
@ -589,78 +591,113 @@ export class ContributionResolver {
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
const receivedCallDate = new Date()
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
try {
const lastTransaction = await queryRunner.manager
.createQueryBuilder()
.select('transaction')
.from(DbTransaction, 'transaction')
.where('transaction.userId = :id', { id: contribution.userId })
.orderBy('transaction.id', 'DESC')
.getOne()
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
let newBalance = new Decimal(0)
let decay: Decay | null = null
if (lastTransaction) {
decay = calculateDecay(
lastTransaction.balance,
lastTransaction.balanceDate,
receivedCallDate,
)
newBalance = decay.balance
const clientTimezoneOffset = getClientTimezoneOffset(context)
const contribution = await DbContribution.findOne(id)
if (!contribution) {
logger.error(`Contribution not found for given id: ${id}`)
throw new Error('Contribution not found to given id.')
}
newBalance = newBalance.add(contribution.amount.toString())
if (contribution.confirmedAt) {
logger.error(`Contribution already confirmd: ${id}`)
throw new Error('Contribution already confirmd.')
}
const moderatorUser = getUser(context)
if (moderatorUser.id === contribution.userId) {
logger.error('Moderator can not confirm own contribution')
throw new Error('Moderator can not confirm own contribution')
}
const user = await DbUser.findOneOrFail(
{ id: contribution.userId },
{ withDeleted: true, relations: ['emailContact'] },
)
if (user.deletedAt) {
logger.error('This user was deleted. Cannot confirm a contribution.')
throw new Error('This user was deleted. Cannot confirm a contribution.')
}
const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false)
validateContribution(
creations,
contribution.amount,
contribution.contributionDate,
clientTimezoneOffset,
)
const transaction = new DbTransaction()
transaction.typeId = TransactionTypeId.CREATION
transaction.memo = contribution.memo
transaction.userId = contribution.userId
transaction.previous = lastTransaction ? lastTransaction.id : null
transaction.amount = contribution.amount
transaction.creationDate = contribution.contributionDate
transaction.balance = newBalance
transaction.balanceDate = receivedCallDate
transaction.decay = decay ? decay.decay : new Decimal(0)
transaction.decayStart = decay ? decay.start : null
await queryRunner.manager.insert(DbTransaction, transaction)
const receivedCallDate = new Date()
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
try {
const lastTransaction = await queryRunner.manager
.createQueryBuilder()
.select('transaction')
.from(DbTransaction, 'transaction')
.where('transaction.userId = :id', { id: contribution.userId })
.orderBy('transaction.id', 'DESC')
.getOne()
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
contribution.confirmedAt = receivedCallDate
contribution.confirmedBy = moderatorUser.id
contribution.transactionId = transaction.id
contribution.contributionStatus = ContributionStatus.CONFIRMED
await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
let newBalance = new Decimal(0)
let decay: Decay | null = null
if (lastTransaction) {
decay = calculateDecay(
lastTransaction.balance,
lastTransaction.balanceDate,
receivedCallDate,
)
newBalance = decay.balance
}
newBalance = newBalance.add(contribution.amount.toString())
await queryRunner.commitTransaction()
logger.info('creation commited successfuly.')
sendContributionConfirmedEmail({
firstName: user.firstName,
lastName: user.lastName,
email: user.emailContact.email,
language: user.language,
senderFirstName: moderatorUser.firstName,
senderLastName: moderatorUser.lastName,
contributionMemo: contribution.memo,
contributionAmount: contribution.amount,
})
} catch (e) {
await queryRunner.rollbackTransaction()
logger.error('Creation was not successful', e)
throw new Error('Creation was not successful.')
const transaction = new DbTransaction()
transaction.typeId = TransactionTypeId.CREATION
transaction.memo = contribution.memo
transaction.userId = contribution.userId
transaction.previous = lastTransaction ? lastTransaction.id : null
transaction.amount = contribution.amount
transaction.creationDate = contribution.contributionDate
transaction.balance = newBalance
transaction.balanceDate = receivedCallDate
transaction.decay = decay ? decay.decay : new Decimal(0)
transaction.decayStart = decay ? decay.start : null
await queryRunner.manager.insert(DbTransaction, transaction)
contribution.confirmedAt = receivedCallDate
contribution.confirmedBy = moderatorUser.id
contribution.transactionId = transaction.id
contribution.contributionStatus = ContributionStatus.CONFIRMED
await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
await queryRunner.commitTransaction()
logger.info('creation commited successfuly.')
sendContributionConfirmedEmail({
firstName: user.firstName,
lastName: user.lastName,
email: user.emailContact.email,
language: user.language,
senderFirstName: moderatorUser.firstName,
senderLastName: moderatorUser.lastName,
contributionMemo: contribution.memo,
contributionAmount: contribution.amount,
})
} catch (e) {
await queryRunner.rollbackTransaction()
logger.error('Creation was not successful', e)
throw new Error('Creation was not successful.')
} finally {
await queryRunner.release()
}
const event = new Event()
const eventContributionConfirm = new EventContributionConfirm()
eventContributionConfirm.userId = user.id
eventContributionConfirm.amount = contribution.amount
eventContributionConfirm.contributionId = contribution.id
await eventProtocol.writeEvent(event.setEventContributionConfirm(eventContributionConfirm))
} finally {
await queryRunner.release()
releaseLock()
}
const event = new Event()
const eventContributionConfirm = new EventContributionConfirm()
eventContributionConfirm.userId = user.id
eventContributionConfirm.amount = contribution.amount
eventContributionConfirm.contributionId = contribution.id
await eventProtocol.writeEvent(event.setEventContributionConfirm(eventContributionConfirm))
return true
}
@ -691,6 +728,25 @@ export class ContributionResolver {
// return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
}
@Authorized([RIGHTS.OPEN_CREATIONS])
@Query(() => [OpenCreation])
async openCreations(
@Arg('userId', () => Int, { nullable: true }) userId: number | null,
@Ctx() context: Context,
): Promise<OpenCreation[]> {
const id = userId || getUser(context).id
const clientTimezoneOffset = getClientTimezoneOffset(context)
const creationDates = getCreationDates(clientTimezoneOffset)
const creations = await getUserCreation(id, clientTimezoneOffset)
return creationDates.map((date, index) => {
return {
month: date.getMonth(),
year: date.getFullYear(),
amount: creations[index],
}
})
}
@Authorized([RIGHTS.REJECT_CONTRIBUTION])
@Mutation(() => Boolean)
async rejectContribution(

View File

@ -211,7 +211,7 @@ export class TransactionResolver {
// find current balance
const lastTransaction = await dbTransaction.findOne(
{ userId: user.id },
{ order: { balanceDate: 'DESC' }, relations: ['contribution'] },
{ order: { id: 'DESC' }, relations: ['contribution'] },
)
logger.debug(`lastTransaction=${lastTransaction}`)

View File

@ -58,7 +58,7 @@ import {
EventSendConfirmationEmail,
EventActivateAccount,
} from '@/event/Event'
import { getUserCreation, getUserCreations } from './util/creations'
import { getUserCreations } from './util/creations'
import { isValidPassword } from '@/password/EncryptorUtils'
import { FULL_CREATION_AVAILABLE } from './const/const'
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
@ -114,9 +114,8 @@ export class UserResolver {
async verifyLogin(@Ctx() context: Context): Promise<User> {
logger.info('verifyLogin...')
// TODO refactor and do not have duplicate code with login(see below)
const clientTimezoneOffset = getClientTimezoneOffset(context)
const userEntity = getUser(context)
const user = new User(userEntity, await getUserCreation(userEntity.id, clientTimezoneOffset))
const user = new User(userEntity)
// Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage(context)
@ -132,7 +131,6 @@ export class UserResolver {
@Ctx() context: Context,
): Promise<User> {
logger.info(`login with ${email}, ***, ${publisherId} ...`)
const clientTimezoneOffset = getClientTimezoneOffset(context)
email = email.trim().toLowerCase()
const dbUser = await findUserByEmail(email)
if (dbUser.deletedAt) {
@ -163,7 +161,7 @@ export class UserResolver {
logger.addContext('user', dbUser.id)
logger.debug('validation of login credentials successful...')
const user = new User(dbUser, await getUserCreation(dbUser.id, clientTimezoneOffset))
const user = new User(dbUser)
logger.debug(`user= ${JSON.stringify(user, null, 2)}`)
i18n.setLocale(user.language)

View File

@ -101,15 +101,19 @@ export const getUserCreation = async (
}
const getCreationMonths = (timezoneOffset: number): number[] => {
return getCreationDates(timezoneOffset).map((date) => date.getMonth() + 1)
}
export const getCreationDates = (timezoneOffset: number): Date[] => {
const clientNow = new Date()
clientNow.setTime(clientNow.getTime() - timezoneOffset * 60 * 1000)
logger.info(
`getCreationMonths -- offset: ${timezoneOffset} -- clientNow: ${clientNow.toISOString()}`,
)
return [
new Date(clientNow.getFullYear(), clientNow.getMonth() - 2, 1).getMonth() + 1,
new Date(clientNow.getFullYear(), clientNow.getMonth() - 1, 1).getMonth() + 1,
clientNow.getMonth() + 1,
new Date(clientNow.getFullYear(), clientNow.getMonth() - 2, 1),
new Date(clientNow.getFullYear(), clientNow.getMonth() - 1, 1),
clientNow,
]
}

View File

@ -1913,9 +1913,9 @@ camelcase@^6.2.0:
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
caniuse-lite@^1.0.30001264:
version "1.0.30001418"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz"
integrity sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==
version "1.0.30001442"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz"
integrity sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow==
chacha20-universal@^1.0.4:
version "1.0.4"

View File

@ -112,6 +112,17 @@ services:
volumes:
- /sessions
########################################################
# MAILSERVER TO FAKE SMTP ##############################
########################################################
mailserver:
image: maildev/maildev
ports:
- 1080:1080
- 1025:1025
networks:
- external-net
volumes:
frontend_node_modules:
admin_node_modules:

View File

@ -81,6 +81,17 @@ services:
nginx:
image: gradido/nginx:test
########################################################
# MAILSERVER TO FAKE SMTP ##############################
########################################################
mailserver:
image: maildev/maildev
ports:
- 1080:1080
- 1025:1025
networks:
- external-net
networks:
external-net:
internal-net:

View File

@ -37,7 +37,7 @@
</b-row>
<b-row class="mt-5 p-5">
<b-col>
<b-button @click="$emit('on-reset')">{{ $t('back') }}</b-button>
<b-button @click="$emit('on-back')">{{ $t('back') }}</b-button>
</b-col>
<b-col class="text-right">
<b-button

View File

@ -41,7 +41,7 @@
</b-row>
<b-row class="mt-5 p-5">
<b-col>
<b-button @click="$emit('on-reset')">{{ $t('back') }}</b-button>
<b-button @click="$emit('on-back')">{{ $t('back') }}</b-button>
</b-col>
<b-col class="text-right">
<b-button

View File

@ -322,7 +322,7 @@ Die ganze Welt bezwingen.“`)
[
{
email: 'someone@watches.tv',
amount: '87.23',
amount: 87.23,
memo: 'Long enough',
selected: 'send',
},

View File

@ -144,7 +144,7 @@ export default {
this.$emit('set-transaction', {
selected: this.radioSelected,
email: this.form.email,
amount: this.form.amount,
amount: Number(this.form.amount.replace(',', '.')),
memo: this.form.memo,
})
},

View File

@ -10,7 +10,7 @@
<div class="text-center">
<div><figure-qr-code :link="link" /></div>
<div>
<b-button variant="secondary" @click="$emit('on-reset')" class="mt-4" data-test="close-btn">
<b-button variant="secondary" @click="$emit('on-back')" class="mt-4" data-test="close-btn">
{{ $t('form.close') }}
</b-button>
</div>

View File

@ -6,7 +6,7 @@
{{ $t('form.send_transaction_success') }}
</div>
<div class="text-center mt-5">
<b-button variant="primary" @click="$emit('on-reset')">{{ $t('form.close') }}</b-button>
<b-button variant="primary" @click="$emit('on-back')">{{ $t('form.close') }}</b-button>
</div>
</div>
</template>

View File

@ -40,11 +40,11 @@ export default {
}
},
},
name: { type: String, default: 'Email' },
label: { type: String, default: 'Email' },
placeholder: { type: String, default: 'Email' },
value: { required: true, type: String, default: '' },
disabled: { required: false, type: Boolean, default: false },
name: { type: String, required: true },
label: { type: String, required: true },
placeholder: { type: String, required: true },
value: { type: String, required: true },
disabled: { type: Boolean, required: false, default: false },
},
data() {
return {

View File

@ -12,8 +12,8 @@
<span class="navbar-toggler-icon"></span>
</b-button>
</b-navbar-brand>
<b-img class="sheet-img position-absolute zindex1" :src="sheet"></b-img>
<router-link to="/settings" class="d-block d-lg-none zindex1000">
<router-link to="/settings" class="d-block d-lg-none">
<div class="d-flex align-items-center">
<div class="mr-3">
<avatar
@ -25,6 +25,7 @@
</div>
</div>
</router-link>
<b-img class="sheet-img position-absolute zindex-1" :src="sheet"></b-img>
<b-collapse id="nav-collapse" is-nav class="ml-5">
<b-navbar-nav class="ml-auto" right>
<div class="mb-2">

View File

@ -24,7 +24,7 @@
:size="72"
:color="'#fff'"
:username="`${transaction.linkedUser.firstName} ${transaction.linkedUser.lastName}`"
:initials="`${transaction.linkedUser.firstName[0]} ${transaction.linkedUser.lastName[0]}`"
:initials="`${transaction.linkedUser.firstName[0]}${transaction.linkedUser.lastName[0]}`"
></avatar>
</div>
</b-col>

View File

@ -154,7 +154,6 @@ export const login = gql`
hasElopage
publisherId
isAdmin
creation
hideAmountGDD
hideAmountGDT
}

View File

@ -256,3 +256,13 @@ export const listContributionMessages = gql`
}
}
`
export const openCreations = gql`
query {
openCreations {
year
month
amount
}
}
`

View File

@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
import Community from './Community'
import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
import { listContributions, listAllContributions, verifyLogin } from '@/graphql/queries'
import { listContributions, listAllContributions } from '@/graphql/queries'
import VueRouter from 'vue-router'
import routes from '../routes/routes'
@ -13,6 +13,7 @@ localVue.use(VueRouter)
const mockStoreDispach = jest.fn()
const apolloQueryMock = jest.fn()
const apolloMutationMock = jest.fn()
const apolloRefetchMock = jest.fn()
const router = new VueRouter({
base: '/',
@ -39,6 +40,11 @@ describe('Community', () => {
$apollo: {
query: apolloQueryMock,
mutate: apolloMutationMock,
queries: {
OpenCreations: {
refetch: apolloRefetchMock,
},
},
},
$store: {
dispatch: mockStoreDispach,
@ -207,10 +213,7 @@ describe('Community', () => {
})
it('verifies the login (to get the new creations available)', () => {
expect(apolloQueryMock).toBeCalledWith({
query: verifyLogin,
fetchPolicy: 'network-only',
})
expect(apolloRefetchMock).toBeCalled()
})
it('set all data to the default values)', () => {
@ -294,10 +297,7 @@ describe('Community', () => {
})
it('verifies the login (to get the new creations available)', () => {
expect(apolloQueryMock).toBeCalledWith({
query: verifyLogin,
fetchPolicy: 'network-only',
})
expect(apolloRefetchMock).toBeCalled()
})
it('set all data to the default values)', () => {
@ -376,10 +376,7 @@ describe('Community', () => {
})
it('verifies the login (to get the new creations available)', () => {
expect(apolloQueryMock).toBeCalledWith({
query: verifyLogin,
fetchPolicy: 'network-only',
})
expect(apolloRefetchMock).toBeCalled()
})
})

View File

@ -5,8 +5,8 @@
<b-tab no-body>
<open-creations-amount
:minimalDate="minimalDate"
:maxGddThisMonth="maxGddThisMonth"
:maxGddLastMonth="maxGddLastMonth"
:maxGddLastMonth="maxForMonths[0]"
:maxGddThisMonth="maxForMonths[1]"
/>
<div class="mb-3"></div>
<contribution-form
@ -15,8 +15,8 @@
v-model="form"
:isThisMonth="isThisMonth"
:minimalDate="minimalDate"
:maxGddLastMonth="maxGddLastMonth"
:maxGddThisMonth="maxGddThisMonth"
:maxGddLastMonth="maxForMonths[0]"
:maxGddThisMonth="maxForMonths[1]"
/>
</b-tab>
<b-tab no-body>
@ -52,7 +52,7 @@ import OpenCreationsAmount from '@/components/Contributions/OpenCreationsAmount.
import ContributionForm from '@/components/Contributions/ContributionForm.vue'
import ContributionList from '@/components/Contributions/ContributionList.vue'
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
import { listContributions, listAllContributions, verifyLogin } from '@/graphql/queries'
import { listContributions, listAllContributions, openCreations } from '@/graphql/queries'
export default {
name: 'Community',
@ -82,6 +82,7 @@ export default {
},
updateAmount: '',
maximalDate: new Date(),
openCreations: [],
}
},
mounted() {
@ -90,6 +91,23 @@ export default {
this.hashLink = this.$route.hash
})
},
apollo: {
OpenCreations: {
query() {
return openCreations
},
fetchPolicy: 'network-only',
variables() {
return {}
},
update({ openCreations }) {
this.openCreations = openCreations
},
error({ message }) {
this.toastError(message)
},
},
},
watch: {
$route(to, from) {
this.tabIndex = this.tabLinkHashes.findIndex((hashLink) => hashLink === to.hash)
@ -120,17 +138,20 @@ export default {
formDate.getMonth() === this.maximalDate.getMonth()
)
},
maxGddLastMonth() {
amountToAdd() {
// when existing contribution is edited, the amount is added back on top of the amount
return this.form.id && !this.isThisMonth
? parseInt(this.$store.state.creation[1]) + parseInt(this.updateAmount)
: parseInt(this.$store.state.creation[1])
if (this.form.id) return parseInt(this.updateAmount)
return 0
},
maxGddThisMonth() {
// when existing contribution is edited, the amount is added back on top of the amount
return this.form.id && this.isThisMonth
? parseInt(this.$store.state.creation[2]) + parseInt(this.updateAmount)
: parseInt(this.$store.state.creation[2])
maxForMonths() {
const formDate = new Date(this.form.date)
if (this.openCreations && this.openCreations.length)
return this.openCreations.slice(1).map((creation) => {
if (creation.year === formDate.getFullYear() && creation.month === formDate.getMonth())
return parseInt(creation.amount) + this.amountToAdd
return parseInt(creation.amount)
})
return [0, 0]
},
},
methods: {
@ -160,7 +181,7 @@ export default {
currentPage: this.currentPage,
pageSize: this.pageSize,
})
this.verifyLogin()
this.$apollo.queries.OpenCreations.refetch()
})
.catch((err) => {
this.toastError(err.message)
@ -188,7 +209,7 @@ export default {
currentPage: this.currentPage,
pageSize: this.pageSize,
})
this.verifyLogin()
this.$apollo.queries.OpenCreations.refetch()
})
.catch((err) => {
this.toastError(err.message)
@ -213,7 +234,7 @@ export default {
currentPage: this.currentPage,
pageSize: this.pageSize,
})
this.verifyLogin()
this.$apollo.queries.OpenCreations.refetch()
})
.catch((err) => {
this.toastError(err.message)
@ -268,22 +289,6 @@ export default {
this.toastError(err.message)
})
},
verifyLogin() {
this.$apollo
.query({
query: verifyLogin,
fetchPolicy: 'network-only',
})
.then((result) => {
const {
data: { verifyLogin },
} = result
this.$store.dispatch('login', verifyLogin)
})
.catch(() => {
this.$emit('logout')
})
},
updateContributionForm(item) {
this.form.id = item.id
this.form.date = item.contributionDate
@ -303,8 +308,6 @@ export default {
},
created() {
// verifyLogin is important at this point so that creation is updated on reload if they are deleted in a session in the admin area.
this.verifyLogin()
this.updateListContributions({
currentPage: this.currentPage,
pageSize: this.pageSize,

View File

@ -58,11 +58,11 @@ describe('ForgotPassword', () => {
})
it('has the label "Email"', () => {
expect(form.find('label').text()).toEqual('Email')
expect(form.find('label').text()).toEqual('form.email')
})
it('has the placeholder "Email"', () => {
expect(form.find('input').attributes('placeholder')).toEqual('Email')
expect(form.find('input').attributes('placeholder')).toEqual('form.email')
})
it('has a submit button', () => {

View File

@ -6,7 +6,12 @@
<b-col>
<validation-observer ref="observer" v-slot="{ handleSubmit }">
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
<input-email v-model="form.email"></input-email>
<input-email
v-model="form.email"
:name="$t('form.email')"
:label="$t('form.email')"
:placeholder="$t('form.email')"
></input-email>
<div class="text-center">
<b-button type="submit" variant="gradido">
{{ $t('settings.password.send_now') }}

View File

@ -76,7 +76,7 @@ describe('Login', () => {
})
it('has an Email input field', () => {
expect(wrapper.find('input[placeholder="Email"]').exists()).toBe(true)
expect(wrapper.find('div[data-test="input-email"]').find('input').exists()).toBe(true)
})
it('has an Password input field', () => {
@ -110,7 +110,10 @@ describe('Login', () => {
describe('valid data', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
await wrapper
.find('div[data-test="input-email"]')
.find('input')
.setValue('user@example.org')
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
await flushPromises()
apolloMutateMock.mockResolvedValue({
@ -159,7 +162,10 @@ describe('Login', () => {
code: 'some-code',
}
wrapper = Wrapper()
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
await wrapper
.find('div[data-test="input-email"]')
.find('input')
.setValue('user@example.org')
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
await flushPromises()
await wrapper.find('form').trigger('submit')
@ -180,7 +186,10 @@ describe('Login', () => {
})
wrapper = Wrapper()
jest.clearAllMocks()
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
await wrapper
.find('div[data-test="input-email"]')
.find('input')
.setValue('user@example.org')
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
await flushPromises()
await wrapper.find('form').trigger('submit')

View File

@ -5,7 +5,14 @@
<validation-observer ref="observer" v-slot="{ handleSubmit }">
<b-form @submit.stop.prevent="handleSubmit(onSubmit)">
<b-row>
<b-col sm="12" md="12" lg="6"><input-email v-model="form.email"></input-email></b-col>
<b-col sm="12" md="12" lg="6">
<input-email
v-model="form.email"
:name="$t('form.email')"
:label="$t('form.email')"
:placeholder="$t('form.email')"
></input-email>
</b-col>
<b-col sm="12" md="12" lg="6">
<input-password
:label="$t('form.password')"

View File

@ -65,7 +65,7 @@ describe('Register', () => {
})
it('has email input fields', () => {
expect(wrapper.find('#Email-input-field').exists()).toBe(true)
expect(wrapper.find('div[data-test="input-email"]').find('input').exists()).toBe(true)
})
it('has 1 checkbox input fields', () => {
@ -107,7 +107,10 @@ describe('Register', () => {
wrapper.find('#registerLastname').setValue('Mustermann')
})
it('has disabled submit button when missing input checked box', () => {
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
wrapper
.find('div[data-test="input-email"]')
.find('input')
.setValue('max.mustermann@gradido.net')
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled')
})
@ -121,7 +124,10 @@ describe('Register', () => {
beforeEach(() => {
wrapper.find('#registerFirstname').setValue('Max')
wrapper.find('#registerLastname').setValue('Mustermann')
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
wrapper
.find('div[data-test="input-email"]')
.find('input')
.setValue('max.mustermann@gradido.net')
wrapper.find('#registerCheckbox').setChecked()
})
@ -211,7 +217,10 @@ describe('Register', () => {
wrapper = Wrapper()
wrapper.find('#registerFirstname').setValue('Max')
wrapper.find('#registerLastname').setValue('Mustermann')
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
wrapper
.find('div[data-test="input-email"]')
.find('input')
.setValue('max.mustermann@gradido.net')
wrapper.find('#registerCheckbox').setChecked()
await wrapper.find('form').trigger('submit')
await flushPromises()

View File

@ -59,7 +59,14 @@
</b-col>
</b-row>
<b-row>
<b-col><input-email v-model="form.email"></input-email></b-col>
<b-col>
<input-email
v-model="form.email"
:name="$t('form.email')"
:label="$t('form.email')"
:placeholder="$t('form.email')"
></input-email>
</b-col>
</b-row>
<div class="my-4">
<b-form-checkbox

View File

@ -13,7 +13,7 @@ const navigatorClipboardMock = jest.fn()
const localVue = global.localVue
describe.skip('Send', () => {
describe('Send', () => {
let wrapper
const propsData = {

View File

@ -15,7 +15,7 @@
:amount="transactionData.amount"
:memo="transactionData.memo"
@send-transaction="sendTransaction"
@on-reset="onReset"
@on-back="onBack"
></transaction-confirmation-send>
</template>
<template #transactionConfirmationLink>
@ -26,17 +26,17 @@
:memo="transactionData.memo"
:loading="loading"
@send-transaction="sendTransaction"
@on-reset="onReset"
@on-back="onBack"
></transaction-confirmation-link>
</template>
<template #transactionResultSendSuccess>
<transaction-result-send-success @on-reset="onReset"></transaction-result-send-success>
<transaction-result-send-success @on-back="onBack"></transaction-result-send-success>
</template>
<template #transactionResultSendError>
<transaction-result-send-error
:error="error"
:errorResult="errorResult"
@on-reset="onReset"
@on-back="onBack"
></transaction-result-send-error>
</template>
<template #transactionResultLink>
@ -45,7 +45,7 @@
:amount="amount"
:memo="memo"
:validUntil="validUntil"
@on-reset="onReset"
@on-back="onBack"
></transaction-result-link>
</template>
</gdd-send>
@ -169,8 +169,9 @@ export default {
}
this.loading = false
},
onReset() {
onBack() {
this.currentTransactionStep = TRANSACTION_STEPS.transactionForm
this.$mount()
},
updateTransactions(pagination) {
this.$emit('update-transactions', pagination)

View File

@ -85,7 +85,7 @@ export default {
}
},
watch: {
gdt() {
currentPage() {
if (this.gdt) {
this.updateGdt()
}

View File

@ -47,9 +47,6 @@ export const mutations = {
hasElopage: (state, hasElopage) => {
state.hasElopage = hasElopage
},
creation: (state, creation) => {
state.creation = creation
},
hideAmountGDD: (state, hideAmountGDD) => {
state.hideAmountGDD = !!hideAmountGDD
},
@ -69,7 +66,6 @@ export const actions = {
commit('hasElopage', data.hasElopage)
commit('publisherId', data.publisherId)
commit('isAdmin', data.isAdmin)
commit('creation', data.creation)
commit('hideAmountGDD', data.hideAmountGDD)
commit('hideAmountGDT', data.hideAmountGDT)
},
@ -83,7 +79,6 @@ export const actions = {
commit('hasElopage', false)
commit('publisherId', null)
commit('isAdmin', false)
commit('creation', null)
commit('hideAmountGDD', false)
commit('hideAmountGDT', true)
localStorage.clear()
@ -111,7 +106,6 @@ try {
newsletterState: null,
hasElopage: false,
publisherId: null,
creation: null,
hideAmountGDD: null,
hideAmountGDT: null,
},

View File

@ -30,7 +30,6 @@ const {
publisherId,
isAdmin,
hasElopage,
creation,
hideAmountGDD,
hideAmountGDT,
} = mutations
@ -143,14 +142,6 @@ describe('Vuex store', () => {
})
})
describe('creation', () => {
it('sets the state of creation', () => {
const state = { creation: null }
creation(state, true)
expect(state.creation).toEqual(true)
})
})
describe('hideAmountGDD', () => {
it('sets the state of hideAmountGDD', () => {
const state = { hideAmountGDD: false }
@ -183,14 +174,13 @@ describe('Vuex store', () => {
hasElopage: false,
publisherId: 1234,
isAdmin: true,
creation: ['1000', '1000', '1000'],
hideAmountGDD: false,
hideAmountGDT: true,
}
it('calls eleven commits', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenCalledTimes(11)
expect(commit).toHaveBeenCalledTimes(10)
})
it('commits email', () => {
@ -233,19 +223,14 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', true)
})
it('commits creation', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(9, 'creation', ['1000', '1000', '1000'])
})
it('commits hideAmountGDD', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDD', false)
expect(commit).toHaveBeenNthCalledWith(9, 'hideAmountGDD', false)
})
it('commits hideAmountGDT', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(11, 'hideAmountGDT', true)
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDT', true)
})
})
@ -255,7 +240,7 @@ describe('Vuex store', () => {
it('calls eleven commits', () => {
logout({ commit, state })
expect(commit).toHaveBeenCalledTimes(11)
expect(commit).toHaveBeenCalledTimes(10)
})
it('commits token', () => {
@ -298,19 +283,14 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', false)
})
it('commits creation', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(9, 'creation', null)
})
it('commits hideAmountGDD', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDD', false)
expect(commit).toHaveBeenNthCalledWith(9, 'hideAmountGDD', false)
})
it('commits hideAmountGDT', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(11, 'hideAmountGDT', true)
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDT', true)
})
// how to get this working?
it.skip('calls localStorage.clear()', () => {

View File

@ -4578,9 +4578,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001181, caniuse-lite@^1.0.30001280, caniuse-lite@^1.0.30001286:
version "1.0.30001439"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz"
integrity sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A==
version "1.0.30001442"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz"
integrity sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow==
capture-exit@^2.0.0:
version "2.0.0"