diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 0d8252402..a62a09bee 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -33,6 +33,7 @@ export enum RIGHTS { LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS', COMMUNITY_STATISTICS = 'COMMUNITY_STATISTICS', SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS', + CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE', // Admin SEARCH_USERS = 'SEARCH_USERS', SET_USER_ROLE = 'SET_USER_ROLE', @@ -50,4 +51,5 @@ export enum RIGHTS { CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK', DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK', UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK', + ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE', } diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index f14e77b17..bad18db9e 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -31,6 +31,7 @@ export const ROLE_USER = new Role('user', [ RIGHTS.SEARCH_ADMIN_USERS, RIGHTS.LIST_CONTRIBUTION_LINKS, RIGHTS.COMMUNITY_STATISTICS, + RIGHTS.CREATE_CONTRIBUTION_MESSAGE, ]) export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights diff --git a/backend/src/graphql/arg/ContributionMessageArgs.ts b/backend/src/graphql/arg/ContributionMessageArgs.ts new file mode 100644 index 000000000..dd75baed0 --- /dev/null +++ b/backend/src/graphql/arg/ContributionMessageArgs.ts @@ -0,0 +1,11 @@ +import { ArgsType, Field, InputType } from 'type-graphql' + +@InputType() +@ArgsType() +export default class ContributionMessageArgs { + @Field(() => Number) + contributionId: number + + @Field(() => String) + message: string +} diff --git a/backend/src/graphql/model/Contribution.ts b/backend/src/graphql/model/Contribution.ts index aa878990c..741debbbb 100644 --- a/backend/src/graphql/model/Contribution.ts +++ b/backend/src/graphql/model/Contribution.ts @@ -1,7 +1,8 @@ import { ObjectType, Field, Int } from 'type-graphql' import Decimal from 'decimal.js-light' import { Contribution as dbContribution } from '@entity/Contribution' -import { User } from './User' +import { User } from '@entity/User' +import { ContributionMessage } from './ContributionMessage' @ObjectType() export class Contribution { @@ -16,6 +17,10 @@ export class Contribution { this.confirmedAt = contribution.confirmedAt this.confirmedBy = contribution.confirmedBy this.contributionDate = contribution.contributionDate + this.state = contribution.contributionStatus + this.messages = contribution.messages + ? contribution.messages.map((message) => new ContributionMessage(message, user)) + : [] } @Field(() => Number) @@ -47,6 +52,12 @@ export class Contribution { @Field(() => Date) contributionDate: Date + + @Field(() => [ContributionMessage]) + messages: ContributionMessage[] + + @Field(() => String) + state: string } @ObjectType() diff --git a/backend/src/graphql/model/ContributionMessage.ts b/backend/src/graphql/model/ContributionMessage.ts new file mode 100644 index 000000000..98b6b2721 --- /dev/null +++ b/backend/src/graphql/model/ContributionMessage.ts @@ -0,0 +1,37 @@ +import { Field, ObjectType } from 'type-graphql' +import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage' +import { User } from '@entity/User' + +@ObjectType() +export class ContributionMessage { + constructor(contributionMessage: DbContributionMessage, user: User) { + this.id = contributionMessage.id + this.message = contributionMessage.message + this.createdAt = contributionMessage.createdAt + this.updatedAt = contributionMessage.updatedAt + this.type = contributionMessage.type + this.userFirstName = user.firstName + this.userLastName = user.lastName + } + + @Field(() => Number) + id: number + + @Field(() => String) + message: string + + @Field(() => Date) + createdAt: Date + + @Field(() => Date, { nullable: true }) + updatedAt?: Date | null + + @Field(() => String) + type: string + + @Field(() => String, { nullable: true }) + userFirstName: string | null + + @Field(() => String, { nullable: true }) + userLastName: string | null +} diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index e70fe71ee..63da1baed 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -62,6 +62,10 @@ import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, } from './const/const' +import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage' +import ContributionMessageArgs from '@arg/ContributionMessageArgs' +import { ContributionMessageType } from '@enum/MessageType' +import { ContributionMessage } from '@model/ContributionMessage' // const EMAIL_OPT_IN_REGISTER = 1 // const EMAIL_OPT_UNKNOWN = 3 // elopage? @@ -696,4 +700,46 @@ export class AdminResolver { logger.debug(`updateContributionLink successful!`) return new ContributionLink(dbContributionLink) } + + @Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTION_MESSAGE]) + @Mutation(() => ContributionMessage) + async adminCreateContributionMessage( + @Args() { contributionId, message }: ContributionMessageArgs, + @Ctx() context: Context, + ): Promise { + const user = getUser(context) + const queryRunner = getConnection().createQueryRunner() + await queryRunner.connect() + await queryRunner.startTransaction('READ UNCOMMITTED') + const contributionMessage = DbContributionMessage.create() + try { + const contribution = await Contribution.findOne({ id: contributionId }) + if (!contribution) { + throw new Error('Contribution not found') + } + contributionMessage.contributionId = contributionId + contributionMessage.createdAt = new Date() + contributionMessage.message = message + contributionMessage.userId = user.id + contributionMessage.type = ContributionMessageType.DIALOG + await queryRunner.manager.insert(DbContributionMessage, contributionMessage) + + if ( + contribution.contributionStatus === ContributionStatus.DELETED || + contribution.contributionStatus === ContributionStatus.DENIED || + contribution.contributionStatus === ContributionStatus.PENDING + ) { + contribution.contributionStatus = ContributionStatus.IN_PROGRESS + await queryRunner.manager.update(Contribution, { id: contributionId }, contribution) + } + await queryRunner.commitTransaction() + } catch (e) { + await queryRunner.rollbackTransaction() + logger.error(`ContributionMessage was not successful: ${e}`) + throw new Error(`ContributionMessage was not successful: ${e}`) + } finally { + await queryRunner.release() + } + return new ContributionMessage(contributionMessage, user) + } } diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.ts b/backend/src/graphql/resolver/ContributionMessageResolver.ts new file mode 100644 index 000000000..c73301598 --- /dev/null +++ b/backend/src/graphql/resolver/ContributionMessageResolver.ts @@ -0,0 +1,56 @@ +import { backendLogger as logger } from '@/server/logger' +import { RIGHTS } from '@/auth/RIGHTS' +import { Context, getUser } from '@/server/context' +import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage' +import { Args, Authorized, Ctx, Mutation, Resolver } from 'type-graphql' +import ContributionMessageArgs from '@arg/ContributionMessageArgs' +import { Contribution } from '@entity/Contribution' +import { ContributionMessageType } from '@enum/MessageType' +import { ContributionStatus } from '@enum/ContributionStatus' +import { getConnection } from '@dbTools/typeorm' +import { ContributionMessage } from '@model/ContributionMessage' + +@Resolver() +export class ContributionMessageResolver { + @Authorized([RIGHTS.CREATE_CONTRIBUTION_MESSAGE]) + @Mutation(() => ContributionMessage) + async createContributionMessage( + @Args() { contributionId, message }: ContributionMessageArgs, + @Ctx() context: Context, + ): Promise { + const user = getUser(context) + const queryRunner = getConnection().createQueryRunner() + await queryRunner.connect() + await queryRunner.startTransaction('READ UNCOMMITTED') + const contributionMessage = DbContributionMessage.create() + try { + const contribution = await Contribution.findOne({ id: contributionId }) + if (!contribution) { + throw new Error('Contribution not found') + } + if (contribution.userId !== user.id) { + throw new Error('Can not send message to contribution of another user') + } + + contributionMessage.contributionId = contributionId + contributionMessage.createdAt = new Date() + contributionMessage.message = message + contributionMessage.userId = user.id + contributionMessage.type = ContributionMessageType.DIALOG + await queryRunner.manager.insert(DbContributionMessage, contributionMessage) + + if (contribution.contributionStatus === ContributionStatus.IN_PROGRESS) { + contribution.contributionStatus = ContributionStatus.PENDING + await queryRunner.manager.update(Contribution, { id: contributionId }, contribution) + } + await queryRunner.commitTransaction() + } catch (e) { + await queryRunner.rollbackTransaction() + logger.error(`ContributionMessage was not successful: ${e}`) + throw new Error(`ContributionMessage was not successful: ${e}`) + } finally { + await queryRunner.release() + } + return new ContributionMessage(contributionMessage, user) + } +} diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 8056ffde3..a59ba0a86 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -96,13 +96,14 @@ export class ContributionResolver { order: { createdAt: order, }, + relations: ['messages'], withDeleted: true, skip: (currentPage - 1) * pageSize, take: pageSize, }) return new ContributionListResult( count, - contributions.map((contribution) => new Contribution(contribution, new User(user))), + contributions.map((contribution) => new Contribution(contribution, user)), ) } @@ -123,9 +124,7 @@ export class ContributionResolver { .getManyAndCount() return new ContributionListResult( count, - dbContributions.map( - (contribution) => new Contribution(contribution, new User(contribution.user)), - ), + dbContributions.map((contribution) => new Contribution(contribution, contribution.user)), ) } diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 03299dd49..f701ad8cf 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -206,6 +206,16 @@ export const listContributions = gql` confirmedAt confirmedBy deletedAt + state + messages { + id + message + createdAt + updatedAt + type + userFirstName + userLastName + } } } }