{
- this.itemsList = result.data.searchUsers.map((user) => {
+ this.rows = result.data.searchUsers.userCount
+ this.itemsList = result.data.searchUsers.userList.map((user) => {
return {
...user,
showDetails: false,
@@ -153,5 +166,13 @@ export default {
this.itemsMassCreation = []
},
},
+ watch: {
+ currentPage() {
+ this.getUsers()
+ },
+ criteria() {
+ this.getUsers()
+ },
+ },
}
diff --git a/admin/src/pages/UserSearch.spec.js b/admin/src/pages/UserSearch.spec.js
index b6d01b254..4beda0c49 100644
--- a/admin/src/pages/UserSearch.spec.js
+++ b/admin/src/pages/UserSearch.spec.js
@@ -5,15 +5,18 @@ const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
- searchUsers: [
- {
- firstName: 'Bibi',
- lastName: 'Bloxberg',
- email: 'bibi@bloxberg.de',
- creation: [200, 400, 600],
- emailChecked: false,
- },
- ],
+ searchUsers: {
+ userCount: 1,
+ userList: [
+ {
+ firstName: 'Bibi',
+ lastName: 'Bloxberg',
+ email: 'bibi@bloxberg.de',
+ creation: [200, 400, 600],
+ emailChecked: false,
+ },
+ ],
+ },
},
})
diff --git a/admin/src/pages/UserSearch.vue b/admin/src/pages/UserSearch.vue
index a5d4c05cf..4b9bcae8a 100644
--- a/admin/src/pages/UserSearch.vue
+++ b/admin/src/pages/UserSearch.vue
@@ -3,7 +3,7 @@
- {{ $t('unregistered_emails') }}
+ {{ filterCheckedEmails ? $t('all_emails') : $t('unregistered_emails') }}
@@ -21,6 +21,14 @@
:fieldsTable="fields"
:criteria="criteria"
/>
+
@@ -67,14 +75,16 @@ export default {
beforeLastMonth: {
short: this.$moment().subtract(2, 'month').format('MMMM'),
},
+ filterCheckedEmails: false,
+ rows: 0,
+ currentPage: 1,
+ perPage: 25,
}
},
-
methods: {
unconfirmedRegisterMails() {
- this.searchResult = this.searchResult.filter((user) => {
- return !user.emailChecked
- })
+ this.filterCheckedEmails = !this.filterCheckedEmails
+ this.getUsers()
},
getUsers() {
this.$apollo
@@ -82,16 +92,25 @@ export default {
query: searchUsers,
variables: {
searchText: this.criteria,
+ currentPage: this.currentPage,
+ pageSize: this.perPage,
+ notActivated: this.filterCheckedEmails,
},
})
.then((result) => {
- this.searchResult = result.data.searchUsers
+ this.rows = result.data.searchUsers.userCount
+ this.searchResult = result.data.searchUsers.userList
})
.catch((error) => {
this.$toasted.error(error.message)
})
},
},
+ watch: {
+ currentPage() {
+ this.getUsers()
+ },
+ },
created() {
this.getUsers()
},
diff --git a/backend/jest.config.js b/backend/jest.config.js
index 9d99c68f6..981475807 100644
--- a/backend/jest.config.js
+++ b/backend/jest.config.js
@@ -1,15 +1,18 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
-module.exports = {
- verbose: true,
- preset: 'ts-jest',
- collectCoverage: true,
- collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**'],
- moduleNameMapper: {
- '@entity/(.*)': '/../database/build/entity/$1',
- // This is hack to fix a problem with the library `ts-mysql-migrate` which does differentiate between its ts/js state
- '@dbTools/(.*)':
- process.env.NODE_ENV === 'development'
- ? '/../database/src/$1'
- : '/../database/build/src/$1',
- },
+module.exports = async () => {
+ process.env.TZ = 'UTC'
+ return {
+ verbose: true,
+ preset: 'ts-jest',
+ collectCoverage: true,
+ collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**'],
+ moduleNameMapper: {
+ '@entity/(.*)': '/../database/build/entity/$1',
+ // This is hack to fix a problem with the library `ts-mysql-migrate` which does differentiate between its ts/js state
+ '@dbTools/(.*)':
+ process.env.NODE_ENV === 'development'
+ ? '/../database/src/$1'
+ : '/../database/build/src/$1',
+ },
+ }
}
diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts
index 544b39d97..0777211ad 100644
--- a/backend/src/apis/KlicktippController.ts
+++ b/backend/src/apis/KlicktippController.ts
@@ -5,7 +5,7 @@ import CONFIG from '../config'
const klicktippConnector = new KlicktippConnector()
-export const signIn = async (
+export const klicktippSignIn = async (
email: string,
language: string,
firstName?: string,
diff --git a/backend/src/graphql/arg/SearchUsersArgs.ts b/backend/src/graphql/arg/SearchUsersArgs.ts
new file mode 100644
index 000000000..5b40fd9ca
--- /dev/null
+++ b/backend/src/graphql/arg/SearchUsersArgs.ts
@@ -0,0 +1,16 @@
+import { ArgsType, Field, Int } from 'type-graphql'
+
+@ArgsType()
+export default class SearchUsersArgs {
+ @Field(() => String)
+ searchText: string
+
+ @Field(() => Int, { nullable: true })
+ currentPage?: number
+
+ @Field(() => Int, { nullable: true })
+ pageSize?: number
+
+ @Field(() => Boolean, { nullable: true })
+ notActivated?: boolean
+}
diff --git a/backend/src/graphql/model/UserAdmin.ts b/backend/src/graphql/model/UserAdmin.ts
index befc203a5..9e28a33d6 100644
--- a/backend/src/graphql/model/UserAdmin.ts
+++ b/backend/src/graphql/model/UserAdmin.ts
@@ -1,4 +1,4 @@
-import { ObjectType, Field } from 'type-graphql'
+import { ObjectType, Field, Int } from 'type-graphql'
@ObjectType()
export class UserAdmin {
@@ -20,3 +20,12 @@ export class UserAdmin {
@Field(() => Boolean)
emailChecked: boolean
}
+
+@ObjectType()
+export class SearchUsersResult {
+ @Field(() => Int)
+ userCount: number
+
+ @Field(() => [UserAdmin])
+ userList: UserAdmin[]
+}
diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts
index 11d8c99cd..8a7e034a5 100644
--- a/backend/src/graphql/resolver/AdminResolver.ts
+++ b/backend/src/graphql/resolver/AdminResolver.ts
@@ -3,7 +3,7 @@
import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx } from 'type-graphql'
import { getCustomRepository, Raw } from 'typeorm'
-import { UserAdmin } from '../model/UserAdmin'
+import { UserAdmin, SearchUsersResult } from '../model/UserAdmin'
import { PendingCreation } from '../model/PendingCreation'
import { CreatePendingCreations } from '../model/CreatePendingCreations'
import { UpdatePendingCreation } from '../model/UpdatePendingCreation'
@@ -14,6 +14,7 @@ import { LoginPendingTasksAdminRepository } from '../../typeorm/repository/Login
import { UserRepository } from '../../typeorm/repository/User'
import CreatePendingCreationArgs from '../arg/CreatePendingCreationArgs'
import UpdatePendingCreationArgs from '../arg/UpdatePendingCreationArgs'
+import SearchUsersArgs from '../arg/SearchUsersArgs'
import moment from 'moment'
import { Transaction } from '@entity/Transaction'
import { TransactionCreation } from '@entity/TransactionCreation'
@@ -26,11 +27,13 @@ import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
@Resolver()
export class AdminResolver {
@Authorized([RIGHTS.SEARCH_USERS])
- @Query(() => [UserAdmin])
- async searchUsers(@Arg('searchText') searchText: string): Promise {
+ @Query(() => SearchUsersResult)
+ async searchUsers(
+ @Args() { searchText, currentPage = 1, pageSize = 25, notActivated = false }: SearchUsersArgs,
+ ): Promise {
const userRepository = getCustomRepository(UserRepository)
const users = await userRepository.findBySearchCriteria(searchText)
- const adminUsers = await Promise.all(
+ let adminUsers = await Promise.all(
users.map(async (user) => {
const adminUser = new UserAdmin()
adminUser.userId = user.id
@@ -42,7 +45,12 @@ export class AdminResolver {
return adminUser
}),
)
- return adminUsers
+ if (notActivated) adminUsers = adminUsers.filter((u) => !u.emailChecked)
+ const first = (currentPage - 1) * pageSize
+ return {
+ userCount: adminUsers.length,
+ userList: adminUsers.slice(first, first + pageSize),
+ }
}
@Authorized([RIGHTS.CREATE_PENDING_CREATION])
diff --git a/backend/src/graphql/resolver/KlicktippResolver.ts b/backend/src/graphql/resolver/KlicktippResolver.ts
index fdffb940a..0ba2387e3 100644
--- a/backend/src/graphql/resolver/KlicktippResolver.ts
+++ b/backend/src/graphql/resolver/KlicktippResolver.ts
@@ -6,7 +6,7 @@ import {
getKlickTippUser,
getKlicktippTagMap,
unsubscribe,
- signIn,
+ klicktippSignIn,
} from '../../apis/KlicktippController'
import { RIGHTS } from '../../auth/RIGHTS'
import SubscribeNewsletterArgs from '../arg/SubscribeNewsletterArgs'
@@ -36,6 +36,6 @@ export class KlicktippResolver {
async subscribeNewsletter(
@Args() { email, language }: SubscribeNewsletterArgs,
): Promise {
- return await signIn(email, language)
+ return await klicktippSignIn(email, language)
}
}
diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts
index 8aaf3b69e..02e490edd 100644
--- a/backend/src/graphql/resolver/UserResolver.test.ts
+++ b/backend/src/graphql/resolver/UserResolver.test.ts
@@ -13,6 +13,7 @@ import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { User } from '@entity/User'
import CONFIG from '../../config'
import { sendAccountActivationEmail } from '../../mailer/sendAccountActivationEmail'
+import { klicktippSignIn } from '../../apis/KlicktippController'
jest.mock('../../mailer/sendAccountActivationEmail', () => {
return {
@@ -21,6 +22,13 @@ jest.mock('../../mailer/sendAccountActivationEmail', () => {
}
})
+jest.mock('../../apis/KlicktippController', () => {
+ return {
+ __esModule: true,
+ klicktippSignIn: jest.fn(),
+ }
+})
+
let mutate: any
let con: any
@@ -220,6 +228,157 @@ describe('UserResolver', () => {
})
})
})
+
+ describe('setPassword', () => {
+ const createUserMutation = gql`
+ mutation (
+ $email: String!
+ $firstName: String!
+ $lastName: String!
+ $language: String!
+ $publisherId: Int
+ ) {
+ createUser(
+ email: $email
+ firstName: $firstName
+ lastName: $lastName
+ language: $language
+ publisherId: $publisherId
+ )
+ }
+ `
+
+ const createUserVariables = {
+ email: 'peter@lustig.de',
+ firstName: 'Peter',
+ lastName: 'Lustig',
+ language: 'de',
+ publisherId: 1234,
+ }
+
+ const setPasswordMutation = gql`
+ mutation ($code: String!, $password: String!) {
+ setPassword(code: $code, password: $password)
+ }
+ `
+ let result: any
+ let emailOptIn: string
+
+ describe('valid optin code and valid password', () => {
+ let loginUser: any
+ let newLoginUser: any
+ let newUser: any
+
+ beforeAll(async () => {
+ await mutate({ mutation: createUserMutation, variables: createUserVariables })
+ const loginEmailOptIn = await getRepository(LoginEmailOptIn)
+ .createQueryBuilder('login_email_optin')
+ .getMany()
+ loginUser = await getRepository(LoginUser).createQueryBuilder('login_user').getMany()
+ emailOptIn = loginEmailOptIn[0].verificationCode.toString()
+ result = await mutate({
+ mutation: setPasswordMutation,
+ variables: { code: emailOptIn, password: 'Aa12345_' },
+ })
+ newLoginUser = await getRepository(LoginUser).createQueryBuilder('login_user').getMany()
+ newUser = await getRepository(User).createQueryBuilder('state_user').getMany()
+ })
+
+ afterAll(async () => {
+ await resetDB()
+ })
+
+ it('sets email checked to true', () => {
+ expect(newLoginUser[0].emailChecked).toBeTruthy()
+ })
+
+ it('updates the password', () => {
+ expect(newLoginUser[0].password).toEqual('3917921995996627700')
+ })
+
+ it('updates the public Key on both user tables', () => {
+ expect(newLoginUser[0].pubKey).toEqual(expect.any(Buffer))
+ expect(newLoginUser[0].pubKey).not.toEqual(loginUser[0].pubKey)
+ expect(newLoginUser[0].pubKey).toEqual(newUser[0].pubkey)
+ })
+
+ it('updates the private Key', () => {
+ expect(newLoginUser[0].privKey).toEqual(expect.any(Buffer))
+ expect(newLoginUser[0].privKey).not.toEqual(loginUser[0].privKey)
+ })
+
+ it('removes the optin', async () => {
+ await expect(
+ getRepository(LoginEmailOptIn).createQueryBuilder('login_email_optin').getMany(),
+ ).resolves.toHaveLength(0)
+ })
+
+ it('calls the klicktipp API', () => {
+ expect(klicktippSignIn).toBeCalledWith(
+ loginUser[0].email,
+ loginUser[0].language,
+ loginUser[0].firstName,
+ loginUser[0].lastName,
+ )
+ })
+
+ it('returns true', () => {
+ expect(result).toBeTruthy()
+ })
+ })
+
+ describe('no valid password', () => {
+ beforeAll(async () => {
+ await mutate({ mutation: createUserMutation, variables: createUserVariables })
+ const loginEmailOptIn = await getRepository(LoginEmailOptIn)
+ .createQueryBuilder('login_email_optin')
+ .getMany()
+ emailOptIn = loginEmailOptIn[0].verificationCode.toString()
+ result = await mutate({
+ mutation: setPasswordMutation,
+ variables: { code: emailOptIn, password: 'not-valid' },
+ })
+ })
+
+ afterAll(async () => {
+ await resetDB()
+ })
+
+ it('throws an error', () => {
+ expect(result).toEqual(
+ expect.objectContaining({
+ errors: [
+ new GraphQLError(
+ 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
+ ),
+ ],
+ }),
+ )
+ })
+ })
+
+ describe('no valid optin code', () => {
+ beforeAll(async () => {
+ await mutate({ mutation: createUserMutation, variables: createUserVariables })
+ result = await mutate({
+ mutation: setPasswordMutation,
+ variables: { code: 'not valid', password: 'Aa12345_' },
+ })
+ })
+
+ afterAll(async () => {
+ await resetDB()
+ })
+
+ it('throws an error', () => {
+ expect(result).toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('Could not login with emailVerificationCode')],
+ }),
+ )
+ })
+ })
+ })
})
afterAll(async () => {
diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts
index 83e915bc7..bf4ef2a68 100644
--- a/backend/src/graphql/resolver/UserResolver.ts
+++ b/backend/src/graphql/resolver/UserResolver.ts
@@ -23,7 +23,7 @@ import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { sendResetPasswordEmail } from '../../mailer/sendResetPasswordEmail'
import { sendAccountActivationEmail } from '../../mailer/sendAccountActivationEmail'
import { LoginElopageBuysRepository } from '../../typeorm/repository/LoginElopageBuys'
-import { signIn } from '../../apis/KlicktippController'
+import { klicktippSignIn } from '../../apis/KlicktippController'
import { RIGHTS } from '../../auth/RIGHTS'
import { ServerUserRepository } from '../../typeorm/repository/ServerUser'
import { ROLE_ADMIN } from '../../auth/ROLES'
@@ -641,7 +641,12 @@ export class UserResolver {
// TODO do we always signUp the user? How to handle things with old users?
if (optInCode.emailOptInTypeId === EMAIL_OPT_IN_REGISTER) {
try {
- await signIn(loginUser.email, loginUser.language, loginUser.firstName, loginUser.lastName)
+ await klicktippSignIn(
+ loginUser.email,
+ loginUser.language,
+ loginUser.firstName,
+ loginUser.lastName,
+ )
} catch {
// TODO is this a problem?
// eslint-disable-next-line no-console
diff --git a/backend/src/middleware/klicktippMiddleware.ts b/backend/src/middleware/klicktippMiddleware.ts
index 69a74480d..d0fde8195 100644
--- a/backend/src/middleware/klicktippMiddleware.ts
+++ b/backend/src/middleware/klicktippMiddleware.ts
@@ -1,5 +1,5 @@
import { MiddlewareFn } from 'type-graphql'
-import { /* signIn, */ getKlickTippUser } from '../apis/KlicktippController'
+import { /* klicktippSignIn, */ getKlickTippUser } from '../apis/KlicktippController'
import { KlickTipp } from '../graphql/model/KlickTipp'
import CONFIG from '../config/index'
@@ -12,7 +12,7 @@ import CONFIG from '../config/index'
// // Do Something here before resolver is called
// const result = await next()
// // Do Something here after resolver is completed
-// await signIn(result.email, result.language, result.firstName, result.lastName)
+// await klicktippSignIn(result.email, result.language, result.firstName, result.lastName)
// return result
// }
diff --git a/database/src/index.ts b/database/src/index.ts
index 033a36d8b..fefdea217 100644
--- a/database/src/index.ts
+++ b/database/src/index.ts
@@ -7,6 +7,7 @@ import { CreateBibiBloxbergSeed } from './seeds/users/bibi-bloxberg.seed'
import { CreateRaeuberHotzenplotzSeed } from './seeds/users/raeuber-hotzenplotz.seed'
import { CreateBobBaumeisterSeed } from './seeds/users/bob-baumeister.seed'
import { CreateGarrickOllivanderSeed } from './seeds/users/garrick-ollivander.seed'
+import { CreateUserSeed } from './seeds/create-user.seed'
import { DecayStartBlockSeed } from './seeds/decay-start-block.seed'
import { resetDB, pool, migration } from './helpers'
@@ -45,6 +46,10 @@ const run = async (command: string) => {
await runSeeder(CreateBibiBloxbergSeed)
await runSeeder(CreateRaeuberHotzenplotzSeed)
await runSeeder(CreateBobBaumeisterSeed)
+ // eslint-disable-next-line prefer-spread
+ Array.apply(null, Array(96)).forEach(async () => {
+ await runSeeder(CreateUserSeed)
+ })
await runSeeder(CreateGarrickOllivanderSeed)
break
default:
diff --git a/database/src/seeds/create-user.seed.ts b/database/src/seeds/create-user.seed.ts
index ca3a182c4..69488a790 100644
--- a/database/src/seeds/create-user.seed.ts
+++ b/database/src/seeds/create-user.seed.ts
@@ -1,11 +1,8 @@
import { Factory, Seeder } from 'typeorm-seeding'
-import { User } from '../../entity/User'
-// import { LoginUser } from '../../entity/LoginUser'
+import { userSeeder } from './helpers/user-helpers'
export class CreateUserSeed implements Seeder {
public async run(factory: Factory): Promise {
- // const loginUser = await factory(LoginUser)().make()
- // console.log(loginUser.email)
- await factory(User)().create()
+ await userSeeder(factory, {})
}
}
diff --git a/database/src/seeds/helpers/user-helpers.ts b/database/src/seeds/helpers/user-helpers.ts
index bd46ecdee..e1e33a94b 100644
--- a/database/src/seeds/helpers/user-helpers.ts
+++ b/database/src/seeds/helpers/user-helpers.ts
@@ -27,6 +27,7 @@ import { Factory } from 'typeorm-seeding'
export const userSeeder = async (factory: Factory, userData: UserInterface): Promise => {
const user = await factory(User)(createUserContext(userData)).create()
+ if (!userData.email) userData.email = user.email
const loginUser = await factory(LoginUser)(createLoginUserContext(userData)).create()
await factory(LoginUserBackup)(createLoginUserBackupContext(userData, loginUser)).create()