diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3c486cb20..ac60cfdf2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 }} ########################################################################## diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 9b0d3b79e..43770e9b0 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -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" ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 3b6641073..9634ae8a9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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": "" + } + ], } \ No newline at end of file diff --git a/admin/yarn.lock b/admin/yarn.lock index 7507f2559..5d8275b1a 100644 --- a/admin/yarn.lock +++ b/admin/yarn.lock @@ -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" diff --git a/backend/package.json b/backend/package.json index 69a436563..1fb27f05f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 3fcd921c3..146eeb5b8 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -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', diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index eabaf8e99..2f3b4e081 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -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 diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index fd578af93..d010b4ab0 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -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: diff --git a/backend/src/emails/sendEmailTranslated.ts b/backend/src/emails/sendEmailTranslated.ts index 69008c00e..5652b3424 100644 --- a/backend/src/emails/sendEmailTranslated.ts +++ b/backend/src/emails/sendEmailTranslated.ts @@ -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, diff --git a/backend/src/graphql/model/OpenCreation.ts b/backend/src/graphql/model/OpenCreation.ts new file mode 100644 index 000000000..9ef08fd4a --- /dev/null +++ b/backend/src/graphql/model/OpenCreation.ts @@ -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 +} diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 6e353f6a7..b4fdcae4f 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -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[] } diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index a0016e8f2..26f9cd656 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -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}`) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 3dfd09bb5..9a7fb76f2 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -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', () => { diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 8c9959208..99db32155 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -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 { + 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( diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 0ac5b382e..33914583e 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -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}`) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index ed1bf3f47..c630c240a 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -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 { 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 { 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) diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index 54286d2aa..00137eaa1 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -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, ] } diff --git a/backend/yarn.lock b/backend/yarn.lock index 82bcd6b1f..264cbd409 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -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" diff --git a/docker-compose.override.yml b/docker-compose.override.yml index fe2f68a8d..c69c68b50 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -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: diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 790bd468d..84647ef03 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -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: diff --git a/frontend/src/components/GddSend/TransactionConfirmationLink.vue b/frontend/src/components/GddSend/TransactionConfirmationLink.vue index 0fcb94459..91fea3486 100644 --- a/frontend/src/components/GddSend/TransactionConfirmationLink.vue +++ b/frontend/src/components/GddSend/TransactionConfirmationLink.vue @@ -37,7 +37,7 @@ - {{ $t('back') }} + {{ $t('back') }} - {{ $t('back') }} + {{ $t('back') }}
- + {{ $t('form.close') }}
diff --git a/frontend/src/components/GddSend/TransactionResultSendSuccess.vue b/frontend/src/components/GddSend/TransactionResultSendSuccess.vue index 36ce3f4e6..b57196db4 100644 --- a/frontend/src/components/GddSend/TransactionResultSendSuccess.vue +++ b/frontend/src/components/GddSend/TransactionResultSendSuccess.vue @@ -6,7 +6,7 @@ {{ $t('form.send_transaction_success') }}
- {{ $t('form.close') }} + {{ $t('form.close') }}
diff --git a/frontend/src/components/Inputs/InputEmail.vue b/frontend/src/components/Inputs/InputEmail.vue index 1532e1edd..3a7ed4f4e 100644 --- a/frontend/src/components/Inputs/InputEmail.vue +++ b/frontend/src/components/Inputs/InputEmail.vue @@ -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 { diff --git a/frontend/src/components/Menu/Navbar.vue b/frontend/src/components/Menu/Navbar.vue index caebbed07..309e3ffd5 100644 --- a/frontend/src/components/Menu/Navbar.vue +++ b/frontend/src/components/Menu/Navbar.vue @@ -12,8 +12,8 @@
- - + +
+
diff --git a/frontend/src/components/Template/RightSide/LastTransactions.vue b/frontend/src/components/Template/RightSide/LastTransactions.vue index 1cfb02061..83b4bae7e 100644 --- a/frontend/src/components/Template/RightSide/LastTransactions.vue +++ b/frontend/src/components/Template/RightSide/LastTransactions.vue @@ -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]}`" >
diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index 95d7b0c39..111eb7ab5 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -154,7 +154,6 @@ export const login = gql` hasElopage publisherId isAdmin - creation hideAmountGDD hideAmountGDT } diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index a35e01141..50ee00327 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -256,3 +256,13 @@ export const listContributionMessages = gql` } } ` + +export const openCreations = gql` + query { + openCreations { + year + month + amount + } + } +` diff --git a/frontend/src/pages/Community.spec.js b/frontend/src/pages/Community.spec.js index 62ed46cbb..7297876df 100644 --- a/frontend/src/pages/Community.spec.js +++ b/frontend/src/pages/Community.spec.js @@ -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() }) }) diff --git a/frontend/src/pages/Community.vue b/frontend/src/pages/Community.vue index 16af01ce3..1332ca9d7 100644 --- a/frontend/src/pages/Community.vue +++ b/frontend/src/pages/Community.vue @@ -5,8 +5,8 @@
@@ -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, diff --git a/frontend/src/pages/ForgotPassword.spec.js b/frontend/src/pages/ForgotPassword.spec.js index af7931793..f4b74e555 100644 --- a/frontend/src/pages/ForgotPassword.spec.js +++ b/frontend/src/pages/ForgotPassword.spec.js @@ -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', () => { diff --git a/frontend/src/pages/ForgotPassword.vue b/frontend/src/pages/ForgotPassword.vue index 095920046..77c2ac926 100644 --- a/frontend/src/pages/ForgotPassword.vue +++ b/frontend/src/pages/ForgotPassword.vue @@ -6,7 +6,12 @@ - +
{{ $t('settings.password.send_now') }} diff --git a/frontend/src/pages/Login.spec.js b/frontend/src/pages/Login.spec.js index 90e98cd44..14bf77aa6 100644 --- a/frontend/src/pages/Login.spec.js +++ b/frontend/src/pages/Login.spec.js @@ -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') diff --git a/frontend/src/pages/Login.vue b/frontend/src/pages/Login.vue index 6d2dff5fa..bd07af3ef 100755 --- a/frontend/src/pages/Login.vue +++ b/frontend/src/pages/Login.vue @@ -5,7 +5,14 @@ - + + + { }) 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() diff --git a/frontend/src/pages/Register.vue b/frontend/src/pages/Register.vue index 9ecb196db..eb68ee7c5 100755 --- a/frontend/src/pages/Register.vue +++ b/frontend/src/pages/Register.vue @@ -59,7 +59,14 @@ - + + +
{ +describe('Send', () => { let wrapper const propsData = { diff --git a/frontend/src/pages/Send.vue b/frontend/src/pages/Send.vue index 7131a7247..370943ad6 100644 --- a/frontend/src/pages/Send.vue +++ b/frontend/src/pages/Send.vue @@ -15,7 +15,7 @@ :amount="transactionData.amount" :memo="transactionData.memo" @send-transaction="sendTransaction" - @on-reset="onReset" + @on-back="onBack" > @@ -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) diff --git a/frontend/src/pages/Transactions.vue b/frontend/src/pages/Transactions.vue index 4be91b92b..40d883886 100644 --- a/frontend/src/pages/Transactions.vue +++ b/frontend/src/pages/Transactions.vue @@ -85,7 +85,7 @@ export default { } }, watch: { - gdt() { + currentPage() { if (this.gdt) { this.updateGdt() } diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 84fd82fd5..1cd874c06 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -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, }, diff --git a/frontend/src/store/store.test.js b/frontend/src/store/store.test.js index 5f40d7fa2..33fedd562 100644 --- a/frontend/src/store/store.test.js +++ b/frontend/src/store/store.test.js @@ -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()', () => { diff --git a/frontend/yarn.lock b/frontend/yarn.lock index ff3a7ff1a..f374ac7bd 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -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"