diff --git a/admin/src/graphql/searchUsers.js b/admin/src/graphql/searchUsers.js index 3e33fe96b..15fb8da32 100644 --- a/admin/src/graphql/searchUsers.js +++ b/admin/src/graphql/searchUsers.js @@ -1,12 +1,19 @@ import gql from 'graphql-tag' export const searchUsers = gql` - query ($searchText: String!, $currentPage: Int, $pageSize: Int, $filters: SearchUsersFilters) { + query ( + $query: String! + $filters: SearchUsersFilters + $currentPage: Int = 0 + $pageSize: Int = 25 + $order: Order = ASC + ) { searchUsers( - searchText: $searchText + query: $query + filters: $filters currentPage: $currentPage pageSize: $pageSize - filters: $filters + order: $order ) { userCount userList { diff --git a/admin/src/pages/UserSearch.spec.js b/admin/src/pages/UserSearch.spec.js index 0d145cb89..1f9b0ce6e 100644 --- a/admin/src/pages/UserSearch.spec.js +++ b/admin/src/pages/UserSearch.spec.js @@ -10,11 +10,20 @@ const apolloQueryMock = jest.fn().mockResolvedValue({ userCount: 4, userList: [ { - userId: 1, - firstName: 'Bibi', - lastName: 'Bloxberg', - email: 'bibi@bloxberg.de', - creation: [200, 400, 600], + userId: 4, + firstName: 'New', + lastName: 'User', + email: 'new@user.ch', + creation: [1000, 1000, 1000], + emailChecked: false, + deletedAt: null, + }, + { + userId: 3, + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + creation: [0, 0, 0], emailChecked: true, deletedAt: null, }, @@ -28,23 +37,14 @@ const apolloQueryMock = jest.fn().mockResolvedValue({ deletedAt: new Date(), }, { - userId: 3, - firstName: 'Peter', - lastName: 'Lustig', - email: 'peter@lustig.de', - creation: [0, 0, 0], + userId: 1, + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + creation: [200, 400, 600], emailChecked: true, deletedAt: null, }, - { - userId: 4, - firstName: 'New', - lastName: 'User', - email: 'new@user.ch', - creation: [1000, 1000, 1000], - emailChecked: false, - deletedAt: null, - }, ], }, }, @@ -79,9 +79,10 @@ describe('UserSearch', () => { expect(apolloQueryMock).toBeCalledWith( expect.objectContaining({ variables: { - searchText: '', + query: '', currentPage: 1, pageSize: 25, + order: 'DESC', filters: { byActivated: null, byDeleted: null, @@ -100,9 +101,10 @@ describe('UserSearch', () => { expect(apolloQueryMock).toBeCalledWith( expect.objectContaining({ variables: { - searchText: '', + query: '', currentPage: 1, pageSize: 25, + order: 'DESC', filters: { byActivated: false, byDeleted: null, @@ -122,9 +124,10 @@ describe('UserSearch', () => { expect(apolloQueryMock).toBeCalledWith( expect.objectContaining({ variables: { - searchText: '', + query: '', currentPage: 1, pageSize: 25, + order: 'DESC', filters: { byActivated: null, byDeleted: true, @@ -144,9 +147,10 @@ describe('UserSearch', () => { expect(apolloQueryMock).toBeCalledWith( expect.objectContaining({ variables: { - searchText: '', + query: '', currentPage: 2, pageSize: 25, + order: 'DESC', filters: { byActivated: null, byDeleted: null, @@ -166,9 +170,10 @@ describe('UserSearch', () => { expect(apolloQueryMock).toBeCalledWith( expect.objectContaining({ variables: { - searchText: 'search string', + query: 'search string', currentPage: 1, pageSize: 25, + order: 'DESC', filters: { byActivated: null, byDeleted: null, @@ -185,9 +190,10 @@ describe('UserSearch', () => { expect(apolloQueryMock).toBeCalledWith( expect.objectContaining({ variables: { - searchText: '', + query: '', currentPage: 1, pageSize: 25, + order: 'DESC', filters: { byActivated: null, byDeleted: null, diff --git a/admin/src/pages/UserSearch.vue b/admin/src/pages/UserSearch.vue index 8f24181ee..d4e4083c2 100644 --- a/admin/src/pages/UserSearch.vue +++ b/admin/src/pages/UserSearch.vue @@ -49,7 +49,7 @@ pills size="lg" v-model="currentPage" - per-page="perPage" + :per-page="perPage" :total-rows="rows" align="center" :hide-ellipsis="true" @@ -97,10 +97,11 @@ export default { .query({ query: searchUsers, variables: { - searchText: this.criteria, + query: this.criteria, + filters: this.filters, currentPage: this.currentPage, pageSize: this.perPage, - filters: this.filters, + order: 'DESC', }, fetchPolicy: 'no-cache', }) diff --git a/backend/src/graphql/arg/SearchUsersArgs.ts b/backend/src/graphql/arg/SearchUsersArgs.ts deleted file mode 100644 index 0ebc442c3..000000000 --- a/backend/src/graphql/arg/SearchUsersArgs.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ArgsType, Field, Int } from 'type-graphql' - -import { SearchUsersFilters } from '@arg/SearchUsersFilters' - -@ArgsType() -export class SearchUsersArgs { - @Field(() => String) - searchText: string - - @Field(() => Int, { nullable: true }) - // eslint-disable-next-line type-graphql/invalid-nullable-input-type - currentPage?: number - - @Field(() => Int, { nullable: true }) - // eslint-disable-next-line type-graphql/invalid-nullable-input-type - pageSize?: number - - // eslint-disable-next-line type-graphql/wrong-decorator-signature - @Field(() => SearchUsersFilters, { nullable: true, defaultValue: null }) - filters?: SearchUsersFilters | null -} diff --git a/backend/src/graphql/model/KlickTipp.ts b/backend/src/graphql/model/KlickTipp.ts index 059c7874d..356ed391f 100644 --- a/backend/src/graphql/model/KlickTipp.ts +++ b/backend/src/graphql/model/KlickTipp.ts @@ -1,12 +1,9 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { ObjectType, Field } from 'type-graphql' @ObjectType() export class KlickTipp { - constructor(json: any) { - this.newsletterState = json.status === 'Subscribed' + constructor(newsletterState: boolean) { + this.newsletterState = newsletterState } @Field(() => Boolean) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 25787490f..0384b64c5 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -2110,7 +2110,7 @@ describe('UserResolver', () => { describe('search users', () => { const variablesWithoutTextAndFilters = { - searchText: '', + query: '', currentPage: 1, pageSize: 25, filters: null, diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index a61bbfd32..9df6b861b 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -8,22 +8,12 @@ import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink' import { User as DbUser } from '@entity/User' import { UserContact as DbUserContact } from '@entity/UserContact' import i18n from 'i18n' -import { - Resolver, - Query, - Args, - Arg, - Authorized, - Ctx, - UseMiddleware, - Mutation, - Int, -} from 'type-graphql' +import { Resolver, Query, Args, Arg, Authorized, Ctx, Mutation, Int } from 'type-graphql' import { v4 as uuidv4 } from 'uuid' import { CreateUserArgs } from '@arg/CreateUserArgs' import { Paginated } from '@arg/Paginated' -import { SearchUsersArgs } from '@arg/SearchUsersArgs' +import { SearchUsersFilters } from '@arg/SearchUsersFilters' import { UnsecureLoginArgs } from '@arg/UnsecureLoginArgs' import { UpdateUserInfosArgs } from '@arg/UpdateUserInfosArgs' import { OptInType } from '@enum/OptInType' @@ -60,7 +50,6 @@ import { EVENT_ADMIN_USER_DELETE, EVENT_ADMIN_USER_UNDELETE, } from '@/event/Events' -import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware' import { isValidPassword } from '@/password/EncryptorUtils' import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor' import { Context, getUser, getClientTimezoneOffset } from '@/server/context' @@ -76,6 +65,7 @@ import { randombytes_random } from 'sodium-native' import { FULL_CREATION_AVAILABLE } from './const/const' import { getUserCreations } from './util/creations' import { findUserByIdentifier } from './util/findUserByIdentifier' +import { getKlicktippState } from './util/getKlicktippState' import { validateAlias } from './util/validateAlias' const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl'] @@ -120,7 +110,6 @@ const newGradidoID = async (): Promise => { export class UserResolver { @Authorized([RIGHTS.VERIFY_LOGIN]) @Query(() => User) - @UseMiddleware(klicktippNewsletterStateMiddleware) async verifyLogin(@Ctx() context: Context): Promise { logger.info('verifyLogin...') // TODO refactor and do not have duplicate code with login(see below) @@ -130,12 +119,12 @@ export class UserResolver { user.hasElopage = await this.hasElopage(context) logger.debug(`verifyLogin... successful: ${user.firstName}.${user.lastName}`) + user.klickTipp = await getKlicktippState(userEntity.emailContact.email) return user } @Authorized([RIGHTS.LOGIN]) @Mutation(() => User) - @UseMiddleware(klicktippNewsletterStateMiddleware) async login( @Args() { email, password, publisherId }: UnsecureLoginArgs, @Ctx() context: Context, @@ -181,6 +170,7 @@ export class UserResolver { dbUser.publisherId = publisherId await DbUser.save(dbUser) } + user.klickTipp = await getKlicktippState(dbUser.emailContact.email) context.setHeaders.push({ key: 'token', @@ -638,8 +628,11 @@ export class UserResolver { @Authorized([RIGHTS.SEARCH_USERS]) @Query(() => SearchUsersResult) async searchUsers( + @Arg('query', () => String) query: string, + @Arg('filters', () => SearchUsersFilters, { nullable: true }) + filters: SearchUsersFilters | null | undefined, @Args() - { searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs, + { currentPage = 1, pageSize = 25, order = Order.ASC }: Paginated, @Ctx() context: Context, ): Promise { const clientTimezoneOffset = getClientTimezoneOffset(context) @@ -657,15 +650,16 @@ export class UserResolver { userFields.map((fieldName) => { return 'user.' + fieldName }), - searchText, + query, filters ?? null, currentPage, pageSize, + order, ) if (users.length === 0) { return { - userCount: 0, + userCount: count, userList: [], } } diff --git a/backend/src/graphql/resolver/util/getKlicktippState.ts b/backend/src/graphql/resolver/util/getKlicktippState.ts new file mode 100644 index 000000000..728f565ff --- /dev/null +++ b/backend/src/graphql/resolver/util/getKlicktippState.ts @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { KlickTipp } from '@model/KlickTipp' + +import { getKlickTippUser } from '@/apis/KlicktippController' +import { klickTippLogger as logger } from '@/server/logger' + +export const getKlicktippState = async (email: string): Promise => { + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const klickTippUser = await getKlickTippUser(email) + if (klickTippUser) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return new KlickTipp(klickTippUser.status === 'Subscribed') + } + } catch (err) { + logger.error('There is no klicktipp user for email', email, err) + } + return new KlickTipp(false) +} diff --git a/backend/src/middleware/klicktippMiddleware.ts b/backend/src/middleware/klicktippMiddleware.ts deleted file mode 100644 index 038bd3dd3..000000000 --- a/backend/src/middleware/klicktippMiddleware.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -import { MiddlewareFn } from 'type-graphql' - -import { KlickTipp } from '@model/KlickTipp' - -import { getKlickTippUser } from '@/apis/KlicktippController' -import { klickTippLogger as logger } from '@/server/logger' - -export const klicktippNewsletterStateMiddleware: MiddlewareFn = async ( - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ - { root, args, context, info }, - next, -) => { - // eslint-disable-next-line n/callback-return - const result = await next() - let klickTipp = new KlickTipp({ status: 'Unsubscribed' }) - try { - const klickTippUser = await getKlickTippUser(result.email) - if (klickTippUser) { - klickTipp = new KlickTipp(klickTippUser) - } - } catch (err) { - logger.error(`There is no user for (email='${result.email}') ${err}`) - } - result.klickTipp = klickTipp - return result -} diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index ce7efbfc3..a964cdb3a 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -69,12 +69,19 @@ export const sendResetPasswordEmail = gql` ` export const searchUsers = gql` - query ($searchText: String!, $currentPage: Int, $pageSize: Int, $filters: SearchUsersFilters) { + query ( + $query: String! + $filters: SearchUsersFilters + $currentPage: Int = 1 + $pageSize: Int = 25 + $order: Order = ASC + ) { searchUsers( - searchText: $searchText + query: $query + filters: $filters currentPage: $currentPage pageSize: $pageSize - filters: $filters + order: $order ) { userCount userList { diff --git a/backend/src/typeorm/repository/User.ts b/backend/src/typeorm/repository/User.ts index 53273102d..f54859f41 100644 --- a/backend/src/typeorm/repository/User.ts +++ b/backend/src/typeorm/repository/User.ts @@ -1,7 +1,8 @@ import { Brackets, EntityRepository, IsNull, Not, Repository } from '@dbTools/typeorm' import { User as DbUser } from '@entity/User' -import { SearchUsersFilters } from '@/graphql/arg/SearchUsersFilters' +import { SearchUsersFilters } from '@arg/SearchUsersFilters' +import { Order } from '@enum/Order' @EntityRepository(DbUser) export class UserRepository extends Repository { @@ -11,6 +12,7 @@ export class UserRepository extends Repository { filters: SearchUsersFilters | null, currentPage: number, pageSize: number, + order = Order.ASC, ): Promise<[DbUser[], number]> { const query = this.createQueryBuilder('user') .select(select) @@ -46,6 +48,7 @@ export class UserRepository extends Repository { } return query + .orderBy({ 'user.id': order }) .take(pageSize) .skip((currentPage - 1) * pageSize) .getManyAndCount()