diff --git a/admin/src/components/ContributionLink.vue b/admin/src/components/ContributionLink.vue
index 893e202f4..5621e4330 100644
--- a/admin/src/components/ContributionLink.vue
+++ b/admin/src/components/ContributionLink.vue
@@ -15,7 +15,10 @@
{{ $t('contributionLink.contributionLinks') }}
-
+
@@ -24,6 +27,7 @@
v-if="count > 0"
:items="items"
@editContributionLinkData="editContributionLinkData"
+ @get-contribution-links="$emit('get-contribution-links')"
/>
{{ $t('contributionLink.noContributionLinks') }}
diff --git a/admin/src/components/ContributionLinkForm.vue b/admin/src/components/ContributionLinkForm.vue
index a159d33d3..6daf1e299 100644
--- a/admin/src/components/ContributionLinkForm.vue
+++ b/admin/src/components/ContributionLinkForm.vue
@@ -163,7 +163,6 @@ export default {
if (this.form.validFrom === null)
return this.toastError(this.$t('contributionLink.noStartDate'))
if (this.form.validTo === null) return this.toastError(this.$t('contributionLink.noEndDate'))
- // alert(JSON.stringify(this.form))
this.$apollo
.mutate({
mutation: createContributionLink,
@@ -182,6 +181,8 @@ export default {
this.link = result.data.createContributionLink.link
this.toastSuccess(this.link)
this.onReset()
+ this.$root.$emit('bv::toggle::collapse', 'newContribution')
+ this.$emit('get-contribution-links')
})
.catch((error) => {
this.toastError(error.message)
diff --git a/admin/src/components/ContributionLinkList.spec.js b/admin/src/components/ContributionLinkList.spec.js
index 0b9d131bd..b2baf5e9b 100644
--- a/admin/src/components/ContributionLinkList.spec.js
+++ b/admin/src/components/ContributionLinkList.spec.js
@@ -95,7 +95,7 @@ describe('ContributionLinkList', () => {
})
it('toasts a success message', () => {
- expect(toastSuccessSpy).toBeCalledWith('TODO: request message deleted ')
+ expect(toastSuccessSpy).toBeCalledWith('contributionLink.deleted')
})
})
diff --git a/admin/src/components/ContributionLinkList.vue b/admin/src/components/ContributionLinkList.vue
index 518d7d57e..48b7ce978 100644
--- a/admin/src/components/ContributionLinkList.vue
+++ b/admin/src/components/ContributionLinkList.vue
@@ -1,12 +1,12 @@
-
+
@@ -34,7 +34,7 @@
{{ modalData ? modalData.name : '' }}
- {{ modalData }}
+ {{ modalData.memo ? modalData.memo : '' }}
@@ -70,28 +70,30 @@ export default {
'edit',
'show',
],
- modalData: null,
- modalDataLink: null,
+ modalData: {},
}
},
methods: {
- deleteContributionLink() {
- this.$bvModal.msgBoxConfirm(this.$t('contributionLink.deleteNow')).then(async (value) => {
- if (value)
- await this.$apollo
- .mutate({
- mutation: deleteContributionLink,
- variables: {
- id: this.id,
- },
- })
- .then(() => {
- this.toastSuccess('TODO: request message deleted ')
- })
- .catch((err) => {
- this.toastError(err.message)
- })
- })
+ deleteContributionLink(id, name) {
+ this.$bvModal
+ .msgBoxConfirm(this.$t('contributionLink.deleteNow', { name: name }))
+ .then(async (value) => {
+ if (value)
+ await this.$apollo
+ .mutate({
+ mutation: deleteContributionLink,
+ variables: {
+ id: id,
+ },
+ })
+ .then(() => {
+ this.toastSuccess(this.$t('contributionLink.deleted'))
+ this.$emit('get-contribution-links')
+ })
+ .catch((err) => {
+ this.toastError(err.message)
+ })
+ })
},
editContributionLink(row) {
this.$emit('editContributionLinkData', row)
diff --git a/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.spec.js b/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.spec.js
index 7cca315d7..f8575492a 100644
--- a/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.spec.js
+++ b/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.spec.js
@@ -20,7 +20,7 @@ describe('ContributionMessagesListItem', () => {
const propsData = {
contributionId: 42,
- state: 'PENDING0',
+ state: 'PENDING',
message: {
id: 111,
message: 'asd asda sda sda',
diff --git a/admin/src/components/CreationTransactionList.spec.js b/admin/src/components/CreationTransactionList.spec.js
index 3e2d5893e..ff9607424 100644
--- a/admin/src/components/CreationTransactionList.spec.js
+++ b/admin/src/components/CreationTransactionList.spec.js
@@ -6,30 +6,29 @@ const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
- creationTransactionList: [
- {
- id: 1,
- amount: 100,
- balanceDate: 0,
- creationDate: new Date(),
- memo: 'Testing',
- linkedUser: {
- firstName: 'Gradido',
- lastName: 'Akademie',
+ creationTransactionList: {
+ contributionCount: 2,
+ contributionList: [
+ {
+ id: 1,
+ amount: 5.8,
+ createdAt: '2022-09-21T11:09:51.000Z',
+ confirmedAt: null,
+ contributionDate: '2022-08-01T00:00:00.000Z',
+ memo: 'für deine Hilfe, Fräulein Rottenmeier',
+ state: 'PENDING',
},
- },
- {
- id: 2,
- amount: 200,
- balanceDate: 0,
- creationDate: new Date(),
- memo: 'Testing 2',
- linkedUser: {
- firstName: 'Gradido',
- lastName: 'Akademie',
+ {
+ id: 2,
+ amount: '47',
+ createdAt: '2022-09-21T11:09:28.000Z',
+ confirmedAt: '2022-09-21T11:09:28.000Z',
+ contributionDate: '2022-08-01T00:00:00.000Z',
+ memo: 'für deine Hilfe, Frau Holle',
+ state: 'CONFIRMED',
},
- },
- ],
+ ],
+ },
},
})
@@ -43,7 +42,7 @@ const mocks = {
const propsData = {
userId: 1,
- fields: ['date', 'balance', 'name', 'memo', 'decay'],
+ fields: ['createdAt', 'contributionDate', 'confirmedAt', 'amount', 'memo'],
}
describe('CreationTransactionList', () => {
@@ -63,7 +62,7 @@ describe('CreationTransactionList', () => {
expect.objectContaining({
variables: {
currentPage: 1,
- pageSize: 25,
+ pageSize: 10,
order: 'DESC',
userId: 1,
},
diff --git a/admin/src/components/CreationTransactionList.vue b/admin/src/components/CreationTransactionList.vue
index ec5c12aa4..2ce143c7f 100644
--- a/admin/src/components/CreationTransactionList.vue
+++ b/admin/src/components/CreationTransactionList.vue
@@ -1,7 +1,44 @@
{{ $t('transactionlist.title') }}
-
+
+
+
+ {{ $d(new Date(data.item.contributionDate), 'month') }}
+
+ {{ $d(new Date(data.item.contributionDate)) }}
+
+
+
+
+
{{ $t('help.help') }}
+
+
+ {{ $t('transactionlist.submitted') }} {{ $t('math.equals') }}
+ {{ $t('help.transactionlist.submitted') }}
+
+
+ {{ $t('transactionlist.period') }} {{ $t('math.equals') }}
+ {{ $t('help.transactionlist.periods') }}
+
+
+ {{ $t('transactionlist.confirmed') }} {{ $t('math.equals') }}
+ {{ $t('help.transactionlist.confirmed') }}
+
+
+ {{ $t('transactionlist.state') }} {{ $t('math.equals') }}
+ {{ $t('help.transactionlist.state') }}
+
+
+
diff --git a/admin/src/graphql/creationTransactionList.js b/admin/src/graphql/creationTransactionList.js
index 327221814..fc3a80b18 100644
--- a/admin/src/graphql/creationTransactionList.js
+++ b/admin/src/graphql/creationTransactionList.js
@@ -8,14 +8,15 @@ export const creationTransactionList = gql`
order: $order
userId: $userId
) {
- id
- amount
- balanceDate
- creationDate
- memo
- linkedUser {
- firstName
- lastName
+ contributionCount
+ contributionList {
+ id
+ amount
+ createdAt
+ confirmedAt
+ contributionDate
+ memo
+ state
}
}
}
diff --git a/admin/src/graphql/showContributionLink.js b/admin/src/graphql/showContributionLink.js
deleted file mode 100644
index 8042db6b5..000000000
--- a/admin/src/graphql/showContributionLink.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import gql from 'graphql-tag'
-
-export const showContributionLink = gql`
- query ($id: Int!) {
- showContributionLink {
- id
- validFrom
- validTo
- name
- memo
- amount
- cycle
- maxPerCycle
- maxAmountPerMonth
- code
- }
- }
-`
diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json
index c977a613e..65611b1ab 100644
--- a/admin/src/locales/de.json
+++ b/admin/src/locales/de.json
@@ -7,7 +7,8 @@
"contributionLinks": "Beitragslinks",
"create": "Anlegen",
"cycle": "Zyklus",
- "deleteNow": "Automatische Creations wirklich löschen?",
+ "deleted": "Automatische Schöpfung gelöscht!",
+ "deleteNow": "Automatische Creations '{name}' wirklich löschen?",
"maximumAmount": "maximaler Betrag",
"maxPerCycle": "Wiederholungen",
"memo": "Nachricht",
@@ -74,10 +75,20 @@
"submit": "Senden"
},
"GDD": "GDD",
+ "help": {
+ "help": "Hilfe",
+ "transactionlist": {
+ "confirmed": "Wann wurde es von einem Moderator / Admin bestätigt.",
+ "periods": "Für welchen Zeitraum wurde vom Mitglied eingereicht.",
+ "state": "[PENDING = eingereicht, DELETED = gelöscht, IN_PROGRESS = im Dialog mit Moderator, DENIED = abgelehnt, CONFIRMED = bestätigt]",
+ "submitted": "Wann wurde es vom Mitglied eingereicht"
+ }
+ },
"hide_details": "Details verbergen",
"lastname": "Nachname",
"math": {
"colon": ":",
+ "equals": "=",
"exclaim": "!",
"pipe": "|",
"plus": "+"
@@ -133,10 +144,11 @@
},
"transactionlist": {
"amount": "Betrag",
- "balanceDate": "Schöpfungsdatum",
- "community": "Gemeinschaft",
- "date": "Datum",
+ "confirmed": "Bestätigt",
"memo": "Nachricht",
+ "period": "Zeitraum",
+ "state": "Status",
+ "submitted": "Eingereicht",
"title": "Alle geschöpften Transaktionen für den Nutzer"
},
"undelete_user": "Nutzer wiederherstellen",
diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json
index e99305e08..57fd1c6b4 100644
--- a/admin/src/locales/en.json
+++ b/admin/src/locales/en.json
@@ -7,7 +7,8 @@
"contributionLinks": "Contribution Links",
"create": "Create",
"cycle": "Cycle",
- "deleteNow": "Do you really delete automatic creations?",
+ "deleted": "Automatic creation deleted!",
+ "deleteNow": "Do you really delete automatic creations '{name}'?",
"maximumAmount": "Maximum amount",
"maxPerCycle": "Repetition",
"memo": "Memo",
@@ -74,10 +75,20 @@
"submit": "Send"
},
"GDD": "GDD",
+ "help": {
+ "help": "Help",
+ "transactionlist": {
+ "confirmed": "When was it confirmed by a moderator / admin.",
+ "periods": "For what period was it submitted by the member.",
+ "state": "[PENDING = submitted, DELETED = deleted, IN_PROGRESS = in dialogue with moderator, DENIED = denied, CONFIRMED = confirmed]",
+ "submitted": "When was it submitted by the member"
+ }
+ },
"hide_details": "Hide details",
"lastname": "Lastname",
"math": {
"colon": ":",
+ "equals": "=",
"exclaim": "!",
"pipe": "|",
"plus": "+"
@@ -133,10 +144,11 @@
},
"transactionlist": {
"amount": "Amount",
- "balanceDate": "Creation date",
- "community": "Community",
- "date": "Date",
+ "confirmed": "Confirmed",
"memo": "Message",
+ "period": "Period",
+ "state": "State",
+ "submitted": "Submitted",
"title": "All creation-transactions for the user"
},
"undelete_user": "Undelete User",
diff --git a/admin/src/pages/Overview.vue b/admin/src/pages/Overview.vue
index 4c4ba47fa..cfa247b8e 100644
--- a/admin/src/pages/Overview.vue
+++ b/admin/src/pages/Overview.vue
@@ -28,7 +28,11 @@
-
+
diff --git a/backend/src/graphql/model/Contribution.ts b/backend/src/graphql/model/Contribution.ts
index 1f690a3d8..cf57e632f 100644
--- a/backend/src/graphql/model/Contribution.ts
+++ b/backend/src/graphql/model/Contribution.ts
@@ -5,7 +5,7 @@ import { User } from '@entity/User'
@ObjectType()
export class Contribution {
- constructor(contribution: dbContribution, user: User) {
+ constructor(contribution: dbContribution, user?: User | null) {
this.id = contribution.id
this.firstName = user ? user.firstName : null
this.lastName = user ? user.lastName : null
diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts
index e9ee0b55b..3435edb94 100644
--- a/backend/src/graphql/resolver/AdminResolver.ts
+++ b/backend/src/graphql/resolver/AdminResolver.ts
@@ -15,6 +15,7 @@ import { AdminCreateContributions } from '@model/AdminCreateContributions'
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
import { ContributionLink } from '@model/ContributionLink'
import { ContributionLinkList } from '@model/ContributionLinkList'
+import { Contribution } from '@model/Contribution'
import { RIGHTS } from '@/auth/RIGHTS'
import { UserRepository } from '@repository/User'
import AdminCreateContributionArgs from '@arg/AdminCreateContributionArgs'
@@ -23,12 +24,10 @@ import SearchUsersArgs from '@arg/SearchUsersArgs'
import ContributionLinkArgs from '@arg/ContributionLinkArgs'
import { Transaction as DbTransaction } from '@entity/Transaction'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
-import { Transaction } from '@model/Transaction'
import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
-import { TransactionRepository } from '@repository/Transaction'
import { calculateDecay } from '@/util/decay'
-import { Contribution } from '@entity/Contribution'
+import { Contribution as DbContribution } from '@entity/Contribution'
import { hasElopageBuys } from '@/util/hasElopageBuys'
import { User as dbUser } from '@entity/User'
import { User } from '@model/User'
@@ -40,7 +39,6 @@ import { Decay } from '@model/Decay'
import Paginated from '@arg/Paginated'
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
import { Order } from '@enum/Order'
-import { communityUser } from '@/util/communityUser'
import { findUserByEmail, activationLink, printTimeDuration } from './UserResolver'
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
@@ -66,6 +64,7 @@ import { ContributionMessageType } from '@enum/MessageType'
import { ContributionMessage } from '@model/ContributionMessage'
import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail'
import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributionMessageEmail'
+import { ContributionListResult } from '../model/Contribution'
// const EMAIL_OPT_IN_REGISTER = 1
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
@@ -248,7 +247,7 @@ export class AdminResolver {
const creationDateObj = new Date(creationDate)
logger.trace('creationDateObj:', creationDateObj)
validateContribution(creations, amount, creationDateObj)
- const contribution = Contribution.create()
+ const contribution = DbContribution.create()
contribution.userId = emailContact.userId
contribution.amount = amount
contribution.createdAt = new Date()
@@ -259,7 +258,7 @@ export class AdminResolver {
contribution.contributionStatus = ContributionStatus.PENDING
logger.trace('contribution to save', contribution)
- await Contribution.save(contribution)
+ await DbContribution.save(contribution)
return getUserCreation(emailContact.userId)
}
@@ -317,7 +316,7 @@ export class AdminResolver {
const moderator = getUser(context)
- const contributionToUpdate = await Contribution.findOne({
+ const contributionToUpdate = await DbContribution.findOne({
where: { id, confirmedAt: IsNull() },
})
@@ -350,7 +349,7 @@ export class AdminResolver {
contributionToUpdate.moderatorId = moderator.id
contributionToUpdate.contributionStatus = ContributionStatus.PENDING
- await Contribution.save(contributionToUpdate)
+ await DbContribution.save(contributionToUpdate)
const result = new AdminUpdateContribution()
result.amount = amount
result.memo = contributionToUpdate.memo
@@ -367,7 +366,7 @@ export class AdminResolver {
const contributions = await getConnection()
.createQueryBuilder()
.select('c')
- .from(Contribution, 'c')
+ .from(DbContribution, 'c')
.leftJoinAndSelect('c.messages', 'm')
.where({ confirmedAt: IsNull() })
.getMany()
@@ -399,7 +398,7 @@ export class AdminResolver {
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])
@Mutation(() => Boolean)
async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise {
- const contribution = await Contribution.findOne(id)
+ const contribution = await DbContribution.findOne(id)
if (!contribution) {
logger.error(`Contribution not found for given id: ${id}`)
throw new Error('Contribution not found for given id.')
@@ -416,7 +415,7 @@ export class AdminResolver {
@Arg('id', () => Int) id: number,
@Ctx() context: Context,
): Promise {
- const contribution = await Contribution.findOne(id)
+ 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.')
@@ -481,7 +480,7 @@ export class AdminResolver {
contribution.confirmedBy = moderatorUser.id
contribution.transactionId = transaction.id
contribution.contributionStatus = ContributionStatus.CONFIRMED
- await queryRunner.manager.update(Contribution, { id: contribution.id }, contribution)
+ await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
await queryRunner.commitTransaction()
logger.info('creation commited successfuly.')
@@ -506,24 +505,29 @@ export class AdminResolver {
}
@Authorized([RIGHTS.CREATION_TRANSACTION_LIST])
- @Query(() => [Transaction])
+ @Query(() => ContributionListResult)
async creationTransactionList(
@Args()
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
@Arg('userId', () => Int) userId: number,
- ): Promise {
+ ): Promise {
const offset = (currentPage - 1) * pageSize
- const transactionRepository = getCustomRepository(TransactionRepository)
- const [userTransactions] = await transactionRepository.findByUserPaged(
- userId,
- pageSize,
- offset,
- order,
- true,
- )
+ const [contributionResult, count] = await getConnection()
+ .createQueryBuilder()
+ .select('c')
+ .from(DbContribution, 'c')
+ .leftJoinAndSelect('c.user', 'u')
+ .where(`user_id = ${userId}`)
+ .limit(pageSize)
+ .offset(offset)
+ .orderBy('c.created_at', order)
+ .getManyAndCount()
- const user = await dbUser.findOneOrFail({ id: userId })
- return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
+ return new ContributionListResult(
+ count,
+ contributionResult.map((contribution) => new Contribution(contribution, contribution.user)),
+ )
+ // return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
}
@Authorized([RIGHTS.SEND_ACTIVATION_EMAIL])
@@ -744,7 +748,7 @@ export class AdminResolver {
await queryRunner.startTransaction('REPEATABLE READ')
const contributionMessage = DbContributionMessage.create()
try {
- const contribution = await Contribution.findOne({
+ const contribution = await DbContribution.findOne({
where: { id: contributionId },
relations: ['user'],
})
@@ -773,7 +777,7 @@ export class AdminResolver {
contribution.contributionStatus === ContributionStatus.PENDING
) {
contribution.contributionStatus = ContributionStatus.IN_PROGRESS
- await queryRunner.manager.update(Contribution, { id: contributionId }, contribution)
+ await queryRunner.manager.update(DbContribution, { id: contributionId }, contribution)
}
await sendAddedContributionMessageEmail({
diff --git a/database/migrations/0049-add_user_contacts_table.ts b/database/migrations/0049-add_user_contacts_table.ts
index c3b89ed88..acdd1af61 100644
--- a/database/migrations/0049-add_user_contacts_table.ts
+++ b/database/migrations/0049-add_user_contacts_table.ts
@@ -14,8 +14,8 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
\`type\` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
\`user_id\` int(10) unsigned NOT NULL,
\`email\` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL UNIQUE,
- \`email_verification_code\` bigint(20) unsigned NOT NULL UNIQUE,
- \`email_opt_in_type_id\` int NOT NULL,
+ \`email_verification_code\` bigint(20) unsigned DEFAULT NULL UNIQUE,
+ \`email_opt_in_type_id\` int DEFAULT NULL,
\`email_resend_count\` int DEFAULT '0',
\`email_checked\` tinyint(4) NOT NULL DEFAULT 0,
\`phone\` varchar(255) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
@@ -41,47 +41,13 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
// merge values from login_email_opt_in table with users.email in new user_contacts table
await queryFn(`
- INSERT INTO user_contacts
- (type, user_id, email, email_verification_code, email_opt_in_type_id, email_resend_count, email_checked, created_at, updated_at, deleted_at)
- SELECT
- 'EMAIL',
- u.id as user_id,
- u.email,
- e.verification_code as email_verification_code,
- e.email_opt_in_type_id,
- e.resend_count as email_resend_count,
- u.email_checked,
- e.created as created_at,
- e.updated as updated_at,
- u.deletedAt as deleted_at\
- FROM
- users as u,
- login_email_opt_in as e
- WHERE
- u.id = e.user_id AND
- e.id in (
- WITH opt_in AS (
- SELECT
- le.id, le.user_id, le.created, le.updated, ROW_NUMBER() OVER (PARTITION BY le.user_id ORDER BY le.created DESC) AS row_num
- FROM
- login_email_opt_in as le
- )
- SELECT
- opt_in.id
- FROM
- opt_in
- WHERE
- row_num = 1);`)
- /*
- // SELECT
- // le.id
- // FROM
- // login_email_opt_in as le
- // WHERE
- // le.user_id = u.id
- // ORDER BY
- // le.updated DESC, le.created DESC LIMIT 1);`)
- */
+ INSERT INTO user_contacts
+ (type, user_id, email, email_verification_code, email_opt_in_type_id, email_resend_count, email_checked, created_at, updated_at, deleted_at)
+ SELECT 'EMAIL', users.id, users.email, optin.verification_code, optin.email_opt_in_type_id, optin.resend_count, users.email_checked, users.created, null, users.deletedAt
+ FROM users LEFT JOIN
+ (SELECT le.id, le.user_id, le.verification_code, le.email_opt_in_type_id, le.resend_count, le.created, le.updated,
+ ROW_NUMBER() OVER (PARTITION BY le.user_id ORDER BY le.created DESC) AS row_num
+ FROM login_email_opt_in as le) AS optin ON users.id = optin.user_id AND row_num = 1;`)
// insert in users table the email_id of the new created email-contacts
const contacts = await queryFn(`SELECT c.id, c.user_id FROM user_contacts as c`)
@@ -113,11 +79,13 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom
)
// reconstruct the previous email back from contacts to users table
- const contacts = await queryFn(`SELECT c.id, c.email, c.user_id FROM user_contacts as c`)
+ const contacts = await queryFn(
+ `SELECT c.id, c.email, c.user_id, c.email_checked FROM user_contacts as c`,
+ )
for (const id in contacts) {
const contact = contacts[id]
await queryFn(
- `UPDATE users SET email = "${contact.email}" WHERE id = "${contact.user_id}" and email_id = "${contact.id}"`,
+ `UPDATE users SET email = "${contact.email}", email_checked="${contact.email_checked}" WHERE id = "${contact.user_id}" and email_id = "${contact.id}"`,
)
}
await queryFn('ALTER TABLE users MODIFY COLUMN email varchar(255) NOT NULL UNIQUE;')
diff --git a/deployment/bare_metal/install.sh b/deployment/bare_metal/install.sh
index ddb2706eb..9e60bec08 100755
--- a/deployment/bare_metal/install.sh
+++ b/deployment/bare_metal/install.sh
@@ -4,6 +4,12 @@
# How to do this is described in detail in [setup.md](./setup.md)
# Find current directory & configure paths
+## For manualy use in terminal
+## set -o allexport
+## SCRIPT_DIR=$(pwd)
+## PROJECT_ROOT=$SCRIPT_DIR/../..
+## set +o allexport
+# Use here in script
set -o allexport
SCRIPT_PATH=$(realpath $0)
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
@@ -90,7 +96,7 @@ sudo certbot
# Install 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 cp $SCRIPT_DIR/logrotate/gradido.conf.template /etc/logrotate.d/gradido.conf
sudo chown root:root /etc/logrotate.d/gradido.conf
# Install mysql autobackup
@@ -137,4 +143,4 @@ envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/admin/.env.te
# daily job: 0 4 * * * find /tmp -name "yarn--*" -ctime +1 -exec rm -r {} \; > /dev/null
# Start gradido
# Note: on first startup some errors will occur - nothing serious
-./start.sh
\ No newline at end of file
+./start.sh
diff --git a/deployment/bare_metal/setup.md b/deployment/bare_metal/setup.md
index 652a0a5ce..5892cf4fc 100644
--- a/deployment/bare_metal/setup.md
+++ b/deployment/bare_metal/setup.md
@@ -1,107 +1,233 @@
-# Setup script to setup the server be ready to run gradido
-# This assums you have root access via ssh to your cleanly setup server
-# Furthermore this assumes you have debian (11 64bit) running
-# Check your (Sub-)Domain with your Provider.
-# In this document gddhost.tld refers to your chosen domain
+# Instructions To Run `Gradido` On Your Server
-> ssh root@gddhost.tld
+We split setting up `Gradido` on your server into three steps:
-# change root default shell
-> chsh -s /bin/bash
-# Create user `gradido`
-> useradd -d /home/gradido -m gradido
-> passwd gradido
->> enter new password twice
+- [Preparing your server](#command-list-to-setup-your-server-be-ready-to-install-gradido)
+- [Installing `Gradido`](#use-commands-in-installsh-manually-in-your-shell-for-now)
+- [Crone-Job for `Gradido`](#define-cronjob-to-compensate-yarn-output-in-tmp)
-# Gives the user priviledges - this might be omitted in order to harden security
-# Care: This will require another administering user if you don't want root access.
-# Since this setup expects the user running the software be the same as the administering user,
-# you have to adjust the instructions according to that scenario.
-# You might lock yourself out, if done wrong.
-> usermod -a -G sudo gradido
+## Command List To Setup Your Server Be Ready To Install `Gradido`
-# change gradido default shell
-> chsh -s /bin/bash gradido
-# Install sudo
-> apt-get install sudo
-# switch to the new user
-> su gradido
+We assume you have root access via ssh to your cleanly setup server.
+Furthermore we assume you have debian (11 64bit) running.
-# Register first ssh key for user `gradido`
-> mkdir ~/.ssh
-> chmod 700 ~/.ssh
-> nano ~/.ssh/authorized_keys
->> insert public key
->> ctrl + x
->> save
+Check your (Sub-)Domain with your Provider.
+In this document `gddhost.tld` refers to your chosen domain.
-# Test authentication via SSH
-> ssh -i /path/to/privKey gradido@gddhost.tld
->> This should log you in and allow you to use sudo commands, which will require the user's password
+### SSH into your server
-# Disable password authentication & root login
-> cd /etc/ssh
-> sudo cp sshd_config sshd_config.org
-> sudo nano sshd_config
->> change `PermitRootLogin yes` to `PermitRootLogin no`
->> change `#PasswordAuthentication yes` to `PasswordAuthentication no`
->> change `UsePAM yes` to `UsePAM no`
->> ctrl + x
->> save
-> sudo /etc/init.d/ssh restart
+```bash
+ssh root@gddhost.tld
+```
-# Test SSH Access only, no root ssh access
-> ssh gradido@gddhost.tld
->> Will result in in either a password request for your key or the message `Permission denied (publickey)`
-> ssh -i /path/to/privKey root@gddhost.tld
->> Will result in `Permission denied (publickey)`
-> ssh -i /path/to/privKey gradido@gddhost.tld
->> Will succeed after entering the correct keys password (if any)
+### Change root default shell
-# update system
-> sudo apt-get update
-> sudo apt-get upgrade
+```bash
+chsh -s /bin/bash
+```
-# Install security tools
-## ufw
-> sudo apt-get install ufw
-> sudo ufw allow http
-> sudo ufw allow https
-> sudo ufw allow ssh
-> sudo ufw enable
+### Create user `gradido`
-## fail2ban
-> sudo apt-get install -y fail2ban
-> sudo /etc/init.d/fail2ban restart
+```bash
+$ useradd -d /home/gradido -m gradido
+$ passwd gradido
+# enter new password twice
+```
-# Install gradido
-> sudo apt-get install -y git
-> cd ~
-> git clone https://github.com/gradido/gradido.git
+### Give the user priviledges
-# Timezone
-# Note: This is needed - since there is Summer-Time included in the default server Setup - UTC 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
+This might be omitted in order to harden security.
-# Adjust .env
-# NOTE ';' can not be part of any value
-# The Github Secret is Created on Github in Settimgs -> Webhooks
-> cd gradido/deployment/bare_metal
-> cp .env.dist .env
-> nano .env
->> Adjust values accordingly
-# Define cronjob to compensate yarn output in /tmp
-> yarn creates output in /tmp directory, which must be deleted regularly and will be done per cronjob
-> on stage1 a hourly job is necessary by setting the following job in the crontab for the gradido user
-> crontab -e opens the crontab in edit-mode and insert the following entry:
-> "0 * * * * find /tmp -name "yarn--*" -cmin +60 -exec rm -r {} \; > /dev/null"
-> on stage2 a daily job is necessary by setting the following job in the crontab for the gradido user
-> crontab -e opens the crontab in edit-mode and insert the following entry:
-> "0 4 * * * find /tmp -name "yarn--*" -ctime +1 -exec rm -r {} \; > /dev/null"
-# TODO the install.sh is not yet ready to run directly - consider to use it as pattern to do it manually
-> ./install.sh
+***!!! Attention !!!***
+
+- Care: This will require another administering user if you don't want root access.
+- Since this setup expects the user running the software be the same as the administering user,
+ - you have to adjust the instructions according to that scenario.
+ - you might lock yourself out, if done wrong.
+
+#### Add the new user `gradido` to `sudo` group
+
+```bash
+usermod -a -G sudo gradido
+```
+
+### Change gradido default shell
+
+```bash
+chsh -s /bin/bash gradido
+```
+
+### Install sudo
+
+```bash
+apt-get install sudo
+```
+
+### Switch to the new user
+
+```bash
+su gradido
+```
+
+### Register first ssh key for user `gradido`
+
+```bash
+$ mkdir ~/.ssh
+$ chmod 700 ~/.ssh
+$ nano ~/.ssh/authorized_keys
+# insert public key
+# ctrl + x
+# save
+```
+
+### Test authentication via SSH
+
+If you logout from the server you can test authentication:
+
+```bash
+$ ssh -i /path/to/privKey gradido@gddhost.tld
+# This should log you in and allow you to use sudo commands, which will require the user's password
+```
+
+### Disable password authentication and root login
+
+```bash
+$ cd /etc/ssh
+$ sudo cp sshd_config sshd_config.org
+$ sudo nano sshd_config
+# change 'PermitRootLogin yes' to `PermitRootLogin no`
+# change 'PasswordAuthentication yes' to 'PasswordAuthentication no'
+# change 'UsePAM yes' to 'UsePAM no'
+# ctrl + x
+# save
+$ sudo /etc/init.d/ssh restart
+```
+
+### Test SSH Access only, no root ssh access
+
+```bash
+$ ssh gradido@gddhost.tld
+# Will result in in either a passphrase request for your key or the message 'Permission denied (publickey)'
+$ ssh -i /path/to/privKey root@gddhost.tld
+# Will result in 'Permission denied (publickey)'
+$ ssh -i /path/to/privKey gradido@gddhost.tld
+# Will succeed after entering the correct keys passphrase (if any)
+```
+
+### Update system
+
+```bash
+sudo apt-get update
+sudo apt-get upgrade
+```
+
+### Install security tools
+
+#### Install: `ufw`
+
+```bash
+sudo apt-get install ufw
+sudo ufw allow http
+sudo ufw allow https
+sudo ufw allow ssh
+sudo ufw enable
+```
+
+#### Install: `fail2ban`
+
+```bash
+sudo apt-get install -y fail2ban
+sudo /etc/init.d/fail2ban restart
+```
+
+### Install `Gradido` code
+
+```bash
+sudo apt-get install -y git
+cd ~
+git clone https://github.com/gradido/gradido.git
+```
+
+### Timezone
+
+*Note: This is needed - since there is Summer-Time included in the default server Setup - UTC is REQUIRED for production data.*
+
+```bash
+sudo timedatectl set-timezone UTC
+sudo timedatectl set-ntp on
+sudo apt purge ntp
+sudo systemctl start systemd-timesyncd
+# timedatectl to verify
+```
+
+### Adjust the values in `.env`
+
+***!!! Attention !!!***
+
+*Don't forget this step!
+All your following installations in `install.sh` will fail!*
+
+*Notes:*
+
+- *`;` cannot be part of any value!*
+- *The GitHub secret is created on GitHub in Settings -> Webhooks.*
+
+#### Create `.env` and set values
+
+```bash
+$ cd gradido/deployment/bare_metal
+$ cp .env.dist .env
+$ nano .env
+# adjust values accordingly
+```
+
+## Use Commands In `install.sh` Manually In Your Shell For Now
+
+The script `install.sh` is not yet ready to run directly.
+Use it as pattern to do all steps manually in your terminal shell.
+
+*TODO: Bring the `install.sh` script to run in the shell.*
+
+***!!! Attention !!!***
+
+- *Commands in `install.sh`:*
+ - *The commands for setting the paths in the used env variables are not working directly in the terminal, consider the out commented commands for this purpose.*
+
+Follow the commands in `./install.sh` as installation pattern.
+
+## Define Cronjob To Compensate Yarn Output In `/tmp`
+
+`yarn` creates output in `/tmp` directory, which must be deleted regularly and will be done per Cron-Job.
+
+### On `stage1`
+
+An hourly job is necessary on `stage1` by setting the following job in the `crontab` for the `gradido` user.
+
+Run:
+
+```bash
+crontab -e
+```
+
+This opens the crontab in edit-mode and insert the following entry:
+
+```bash
+0 * * * * find /tmp -name "yarn--*" -cmin +60 -exec rm -r {} \; > /dev/null
+```
+
+### On `stage2`
+
+A daily job is necessary on `stage2` by setting the following job in the `crontab` for the `gradido` user.
+
+Run:
+
+```bash
+crontab -e
+```
+
+This opens the `crontab` in edit-mode and insert the following entry:
+
+```bash
+0 4 * * * find /tmp -name "yarn--*" -ctime +1 -exec rm -r {} \; > /dev/null
+```
diff --git a/e2e-tests/cypress/README.md b/e2e-tests/cypress/README.md
index b1ddae514..4ec1ebe51 100644
--- a/e2e-tests/cypress/README.md
+++ b/e2e-tests/cypress/README.md
@@ -1,24 +1,73 @@
# Gradido End-to-End Testing with [Cypress](https://www.cypress.io/) (CI-ready via Docker)
+A setup to show-case Cypress as an end-to-end testing tool for Gradido running in a Docker container.
+The tests are organized in feature files written in Gherkin syntax.
+
+
+## Features under test
+
+So far these features are initially tested
+- [User authentication](https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/User.Authentication.feature)
+- [User profile - change password](https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/UserProfile.ChangePassword.feature)
+- [User registration]((https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/User.Registration.feature)) (WIP)
-A sample setup to show-case Cypress as an end-to-end testing tool for Gradido running in a Docker container.
-Here we have a simple UI-based happy path login test running against the DEV system.
## Precondition
-Since dependencies and configurations for Github Actions integration is not set up yet, please run in root directory
+
+Before running the tests, change to the repo's root directory (gradido).
+
+### Boot up the system under test
```bash
docker-compose up
```
-to boot up the DEV system, before running the test.
+### Seed the database
+
+The database has to be seeded upfront to every test run.
+
+```bash
+# change to the backend directory
+cd /path/to/gradido/gradido/backend
+
+# install all dependencies
+yarn
+
+# seed the database (everytime before running the tests)
+yarn seed
+```
## Execute the test
+This setup will be integrated in the Gradido Github Actions to automatically support the CI/CD process.
+For now the test setup can only be used locally in two modes.
+
+### Run Cypress directly from the code
+
```bash
+# change to the tests directory
+cd /path/to/gradido/e2e-tests/cypress/tests
+
+# install all dependencies
+yarn install
+
+# a) run the tests on command line
+yarn cypress run
+
+# b) open the Cypress GUI to run the tests in interactive mode
+yarn cypress open
+```
+
+
+### Run Cyprss from a separate Docker container
+
+```bash
+# change to the cypress directory
+cd /path/to/gradido/e2e-tests/cypress/
+
# build a Docker image from the Dockerfile
docker build -t gradido_e2e-tests-cypress .
-# run the Docker container and execute the given tests
-docker run -it --network=host gradido_e2e-tests-cypress yarn run cypress-e2e-tests
+# run the Docker image and execute the given tests
+docker run -it --network=host gradido_e2e-tests-cypress yarn cypress-e2e
```
diff --git a/e2e-tests/cypress/tests/cypress.config.ts b/e2e-tests/cypress/tests/cypress.config.ts
index 815394c5e..ad6a8d7de 100644
--- a/e2e-tests/cypress/tests/cypress.config.ts
+++ b/e2e-tests/cypress/tests/cypress.config.ts
@@ -32,6 +32,7 @@ export default defineConfig({
excludeSpecPattern: "*.js",
baseUrl: "http://localhost:3000",
chromeWebSecurity: false,
+ defaultCommandTimeout: 10000,
supportFile: "cypress/support/index.ts",
viewportHeight: 720,
viewportWidth: 1280,
diff --git a/e2e-tests/cypress/tests/cypress/e2e/User.Registration.feature b/e2e-tests/cypress/tests/cypress/e2e/User.Registration.feature
new file mode 100644
index 000000000..9361d2b84
--- /dev/null
+++ b/e2e-tests/cypress/tests/cypress/e2e/User.Registration.feature
@@ -0,0 +1,13 @@
+Feature: User registration
+ As a user
+ I want to register to create an account
+
+ @skip
+ Scenario: Register successfully
+ Given the browser navigates to page "/register"
+ When the user fills name and email "Regina" "Register" "regina@register.com"
+ And the user agrees to the privacy policy
+ And the user submits the registration form
+ Then the user can use a provided activation link
+ And the user can set a password "Aa12345_"
+ And the user can login with the credentials "regina@register.com" "Aa12345_"
diff --git a/e2e-tests/cypress/tests/cypress/e2e/models/LoginPage.ts b/e2e-tests/cypress/tests/cypress/e2e/models/LoginPage.ts
index a16b93a11..9a0df62ee 100644
--- a/e2e-tests/cypress/tests/cypress/e2e/models/LoginPage.ts
+++ b/e2e-tests/cypress/tests/cypress/e2e/models/LoginPage.ts
@@ -2,8 +2,8 @@
export class LoginPage {
// selectors
- emailInput = "#Email-input-field";
- passwordInput = "#Password-input-field";
+ emailInput = "input[type=email]";
+ passwordInput = "input[type=password]";
submitBtn = "[type=submit]";
emailHint = "#vee_Email";
passwordHint = "#vee_Password";
diff --git a/e2e-tests/cypress/tests/cypress/e2e/models/ProfilePage.ts b/e2e-tests/cypress/tests/cypress/e2e/models/ProfilePage.ts
index b280a0b2a..0532a7ff8 100644
--- a/e2e-tests/cypress/tests/cypress/e2e/models/ProfilePage.ts
+++ b/e2e-tests/cypress/tests/cypress/e2e/models/ProfilePage.ts
@@ -4,8 +4,8 @@ export class ProfilePage {
// selectors
openChangePassword = "[data-test=open-password-change-form]";
oldPasswordInput = "#password-input-field";
- newPasswordInput = "#New-password-input-field";
- newPasswordRepeatInput = "#Repeat-new-password-input-field";
+ newPasswordInput = "#new-password-input-field";
+ newPasswordRepeatInput = "#repeat-new-password-input-field";
submitNewPasswordBtn = "[data-test=submit-new-password-btn]";
goto() {
@@ -19,12 +19,12 @@ export class ProfilePage {
}
enterNewPassword(password: string) {
- cy.get(this.newPasswordInput).clear().type(password);
+ cy.get(this.newPasswordInput).find("input").clear().type(password);
return this;
}
enterRepeatPassword(password: string) {
- cy.get(this.newPasswordRepeatInput).clear().type(password);
+ cy.get(this.newPasswordRepeatInput).find("input").clear().type(password);
return this;
}
diff --git a/e2e-tests/cypress/tests/cypress/e2e/models/RegistrationPage.ts b/e2e-tests/cypress/tests/cypress/e2e/models/RegistrationPage.ts
new file mode 100644
index 000000000..27a9cb8cc
--- /dev/null
+++ b/e2e-tests/cypress/tests/cypress/e2e/models/RegistrationPage.ts
@@ -0,0 +1,42 @@
+///
+
+export class RegistrationPage {
+ // selectors
+ firstnameInput = "#registerFirstname";
+ lastnameInput = "#registerLastname";
+ emailInput = "#Email-input-field";
+ checkbox = "#registerCheckbox";
+ submitBtn = "[type=submit]";
+
+ RegistrationThanxHeadline = ".test-message-headline";
+ RegistrationThanxText = ".test-message-subtitle";
+
+ goto() {
+ cy.visit("/register");
+ return this;
+ }
+
+ enterFirstname(firstname: string) {
+ cy.get(this.firstnameInput).clear().type(firstname);
+ return this;
+ }
+
+ enterLastname(lastname: string) {
+ cy.get(this.lastnameInput).clear().type(lastname);
+ return this;
+ }
+
+ enterEmail(email: string) {
+ cy.get(this.emailInput).clear().type(email);
+ return this;
+ }
+
+ checkPrivacyCheckbox() {
+ cy.get(this.checkbox).click({ force: true });
+ }
+
+ submitRegistrationPage() {
+ cy.get(this.submitBtn).should("be.enabled");
+ cy.get(this.submitBtn).click();
+ }
+}
diff --git a/e2e-tests/cypress/tests/cypress/e2e/models/Toasts.ts b/e2e-tests/cypress/tests/cypress/e2e/models/Toasts.ts
index b2198bc8d..aabd0a45e 100644
--- a/e2e-tests/cypress/tests/cypress/e2e/models/Toasts.ts
+++ b/e2e-tests/cypress/tests/cypress/e2e/models/Toasts.ts
@@ -2,6 +2,9 @@
export class Toasts {
// selectors
+ toastSlot = ".b-toaster-slot";
+ toastTypeSuccess = ".b-toast-success";
+ toastTypeError = ".b-toast-danger";
toastTitle = ".gdd-toaster-title";
toastMessage = ".gdd-toaster-body";
}
diff --git a/e2e-tests/cypress/tests/cypress/support/step_definitions/common_steps.ts b/e2e-tests/cypress/tests/cypress/support/step_definitions/common_steps.ts
index 439974cda..f45358f3c 100644
--- a/e2e-tests/cypress/tests/cypress/support/step_definitions/common_steps.ts
+++ b/e2e-tests/cypress/tests/cypress/support/step_definitions/common_steps.ts
@@ -25,11 +25,11 @@ Then("the user is logged in with username {string}", (username: string) => {
Then("the user cannot login", () => {
const toast = new Toasts();
- cy.get(toast.toastTitle).should("contain.text", "Error!");
- cy.get(toast.toastMessage).should(
- "contain.text",
- "No user with this credentials."
- );
+ cy.get(toast.toastSlot).within(() => {
+ cy.get(toast.toastTypeError);
+ cy.get(toast.toastTitle).should("be.visible");
+ cy.get(toast.toastMessage).should("be.visible");
+ });
});
//
diff --git a/e2e-tests/cypress/tests/cypress/support/step_definitions/user_profile_change_password_steps.ts b/e2e-tests/cypress/tests/cypress/support/step_definitions/user_profile_change_password_steps.ts
index cbe851f02..5396b66bb 100644
--- a/e2e-tests/cypress/tests/cypress/support/step_definitions/user_profile_change_password_steps.ts
+++ b/e2e-tests/cypress/tests/cypress/support/step_definitions/user_profile_change_password_steps.ts
@@ -24,9 +24,9 @@ And("the user submits the password form", () => {
When("the user is presented a {string} message", (type: string) => {
const toast = new Toasts();
- cy.get(toast.toastTitle).should("contain.text", "Success");
- cy.get(toast.toastMessage).should(
- "contain.text",
- "Your password has been changed."
- );
+ cy.get(toast.toastSlot).within(() => {
+ cy.get(toast.toastTypeSuccess);
+ cy.get(toast.toastTitle).should("be.visible");
+ cy.get(toast.toastMessage).should("be.visible");
+ });
});
diff --git a/e2e-tests/cypress/tests/package.json b/e2e-tests/cypress/tests/package.json
index a9979725e..a6f817503 100644
--- a/e2e-tests/cypress/tests/package.json
+++ b/e2e-tests/cypress/tests/package.json
@@ -14,7 +14,7 @@
}
},
"scripts": {
- "cypress": "cypress run",
+ "cypress-e2e": "cypress run",
"lint": "eslint --max-warnings=0 --ext .js,.ts ."
},
"dependencies": {
diff --git a/frontend/src/components/Inputs/InputPasswordConfirmation.vue b/frontend/src/components/Inputs/InputPasswordConfirmation.vue
index 3209018c3..56d58d9ad 100644
--- a/frontend/src/components/Inputs/InputPasswordConfirmation.vue
+++ b/frontend/src/components/Inputs/InputPasswordConfirmation.vue
@@ -29,6 +29,7 @@
required: true,
samePassword: value.password,
}"
+ id="repeat-new-password-input-field"
:label="register ? $t('form.passwordRepeat') : $t('form.password_new_repeat')"
:immediate="true"
:name="createId(register ? $t('form.passwordRepeat') : $t('form.password_new_repeat'))"