diff --git a/backend/README.md b/backend/README.md index e74750c46..b27ab16d9 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,16 +1,18 @@ # backend ## Project setup -``` + +```bash yarn install ``` ## Seed DB -``` + +```bash yarn seed ``` -Deletes all data in database. Then seeds data in database. +Deletes all data in database. Then seeds data in database. ## Seeded Users @@ -22,3 +24,47 @@ Deletes all data in database. Then seeds data in database. | bob@baumeister.de | `Aa12345_` | `false` | `true` | `false` | | garrick@ollivander.com | | `false` | `false` | `false` | | stephen@hawking.uk | `Aa12345_` | `false` | `true` | `true` | + +## Setup GraphQL Playground + +### Setup In The Code + +Setting up the GraphQL Playground in our code requires the following steps: + +- Create an empty `.env` file in the `backend` folder and set "GRAPHIQL=true" there. +- Start or restart Docker Compose. +- For verification, Docker should display `GraphQL available at http://localhost:4000` in the terminal. +- If you open "http://localhost:4000/" in your browser, you should see the GraphQL Playground. + +### Authentication + +You need to authenticate yourself in GraphQL Playground to be able to send queries and mutations, to do so follow the steps below: + +- in Firefox go to "Network Analysis" and delete all entries +- enter and send the login query: + +```gql +{ + login(email: "bibi@bloxberg.de", password:"Aa12345_") { + id + publisherId + email + firstName + lastName + emailChecked + language + hasElopage + } +} +``` + +- search in Firefox under „Network Analysis" for the smallest size of a header and copy the value of the token +- open the header section in GraphQL Playground and set your current token by filling in and replacing `XXX`: + +```qgl +{ + "Authorization": "XXX" +} +``` + +Now you can open a new tap in the Playground and enter your query or mutation there. diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 1d0fd2856..639326a64 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -27,6 +27,7 @@ export enum RIGHTS { GDT_BALANCE = 'GDT_BALANCE', CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION', LIST_CONTRIBUTIONS = 'LIST_CONTRIBUTIONS', + LIST_ALL_CONTRIBUTIONS = 'LIST_ALL_CONTRIBUTIONS', UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION', // Admin SEARCH_USERS = 'SEARCH_USERS', diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index 75e552e79..935bd94e5 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -25,6 +25,7 @@ export const ROLE_USER = new Role('user', [ RIGHTS.GDT_BALANCE, RIGHTS.CREATE_CONTRIBUTION, RIGHTS.LIST_CONTRIBUTIONS, + RIGHTS.LIST_ALL_CONTRIBUTIONS, RIGHTS.UPDATE_CONTRIBUTION, ]) export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights diff --git a/backend/src/graphql/model/Contribution.ts b/backend/src/graphql/model/Contribution.ts index 348a6eb98..34bffd6d7 100644 --- a/backend/src/graphql/model/Contribution.ts +++ b/backend/src/graphql/model/Contribution.ts @@ -7,7 +7,8 @@ import { User } from './User' export class Contribution { constructor(contribution: dbContribution, user: User) { this.id = contribution.id - this.user = user + this.firstName = user ? user.firstName : null + this.lastName = user ? user.lastName : null this.amount = contribution.amount this.memo = contribution.memo this.createdAt = contribution.createdAt @@ -19,8 +20,11 @@ export class Contribution { @Field(() => Number) id: number - @Field(() => User) - user: User + @Field(() => String, { nullable: true }) + firstName: string | null + + @Field(() => String, { nullable: true }) + lastName: string | null @Field(() => Decimal) amount: Decimal @@ -43,6 +47,11 @@ export class Contribution { @ObjectType() export class ContributionListResult { + constructor(count: number, list: Contribution[]) { + this.linkCount = count + this.linkList = list + } + @Field(() => Int) linkCount: number diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 7afae08f6..e6478ffc2 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -7,7 +7,7 @@ import { createContribution, updateContribution, } from '@/seeds/graphql/mutations' -import { listContributions, login } from '@/seeds/graphql/queries' +import { listAllContributions, listContributions, login } from '@/seeds/graphql/queries' import { cleanDB, resetToken, testEnvironment } from '@test/helpers' import { GraphQLError } from 'graphql' import { userFactory } from '@/seeds/factory/user' @@ -438,4 +438,82 @@ describe('ContributionResolver', () => { }) }) }) + + describe('listAllContribution', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + query({ + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + filterConfirmed: false, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + await userFactory(testEnv, peterLustig) + const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await creationFactory(testEnv, bibisCreation!) + await query({ + query: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + await mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test env contribution', + creationDate: new Date().toString(), + }, + }) + }) + + it('returns allCreation', async () => { + await expect( + query({ + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + filterConfirmed: false, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listAllContributions: { + linkCount: 2, + linkList: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + memo: 'Herzlich Willkommen bei Gradido!', + amount: '1000', + }), + expect.objectContaining({ + id: expect.any(Number), + memo: 'Test env contribution', + amount: '100', + }), + ]), + }, + }, + }), + ) + }) + }) + }) }) diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index b21232722..d71ecac59 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -7,7 +7,7 @@ import { FindOperator, IsNull } from '@dbTools/typeorm' import ContributionArgs from '@arg/ContributionArgs' import Paginated from '@arg/Paginated' import { Order } from '@enum/Order' -import { Contribution } from '@model/Contribution' +import { Contribution, ContributionListResult } from '@model/Contribution' import { UnconfirmedContribution } from '@model/UnconfirmedContribution' import { User } from '@model/User' import { validateContribution, getUserCreation, updateCreations } from './util/creations' @@ -65,6 +65,28 @@ export class ContributionResolver { return contributions.map((contribution) => new Contribution(contribution, new User(user))) } + @Authorized([RIGHTS.LIST_ALL_CONTRIBUTIONS]) + @Query(() => ContributionListResult) + async listAllContributions( + @Args() + { currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated, + ): Promise { + const [dbContributions, count] = await dbContribution.findAndCount({ + relations: ['user'], + order: { + createdAt: order, + }, + skip: (currentPage - 1) * pageSize, + take: pageSize, + }) + return new ContributionListResult( + count, + dbContributions.map( + (contribution) => new Contribution(contribution, new User(contribution.user)), + ), + ) + } + @Authorized([RIGHTS.UPDATE_CONTRIBUTION]) @Mutation(() => UnconfirmedContribution) async updateContribution( diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index c27ecdd66..deae5f97b 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -191,6 +191,24 @@ export const listContributions = gql` } } ` + +export const listAllContributions = ` +query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC) { + listAllContributions(currentPage: $currentPage, pageSize: $pageSize, order: $order) { + linkCount + linkList { + id + firstName + lastName + amount + memo + createdAt + confirmedAt + confirmedBy + } + } +} +` // from admin interface export const listUnconfirmedContributions = gql` diff --git a/database/entity/0039-contributions_table/Contribution.ts b/database/entity/0039-contributions_table/Contribution.ts index 6c7358f90..b5e6ac0e0 100644 --- a/database/entity/0039-contributions_table/Contribution.ts +++ b/database/entity/0039-contributions_table/Contribution.ts @@ -1,6 +1,15 @@ import Decimal from 'decimal.js-light' -import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, DeleteDateColumn } from 'typeorm' +import { + BaseEntity, + Column, + Entity, + PrimaryGeneratedColumn, + DeleteDateColumn, + JoinColumn, + ManyToOne, +} from 'typeorm' import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' +import { User } from '../User' @Entity('contributions') export class Contribution extends BaseEntity { @@ -10,6 +19,10 @@ export class Contribution extends BaseEntity { @Column({ unsigned: true, nullable: false, name: 'user_id' }) userId: number + @ManyToOne(() => User, (user) => user.contributions) + @JoinColumn({ name: 'user_id' }) + user: User + @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' }) createdAt: Date diff --git a/database/entity/0040-add_contribution_link_id_to_user/User.ts b/database/entity/0040-add_contribution_link_id_to_user/User.ts index 9bf76e5f5..56047345a 100644 --- a/database/entity/0040-add_contribution_link_id_to_user/User.ts +++ b/database/entity/0040-add_contribution_link_id_to_user/User.ts @@ -1,4 +1,13 @@ -import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm' +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToMany, + JoinColumn, +} from 'typeorm' +import { Contribution } from '../Contribution' @Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) export class User extends BaseEntity { @@ -76,4 +85,8 @@ export class User extends BaseEntity { default: null, }) passphrase: string + + @OneToMany(() => Contribution, (contribution) => contribution.user) + @JoinColumn({ name: 'user_id' }) + contributions?: Contribution[] }