mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into send-form-border-radius
This commit is contained in:
commit
18f3b27c86
@ -10,6 +10,7 @@ const authLink = new ApolloLink((operation, forward) => {
|
|||||||
operation.setContext({
|
operation.setContext({
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
|
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
|
||||||
|
clientRequestTime: new Date().toString(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return forward(operation).map((response) => {
|
return forward(operation).map((response) => {
|
||||||
|
|||||||
@ -94,6 +94,7 @@ describe('apolloProvider', () => {
|
|||||||
expect(setContextMock).toBeCalledWith({
|
expect(setContextMock).toBeCalledWith({
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: 'Bearer some-token',
|
Authorization: 'Bearer some-token',
|
||||||
|
clientRequestTime: expect.any(String),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -109,6 +110,7 @@ describe('apolloProvider', () => {
|
|||||||
expect(setContextMock).toBeCalledWith({
|
expect(setContextMock).toBeCalledWith({
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: '',
|
Authorization: '',
|
||||||
|
clientRequestTime: expect.any(String),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
CONFIG_VERSION=v9.2022-07-07
|
CONFIG_VERSION=v10.2022-09-20
|
||||||
|
|
||||||
# Server
|
# Server
|
||||||
PORT=4000
|
PORT=4000
|
||||||
@ -37,6 +37,8 @@ LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
|||||||
|
|
||||||
# EMail
|
# EMail
|
||||||
EMAIL=false
|
EMAIL=false
|
||||||
|
EMAIL_TEST_MODUS=false
|
||||||
|
EMAIL_TEST_RECEIVER=stage1@gradido.net
|
||||||
EMAIL_USERNAME=gradido_email
|
EMAIL_USERNAME=gradido_email
|
||||||
EMAIL_SENDER=info@gradido.net
|
EMAIL_SENDER=info@gradido.net
|
||||||
EMAIL_PASSWORD=xxx
|
EMAIL_PASSWORD=xxx
|
||||||
|
|||||||
@ -36,6 +36,8 @@ LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
|||||||
|
|
||||||
# EMail
|
# EMail
|
||||||
EMAIL=$EMAIL
|
EMAIL=$EMAIL
|
||||||
|
EMAIL_TEST_MODUS=$EMAIL_TEST_MODUS
|
||||||
|
EMAIL_TEST_RECEIVER=$EMAIL_TEST_RECEIVER
|
||||||
EMAIL_USERNAME=$EMAIL_USERNAME
|
EMAIL_USERNAME=$EMAIL_USERNAME
|
||||||
EMAIL_SENDER=$EMAIL_SENDER
|
EMAIL_SENDER=$EMAIL_SENDER
|
||||||
EMAIL_PASSWORD=$EMAIL_PASSWORD
|
EMAIL_PASSWORD=$EMAIL_PASSWORD
|
||||||
|
|||||||
@ -10,14 +10,14 @@ Decimal.set({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const constants = {
|
const constants = {
|
||||||
DB_VERSION: '0048-add_is_moderator_to_contribution_messages',
|
DB_VERSION: '0049-add_user_contacts_table',
|
||||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||||
LOG4JS_CONFIG: 'log4js-config.json',
|
LOG4JS_CONFIG: 'log4js-config.json',
|
||||||
// default log level on production should be info
|
// default log level on production should be info
|
||||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||||
CONFIG_VERSION: {
|
CONFIG_VERSION: {
|
||||||
DEFAULT: 'DEFAULT',
|
DEFAULT: 'DEFAULT',
|
||||||
EXPECTED: 'v9.2022-07-07',
|
EXPECTED: 'v10.2022-09-20',
|
||||||
CURRENT: '',
|
CURRENT: '',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -67,6 +67,8 @@ const loginServer = {
|
|||||||
|
|
||||||
const email = {
|
const email = {
|
||||||
EMAIL: process.env.EMAIL === 'true' || false,
|
EMAIL: process.env.EMAIL === 'true' || false,
|
||||||
|
EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || 'false',
|
||||||
|
EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'stage1@gradido.net',
|
||||||
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
|
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
|
||||||
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
|
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
|
||||||
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx',
|
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx',
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export class EventRegister extends EventBasicUserId {}
|
|||||||
export class EventRedeemRegister extends EventBasicRedeem {}
|
export class EventRedeemRegister extends EventBasicRedeem {}
|
||||||
export class EventInactiveAccount extends EventBasicUserId {}
|
export class EventInactiveAccount extends EventBasicUserId {}
|
||||||
export class EventSendConfirmationEmail extends EventBasicUserId {}
|
export class EventSendConfirmationEmail extends EventBasicUserId {}
|
||||||
|
export class EventSendAccountMultiRegistrationEmail extends EventBasicUserId {}
|
||||||
export class EventConfirmationEmail extends EventBasicUserId {}
|
export class EventConfirmationEmail extends EventBasicUserId {}
|
||||||
export class EventRegisterEmailKlicktipp extends EventBasicUserId {}
|
export class EventRegisterEmailKlicktipp extends EventBasicUserId {}
|
||||||
export class EventLogin extends EventBasicUserId {}
|
export class EventLogin extends EventBasicUserId {}
|
||||||
@ -113,6 +114,15 @@ export class Event {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setEventSendAccountMultiRegistrationEmail(
|
||||||
|
ev: EventSendAccountMultiRegistrationEmail,
|
||||||
|
): Event {
|
||||||
|
this.setByBasicUser(ev.userId)
|
||||||
|
this.type = EventProtocolType.SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
public setEventConfirmationEmail(ev: EventConfirmationEmail): Event {
|
public setEventConfirmationEmail(ev: EventConfirmationEmail): Event {
|
||||||
this.setByBasicUser(ev.userId)
|
this.setByBasicUser(ev.userId)
|
||||||
this.type = EventProtocolType.CONFIRM_EMAIL
|
this.type = EventProtocolType.CONFIRM_EMAIL
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export enum EventProtocolType {
|
|||||||
REDEEM_REGISTER = 'REDEEM_REGISTER',
|
REDEEM_REGISTER = 'REDEEM_REGISTER',
|
||||||
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
|
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
|
||||||
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
|
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
|
||||||
|
SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL',
|
||||||
CONFIRM_EMAIL = 'CONFIRM_EMAIL',
|
CONFIRM_EMAIL = 'CONFIRM_EMAIL',
|
||||||
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
|
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
|
||||||
LOGIN = 'LOGIN',
|
LOGIN = 'LOGIN',
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
|
|||||||
|
|
||||||
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
|
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
|
||||||
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
|
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
|
||||||
const userRepository = await getCustomRepository(UserRepository)
|
const userRepository = getCustomRepository(UserRepository)
|
||||||
try {
|
try {
|
||||||
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
||||||
context.user = user
|
context.user = user
|
||||||
|
|||||||
11
backend/src/graphql/enum/UserContactType.ts
Normal file
11
backend/src/graphql/enum/UserContactType.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { registerEnumType } from 'type-graphql'
|
||||||
|
|
||||||
|
export enum UserContactType {
|
||||||
|
USER_CONTACT_EMAIL = 'EMAIL',
|
||||||
|
USER_CONTACT_PHONE = 'PHONE',
|
||||||
|
}
|
||||||
|
|
||||||
|
registerEnumType(UserContactType, {
|
||||||
|
name: 'UserContactType', // this one is mandatory
|
||||||
|
description: 'Type of the user contact', // this one is optional
|
||||||
|
})
|
||||||
@ -13,7 +13,7 @@ export class UnconfirmedContribution {
|
|||||||
this.date = contribution.contributionDate
|
this.date = contribution.contributionDate
|
||||||
this.firstName = user ? user.firstName : ''
|
this.firstName = user ? user.firstName : ''
|
||||||
this.lastName = user ? user.lastName : ''
|
this.lastName = user ? user.lastName : ''
|
||||||
this.email = user ? user.email : ''
|
this.email = user ? user.emailContact.email : ''
|
||||||
this.moderator = contribution.moderatorId
|
this.moderator = contribution.moderatorId
|
||||||
this.creation = creations
|
this.creation = creations
|
||||||
this.state = contribution.contributionStatus
|
this.state = contribution.contributionStatus
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { KlickTipp } from './KlickTipp'
|
|||||||
import { User as dbUser } from '@entity/User'
|
import { User as dbUser } from '@entity/User'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { FULL_CREATION_AVAILABLE } from '../resolver/const/const'
|
import { FULL_CREATION_AVAILABLE } from '../resolver/const/const'
|
||||||
|
import { UserContact } from './UserContact'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class User {
|
export class User {
|
||||||
@ -10,12 +11,16 @@ export class User {
|
|||||||
this.id = user.id
|
this.id = user.id
|
||||||
this.gradidoID = user.gradidoID
|
this.gradidoID = user.gradidoID
|
||||||
this.alias = user.alias
|
this.alias = user.alias
|
||||||
this.email = user.email
|
this.emailId = user.emailId
|
||||||
|
if (user.emailContact) {
|
||||||
|
this.email = user.emailContact.email
|
||||||
|
this.emailContact = new UserContact(user.emailContact)
|
||||||
|
this.emailChecked = user.emailContact.emailChecked
|
||||||
|
}
|
||||||
this.firstName = user.firstName
|
this.firstName = user.firstName
|
||||||
this.lastName = user.lastName
|
this.lastName = user.lastName
|
||||||
this.deletedAt = user.deletedAt
|
this.deletedAt = user.deletedAt
|
||||||
this.createdAt = user.createdAt
|
this.createdAt = user.createdAt
|
||||||
this.emailChecked = user.emailChecked
|
|
||||||
this.language = user.language
|
this.language = user.language
|
||||||
this.publisherId = user.publisherId
|
this.publisherId = user.publisherId
|
||||||
this.isAdmin = user.isAdmin
|
this.isAdmin = user.isAdmin
|
||||||
@ -34,12 +39,18 @@ export class User {
|
|||||||
gradidoID: string
|
gradidoID: string
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
alias: string
|
alias?: string
|
||||||
|
|
||||||
|
@Field(() => Number, { nullable: true })
|
||||||
|
emailId: number | null
|
||||||
|
|
||||||
// TODO privacy issue here
|
// TODO privacy issue here
|
||||||
@Field(() => String)
|
@Field(() => String, { nullable: true })
|
||||||
email: string
|
email: string
|
||||||
|
|
||||||
|
@Field(() => UserContact)
|
||||||
|
emailContact: UserContact
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
firstName: string | null
|
firstName: string | null
|
||||||
|
|
||||||
|
|||||||
@ -6,11 +6,11 @@ import { User } from '@entity/User'
|
|||||||
export class UserAdmin {
|
export class UserAdmin {
|
||||||
constructor(user: User, creation: Decimal[], hasElopage: boolean, emailConfirmationSend: string) {
|
constructor(user: User, creation: Decimal[], hasElopage: boolean, emailConfirmationSend: string) {
|
||||||
this.userId = user.id
|
this.userId = user.id
|
||||||
this.email = user.email
|
this.email = user.emailContact.email
|
||||||
this.firstName = user.firstName
|
this.firstName = user.firstName
|
||||||
this.lastName = user.lastName
|
this.lastName = user.lastName
|
||||||
this.creation = creation
|
this.creation = creation
|
||||||
this.emailChecked = user.emailChecked
|
this.emailChecked = user.emailContact.emailChecked
|
||||||
this.hasElopage = hasElopage
|
this.hasElopage = hasElopage
|
||||||
this.deletedAt = user.deletedAt
|
this.deletedAt = user.deletedAt
|
||||||
this.emailConfirmationSend = emailConfirmationSend
|
this.emailConfirmationSend = emailConfirmationSend
|
||||||
|
|||||||
56
backend/src/graphql/model/UserContact.ts
Normal file
56
backend/src/graphql/model/UserContact.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { ObjectType, Field } from 'type-graphql'
|
||||||
|
import { UserContact as dbUserContact } from '@entity/UserContact'
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class UserContact {
|
||||||
|
constructor(userContact: dbUserContact) {
|
||||||
|
this.id = userContact.id
|
||||||
|
this.type = userContact.type
|
||||||
|
this.userId = userContact.userId
|
||||||
|
this.email = userContact.email
|
||||||
|
// this.emailVerificationCode = userContact.emailVerificationCode
|
||||||
|
this.emailOptInTypeId = userContact.emailOptInTypeId
|
||||||
|
this.emailResendCount = userContact.emailResendCount
|
||||||
|
this.emailChecked = userContact.emailChecked
|
||||||
|
this.phone = userContact.phone
|
||||||
|
this.createdAt = userContact.createdAt
|
||||||
|
this.updatedAt = userContact.updatedAt
|
||||||
|
this.deletedAt = userContact.deletedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
type: string
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
userId: number
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
email: string
|
||||||
|
|
||||||
|
// @Field(() => BigInt, { nullable: true })
|
||||||
|
// emailVerificationCode: BigInt | null
|
||||||
|
|
||||||
|
@Field(() => Number, { nullable: true })
|
||||||
|
emailOptInTypeId: number | null
|
||||||
|
|
||||||
|
@Field(() => Number, { nullable: true })
|
||||||
|
emailResendCount: number | null
|
||||||
|
|
||||||
|
@Field(() => Boolean)
|
||||||
|
emailChecked: boolean
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
phone: string | null
|
||||||
|
|
||||||
|
@Field(() => Date)
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@Field(() => Date, { nullable: true })
|
||||||
|
updatedAt: Date | null
|
||||||
|
|
||||||
|
@Field(() => Date, { nullable: true })
|
||||||
|
deletedAt: Date | null
|
||||||
|
}
|
||||||
@ -1126,7 +1126,9 @@ describe('AdminResolver', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('Could not find user with email: bob@baumeister.de')],
|
errors: [
|
||||||
|
new GraphQLError('Could not find UserContact with email: bob@baumeister.de'),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -1516,6 +1518,7 @@ describe('AdminResolver', () => {
|
|||||||
)
|
)
|
||||||
await expect(r2).resolves.toEqual(
|
await expect(r2).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
// data: { confirmContribution: true },
|
||||||
errors: [new GraphQLError('Creation was not successful.')],
|
errors: [new GraphQLError('Creation was not successful.')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -4,8 +4,6 @@ import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx, Int } from 'type
|
|||||||
import {
|
import {
|
||||||
getCustomRepository,
|
getCustomRepository,
|
||||||
IsNull,
|
IsNull,
|
||||||
Not,
|
|
||||||
ObjectLiteral,
|
|
||||||
getConnection,
|
getConnection,
|
||||||
In,
|
In,
|
||||||
MoreThan,
|
MoreThan,
|
||||||
@ -32,7 +30,6 @@ import { TransactionRepository } from '@repository/Transaction'
|
|||||||
import { calculateDecay } from '@/util/decay'
|
import { calculateDecay } from '@/util/decay'
|
||||||
import { Contribution } from '@entity/Contribution'
|
import { Contribution } from '@entity/Contribution'
|
||||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
|
||||||
import { User as dbUser } from '@entity/User'
|
import { User as dbUser } from '@entity/User'
|
||||||
import { User } from '@model/User'
|
import { User } from '@model/User'
|
||||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||||
@ -44,7 +41,7 @@ import Paginated from '@arg/Paginated'
|
|||||||
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
|
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
|
||||||
import { Order } from '@enum/Order'
|
import { Order } from '@enum/Order'
|
||||||
import { communityUser } from '@/util/communityUser'
|
import { communityUser } from '@/util/communityUser'
|
||||||
import { checkOptInCode, activationLink, printTimeDuration } from './UserResolver'
|
import { findUserByEmail, activationLink, printTimeDuration } from './UserResolver'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
@ -62,6 +59,7 @@ import {
|
|||||||
MEMO_MAX_CHARS,
|
MEMO_MAX_CHARS,
|
||||||
MEMO_MIN_CHARS,
|
MEMO_MIN_CHARS,
|
||||||
} from './const/const'
|
} from './const/const'
|
||||||
|
import { UserContact } from '@entity/UserContact'
|
||||||
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
|
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
|
||||||
import ContributionMessageArgs from '@arg/ContributionMessageArgs'
|
import ContributionMessageArgs from '@arg/ContributionMessageArgs'
|
||||||
import { ContributionMessageType } from '@enum/MessageType'
|
import { ContributionMessageType } from '@enum/MessageType'
|
||||||
@ -81,24 +79,12 @@ export class AdminResolver {
|
|||||||
{ searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs,
|
{ searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs,
|
||||||
): Promise<SearchUsersResult> {
|
): Promise<SearchUsersResult> {
|
||||||
const userRepository = getCustomRepository(UserRepository)
|
const userRepository = getCustomRepository(UserRepository)
|
||||||
|
|
||||||
const filterCriteria: ObjectLiteral[] = []
|
|
||||||
if (filters) {
|
|
||||||
if (filters.byActivated !== null) {
|
|
||||||
filterCriteria.push({ emailChecked: filters.byActivated })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.byDeleted !== null) {
|
|
||||||
filterCriteria.push({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userFields = [
|
const userFields = [
|
||||||
'id',
|
'id',
|
||||||
'firstName',
|
'firstName',
|
||||||
'lastName',
|
'lastName',
|
||||||
'email',
|
'emailId',
|
||||||
'emailChecked',
|
'emailContact',
|
||||||
'deletedAt',
|
'deletedAt',
|
||||||
'isAdmin',
|
'isAdmin',
|
||||||
]
|
]
|
||||||
@ -107,7 +93,7 @@ export class AdminResolver {
|
|||||||
return 'user.' + fieldName
|
return 'user.' + fieldName
|
||||||
}),
|
}),
|
||||||
searchText,
|
searchText,
|
||||||
filterCriteria,
|
filters,
|
||||||
currentPage,
|
currentPage,
|
||||||
pageSize,
|
pageSize,
|
||||||
)
|
)
|
||||||
@ -124,32 +110,18 @@ export class AdminResolver {
|
|||||||
const adminUsers = await Promise.all(
|
const adminUsers = await Promise.all(
|
||||||
users.map(async (user) => {
|
users.map(async (user) => {
|
||||||
let emailConfirmationSend = ''
|
let emailConfirmationSend = ''
|
||||||
if (!user.emailChecked) {
|
if (!user.emailContact.emailChecked) {
|
||||||
const emailOptIn = await LoginEmailOptIn.findOne(
|
if (user.emailContact.updatedAt) {
|
||||||
{
|
emailConfirmationSend = user.emailContact.updatedAt.toISOString()
|
||||||
userId: user.id,
|
} else {
|
||||||
},
|
emailConfirmationSend = user.emailContact.createdAt.toISOString()
|
||||||
{
|
|
||||||
order: {
|
|
||||||
updatedAt: 'DESC',
|
|
||||||
createdAt: 'DESC',
|
|
||||||
},
|
|
||||||
select: ['updatedAt', 'createdAt'],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (emailOptIn) {
|
|
||||||
if (emailOptIn.updatedAt) {
|
|
||||||
emailConfirmationSend = emailOptIn.updatedAt.toISOString()
|
|
||||||
} else {
|
|
||||||
emailConfirmationSend = emailOptIn.createdAt.toISOString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const userCreations = creations.find((c) => c.id === user.id)
|
const userCreations = creations.find((c) => c.id === user.id)
|
||||||
const adminUser = new UserAdmin(
|
const adminUser = new UserAdmin(
|
||||||
user,
|
user,
|
||||||
userCreations ? userCreations.creations : FULL_CREATION_AVAILABLE,
|
userCreations ? userCreations.creations : FULL_CREATION_AVAILABLE,
|
||||||
await hasElopageBuys(user.email),
|
await hasElopageBuys(user.emailContact.email),
|
||||||
emailConfirmationSend,
|
emailConfirmationSend,
|
||||||
)
|
)
|
||||||
return adminUser
|
return adminUser
|
||||||
@ -245,24 +217,39 @@ export class AdminResolver {
|
|||||||
@Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs,
|
@Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs,
|
||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<Decimal[]> {
|
): Promise<Decimal[]> {
|
||||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
logger.info(
|
||||||
if (!user) {
|
`adminCreateContribution(email=${email}, amount=${amount}, memo=${memo}, creationDate=${creationDate})`,
|
||||||
|
)
|
||||||
|
const emailContact = await UserContact.findOne({
|
||||||
|
where: { email },
|
||||||
|
withDeleted: true,
|
||||||
|
relations: ['user'],
|
||||||
|
})
|
||||||
|
if (!emailContact) {
|
||||||
|
logger.error(`Could not find user with email: ${email}`)
|
||||||
throw new Error(`Could not find user with email: ${email}`)
|
throw new Error(`Could not find user with email: ${email}`)
|
||||||
}
|
}
|
||||||
if (user.deletedAt) {
|
if (emailContact.deletedAt) {
|
||||||
|
logger.error('This emailContact was deleted. Cannot create a contribution.')
|
||||||
|
throw new Error('This emailContact was deleted. Cannot create a contribution.')
|
||||||
|
}
|
||||||
|
if (emailContact.user.deletedAt) {
|
||||||
|
logger.error('This user was deleted. Cannot create a contribution.')
|
||||||
throw new Error('This user was deleted. Cannot create a contribution.')
|
throw new Error('This user was deleted. Cannot create a contribution.')
|
||||||
}
|
}
|
||||||
if (!user.emailChecked) {
|
if (!emailContact.emailChecked) {
|
||||||
|
logger.error('Contribution could not be saved, Email is not activated')
|
||||||
throw new Error('Contribution could not be saved, Email is not activated')
|
throw new Error('Contribution could not be saved, Email is not activated')
|
||||||
}
|
}
|
||||||
const moderator = getUser(context)
|
const moderator = getUser(context)
|
||||||
logger.trace('moderator: ', moderator.id)
|
logger.trace('moderator: ', moderator.id)
|
||||||
const creations = await getUserCreation(user.id)
|
const creations = await getUserCreation(emailContact.userId)
|
||||||
logger.trace('creations', creations)
|
logger.trace('creations:', creations)
|
||||||
const creationDateObj = new Date(creationDate)
|
const creationDateObj = new Date(creationDate)
|
||||||
|
logger.trace('creationDateObj:', creationDateObj)
|
||||||
validateContribution(creations, amount, creationDateObj)
|
validateContribution(creations, amount, creationDateObj)
|
||||||
const contribution = Contribution.create()
|
const contribution = Contribution.create()
|
||||||
contribution.userId = user.id
|
contribution.userId = emailContact.userId
|
||||||
contribution.amount = amount
|
contribution.amount = amount
|
||||||
contribution.createdAt = new Date()
|
contribution.createdAt = new Date()
|
||||||
contribution.contributionDate = creationDateObj
|
contribution.contributionDate = creationDateObj
|
||||||
@ -273,7 +260,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
logger.trace('contribution to save', contribution)
|
logger.trace('contribution to save', contribution)
|
||||||
await Contribution.save(contribution)
|
await Contribution.save(contribution)
|
||||||
return getUserCreation(user.id)
|
return getUserCreation(emailContact.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTIONS])
|
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTIONS])
|
||||||
@ -309,11 +296,22 @@ export class AdminResolver {
|
|||||||
@Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs,
|
@Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs,
|
||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<AdminUpdateContribution> {
|
): Promise<AdminUpdateContribution> {
|
||||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
const emailContact = await UserContact.findOne({
|
||||||
|
where: { email },
|
||||||
|
withDeleted: true,
|
||||||
|
relations: ['user'],
|
||||||
|
})
|
||||||
|
if (!emailContact) {
|
||||||
|
logger.error(`Could not find UserContact with email: ${email}`)
|
||||||
|
throw new Error(`Could not find UserContact with email: ${email}`)
|
||||||
|
}
|
||||||
|
const user = emailContact.user
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`Could not find user with email: ${email}`)
|
logger.error(`Could not find User to emailContact: ${email}`)
|
||||||
|
throw new Error(`Could not find User to emailContact: ${email}`)
|
||||||
}
|
}
|
||||||
if (user.deletedAt) {
|
if (user.deletedAt) {
|
||||||
|
logger.error(`User was deleted (${email})`)
|
||||||
throw new Error(`User was deleted (${email})`)
|
throw new Error(`User was deleted (${email})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,14 +322,17 @@ export class AdminResolver {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!contributionToUpdate) {
|
if (!contributionToUpdate) {
|
||||||
|
logger.error('No contribution found to given id.')
|
||||||
throw new Error('No contribution found to given id.')
|
throw new Error('No contribution found to given id.')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contributionToUpdate.userId !== user.id) {
|
if (contributionToUpdate.userId !== user.id) {
|
||||||
|
logger.error('user of the pending contribution and send user does not correspond')
|
||||||
throw new Error('user of the pending contribution and send user does not correspond')
|
throw new Error('user of the pending contribution and send user does not correspond')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contributionToUpdate.moderatorId === null) {
|
if (contributionToUpdate.moderatorId === null) {
|
||||||
|
logger.error('An admin is not allowed to update a user contribution.')
|
||||||
throw new Error('An admin is not allowed to update a user contribution.')
|
throw new Error('An admin is not allowed to update a user contribution.')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,7 +378,11 @@ export class AdminResolver {
|
|||||||
|
|
||||||
const userIds = contributions.map((p) => p.userId)
|
const userIds = contributions.map((p) => p.userId)
|
||||||
const userCreations = await getUserCreations(userIds)
|
const userCreations = await getUserCreations(userIds)
|
||||||
const users = await dbUser.find({ where: { id: In(userIds) }, withDeleted: true })
|
const users = await dbUser.find({
|
||||||
|
where: { id: In(userIds) },
|
||||||
|
withDeleted: true,
|
||||||
|
relations: ['emailContact'],
|
||||||
|
})
|
||||||
|
|
||||||
return contributions.map((contribution) => {
|
return contributions.map((contribution) => {
|
||||||
const user = users.find((u) => u.id === contribution.userId)
|
const user = users.find((u) => u.id === contribution.userId)
|
||||||
@ -396,6 +401,7 @@ export class AdminResolver {
|
|||||||
async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise<boolean> {
|
async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise<boolean> {
|
||||||
const contribution = await Contribution.findOne(id)
|
const contribution = await Contribution.findOne(id)
|
||||||
if (!contribution) {
|
if (!contribution) {
|
||||||
|
logger.error(`Contribution not found for given id: ${id}`)
|
||||||
throw new Error('Contribution not found for given id.')
|
throw new Error('Contribution not found for given id.')
|
||||||
}
|
}
|
||||||
contribution.contributionStatus = ContributionStatus.DELETED
|
contribution.contributionStatus = ContributionStatus.DELETED
|
||||||
@ -412,15 +418,22 @@ export class AdminResolver {
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const contribution = await Contribution.findOne(id)
|
const contribution = await Contribution.findOne(id)
|
||||||
if (!contribution) {
|
if (!contribution) {
|
||||||
|
logger.error(`Contribution not found for given id: ${id}`)
|
||||||
throw new Error('Contribution not found to given id.')
|
throw new Error('Contribution not found to given id.')
|
||||||
}
|
}
|
||||||
const moderatorUser = getUser(context)
|
const moderatorUser = getUser(context)
|
||||||
if (moderatorUser.id === contribution.userId)
|
if (moderatorUser.id === contribution.userId) {
|
||||||
|
logger.error('Moderator can not confirm own contribution')
|
||||||
throw new Error('Moderator can not confirm own contribution')
|
throw new Error('Moderator can not confirm own contribution')
|
||||||
|
}
|
||||||
const user = await dbUser.findOneOrFail({ id: contribution.userId }, { withDeleted: true })
|
const user = await dbUser.findOneOrFail(
|
||||||
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a contribution.')
|
{ id: contribution.userId },
|
||||||
|
{ withDeleted: true, relations: ['emailContact'] },
|
||||||
|
)
|
||||||
|
if (user.deletedAt) {
|
||||||
|
logger.error('This user was deleted. Cannot confirm a contribution.')
|
||||||
|
throw new Error('This user was deleted. Cannot confirm a contribution.')
|
||||||
|
}
|
||||||
const creations = await getUserCreation(contribution.userId, false)
|
const creations = await getUserCreation(contribution.userId, false)
|
||||||
validateContribution(creations, contribution.amount, contribution.contributionDate)
|
validateContribution(creations, contribution.amount, contribution.contributionDate)
|
||||||
|
|
||||||
@ -428,7 +441,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
|
||||||
try {
|
try {
|
||||||
const lastTransaction = await queryRunner.manager
|
const lastTransaction = await queryRunner.manager
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
@ -477,7 +490,7 @@ export class AdminResolver {
|
|||||||
senderLastName: moderatorUser.lastName,
|
senderLastName: moderatorUser.lastName,
|
||||||
recipientFirstName: user.firstName,
|
recipientFirstName: user.firstName,
|
||||||
recipientLastName: user.lastName,
|
recipientLastName: user.lastName,
|
||||||
recipientEmail: user.email,
|
recipientEmail: user.emailContact.email,
|
||||||
contributionMemo: contribution.memo,
|
contributionMemo: contribution.memo,
|
||||||
contributionAmount: contribution.amount,
|
contributionAmount: contribution.amount,
|
||||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||||
@ -517,32 +530,35 @@ export class AdminResolver {
|
|||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
|
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
const user = await dbUser.findOneOrFail({ email: email })
|
// const user = await dbUser.findOne({ id: emailContact.userId })
|
||||||
|
const user = await findUserByEmail(email)
|
||||||
// can be both types: REGISTER and RESET_PASSWORD
|
if (!user) {
|
||||||
let optInCode = await LoginEmailOptIn.findOne({
|
logger.error(`Could not find User to emailContact: ${email}`)
|
||||||
where: { userId: user.id },
|
throw new Error(`Could not find User to emailContact: ${email}`)
|
||||||
order: { updatedAt: 'DESC' },
|
}
|
||||||
})
|
if (user.deletedAt) {
|
||||||
|
logger.error(`User with emailContact: ${email} is deleted.`)
|
||||||
optInCode = await checkOptInCode(optInCode, user)
|
throw new Error(`User with emailContact: ${email} is deleted.`)
|
||||||
|
}
|
||||||
|
const emailContact = user.emailContact
|
||||||
|
if (emailContact.deletedAt) {
|
||||||
|
logger.error(`The emailContact: ${email} of htis User is deleted.`)
|
||||||
|
throw new Error(`The emailContact: ${email} of htis User is deleted.`)
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const emailSent = await sendAccountActivationEmail({
|
const emailSent = await sendAccountActivationEmail({
|
||||||
link: activationLink(optInCode),
|
link: activationLink(emailContact.emailVerificationCode),
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
email,
|
email,
|
||||||
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
|
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||||
})
|
})
|
||||||
|
|
||||||
/* uncomment this, when you need the activation link on the console
|
|
||||||
// In case EMails are disabled log the activation link for the user
|
// In case EMails are disabled log the activation link for the user
|
||||||
if (!emailSent) {
|
if (!emailSent) {
|
||||||
// eslint-disable-next-line no-console
|
logger.info(`Account confirmation link: ${activationLink}`)
|
||||||
console.log(`Account confirmation link: ${activationLink}`)
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -720,9 +736,12 @@ export class AdminResolver {
|
|||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<ContributionMessage> {
|
): Promise<ContributionMessage> {
|
||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
|
if (!user.emailContact) {
|
||||||
|
user.emailContact = await UserContact.findOneOrFail({ where: { id: user.emailId } })
|
||||||
|
}
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
const contributionMessage = DbContributionMessage.create()
|
const contributionMessage = DbContributionMessage.create()
|
||||||
try {
|
try {
|
||||||
const contribution = await Contribution.findOne({
|
const contribution = await Contribution.findOne({
|
||||||
@ -735,6 +754,11 @@ export class AdminResolver {
|
|||||||
if (contribution.userId === user.id) {
|
if (contribution.userId === user.id) {
|
||||||
throw new Error('Admin can not answer on own contribution')
|
throw new Error('Admin can not answer on own contribution')
|
||||||
}
|
}
|
||||||
|
if (!contribution.user.emailContact) {
|
||||||
|
contribution.user.emailContact = await UserContact.findOneOrFail({
|
||||||
|
where: { id: contribution.user.emailId },
|
||||||
|
})
|
||||||
|
}
|
||||||
contributionMessage.contributionId = contributionId
|
contributionMessage.contributionId = contributionId
|
||||||
contributionMessage.createdAt = new Date()
|
contributionMessage.createdAt = new Date()
|
||||||
contributionMessage.message = message
|
contributionMessage.message = message
|
||||||
@ -751,19 +775,19 @@ export class AdminResolver {
|
|||||||
contribution.contributionStatus = ContributionStatus.IN_PROGRESS
|
contribution.contributionStatus = ContributionStatus.IN_PROGRESS
|
||||||
await queryRunner.manager.update(Contribution, { id: contributionId }, contribution)
|
await queryRunner.manager.update(Contribution, { id: contributionId }, contribution)
|
||||||
}
|
}
|
||||||
await queryRunner.commitTransaction()
|
|
||||||
|
|
||||||
await sendAddedContributionMessageEmail({
|
await sendAddedContributionMessageEmail({
|
||||||
senderFirstName: user.firstName,
|
senderFirstName: user.firstName,
|
||||||
senderLastName: user.lastName,
|
senderLastName: user.lastName,
|
||||||
recipientFirstName: contribution.user.firstName,
|
recipientFirstName: contribution.user.firstName,
|
||||||
recipientLastName: contribution.user.lastName,
|
recipientLastName: contribution.user.lastName,
|
||||||
recipientEmail: contribution.user.email,
|
recipientEmail: contribution.user.emailContact.email,
|
||||||
senderEmail: user.email,
|
senderEmail: user.emailContact.email,
|
||||||
contributionMemo: contribution.memo,
|
contributionMemo: contribution.memo,
|
||||||
message,
|
message,
|
||||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||||
})
|
})
|
||||||
|
await queryRunner.commitTransaction()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
logger.error(`ContributionMessage was not successful: ${e}`)
|
logger.error(`ContributionMessage was not successful: ${e}`)
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export class ContributionMessageResolver {
|
|||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
const contributionMessage = DbContributionMessage.create()
|
const contributionMessage = DbContributionMessage.create()
|
||||||
try {
|
try {
|
||||||
const contribution = await Contribution.findOne({ id: contributionId })
|
const contribution = await Contribution.findOne({ id: contributionId })
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export class GdtResolver {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const resultGDT = await apiGet(
|
const resultGDT = await apiGet(
|
||||||
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.email}/${currentPage}/${pageSize}/${order}`,
|
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.emailContact.email}/${currentPage}/${pageSize}/${order}`,
|
||||||
)
|
)
|
||||||
if (!resultGDT.success) {
|
if (!resultGDT.success) {
|
||||||
throw new Error(resultGDT.data)
|
throw new Error(resultGDT.data)
|
||||||
@ -37,7 +37,7 @@ export class GdtResolver {
|
|||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
try {
|
try {
|
||||||
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
||||||
email: user.email,
|
email: user.emailContact.email,
|
||||||
})
|
})
|
||||||
if (!resultGDTSum.success) {
|
if (!resultGDTSum.success) {
|
||||||
throw new Error('Call not successful')
|
throw new Error('Call not successful')
|
||||||
|
|||||||
@ -178,7 +178,7 @@ export class TransactionLinkResolver {
|
|||||||
logger.info('redeem contribution link...')
|
logger.info('redeem contribution link...')
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('SERIALIZABLE')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
try {
|
try {
|
||||||
const contributionLink = await queryRunner.manager
|
const contributionLink = await queryRunner.manager
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
@ -283,7 +283,10 @@ export class TransactionLinkResolver {
|
|||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
||||||
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
const linkedUser = await dbUser.findOneOrFail(
|
||||||
|
{ id: transactionLink.userId },
|
||||||
|
{ relations: ['emailContact'] },
|
||||||
|
)
|
||||||
|
|
||||||
if (user.id === linkedUser.id) {
|
if (user.id === linkedUser.id) {
|
||||||
throw new Error('Cannot redeem own transaction link.')
|
throw new Error('Cannot redeem own transaction link.')
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import Decimal from 'decimal.js-light'
|
|||||||
|
|
||||||
import { BalanceResolver } from './BalanceResolver'
|
import { BalanceResolver } from './BalanceResolver'
|
||||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||||
|
import { findUserByEmail } from './UserResolver'
|
||||||
import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed'
|
import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed'
|
||||||
|
|
||||||
export const executeTransaction = async (
|
export const executeTransaction = async (
|
||||||
@ -79,7 +80,7 @@ export const executeTransaction = async (
|
|||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
logger.debug(`open Transaction to write...`)
|
logger.debug(`open Transaction to write...`)
|
||||||
try {
|
try {
|
||||||
// transaction
|
// transaction
|
||||||
@ -149,8 +150,8 @@ export const executeTransaction = async (
|
|||||||
senderLastName: sender.lastName,
|
senderLastName: sender.lastName,
|
||||||
recipientFirstName: recipient.firstName,
|
recipientFirstName: recipient.firstName,
|
||||||
recipientLastName: recipient.lastName,
|
recipientLastName: recipient.lastName,
|
||||||
email: recipient.email,
|
email: recipient.emailContact.email,
|
||||||
senderEmail: sender.email,
|
senderEmail: sender.emailContact.email,
|
||||||
amount,
|
amount,
|
||||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||||
})
|
})
|
||||||
@ -160,8 +161,8 @@ export const executeTransaction = async (
|
|||||||
senderLastName: recipient.lastName,
|
senderLastName: recipient.lastName,
|
||||||
recipientFirstName: sender.firstName,
|
recipientFirstName: sender.firstName,
|
||||||
recipientLastName: sender.lastName,
|
recipientLastName: sender.lastName,
|
||||||
email: sender.email,
|
email: sender.emailContact.email,
|
||||||
senderEmail: recipient.email,
|
senderEmail: recipient.emailContact.email,
|
||||||
amount,
|
amount,
|
||||||
memo,
|
memo,
|
||||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||||
@ -184,7 +185,7 @@ export class TransactionResolver {
|
|||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
|
|
||||||
logger.addContext('user', user.id)
|
logger.addContext('user', user.id)
|
||||||
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.email})`)
|
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`)
|
||||||
|
|
||||||
// find current balance
|
// find current balance
|
||||||
const lastTransaction = await dbTransaction.findOne(
|
const lastTransaction = await dbTransaction.findOne(
|
||||||
@ -306,16 +307,25 @@ export class TransactionResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validate recipient user
|
// validate recipient user
|
||||||
const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true })
|
const recipientUser = await findUserByEmail(email)
|
||||||
|
/*
|
||||||
|
const emailContact = await UserContact.findOne({ email }, { withDeleted: true })
|
||||||
|
if (!emailContact) {
|
||||||
|
logger.error(`Could not find UserContact with email: ${email}`)
|
||||||
|
throw new Error(`Could not find UserContact with email: ${email}`)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// const recipientUser = await dbUser.findOne({ id: emailContact.userId })
|
||||||
if (!recipientUser) {
|
if (!recipientUser) {
|
||||||
logger.error(`recipient not known: email=${email}`)
|
logger.error(`unknown recipient to UserContact: email=${email}`)
|
||||||
throw new Error('recipient not known')
|
throw new Error('unknown recipient')
|
||||||
}
|
}
|
||||||
if (recipientUser.deletedAt) {
|
if (recipientUser.deletedAt) {
|
||||||
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
|
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
|
||||||
throw new Error('The recipient account was deleted')
|
throw new Error('The recipient account was deleted')
|
||||||
}
|
}
|
||||||
if (!recipientUser.emailChecked) {
|
const emailContact = recipientUser.emailContact
|
||||||
|
if (!emailContact.emailChecked) {
|
||||||
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`)
|
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`)
|
||||||
throw new Error('The recipient account is not activated')
|
throw new Error('The recipient account is not activated')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
import { testEnvironment, headerPushMock, resetToken, cleanDB, resetEntity } from '@test/helpers'
|
import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers'
|
||||||
import { userFactory } from '@/seeds/factory/user'
|
import { userFactory } from '@/seeds/factory/user'
|
||||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||||
import {
|
import {
|
||||||
@ -14,7 +14,6 @@ import {
|
|||||||
} from '@/seeds/graphql/mutations'
|
} from '@/seeds/graphql/mutations'
|
||||||
import { login, logout, verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries'
|
import { login, logout, verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries'
|
||||||
import { GraphQLError } from 'graphql'
|
import { GraphQLError } from 'graphql'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
|
||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
@ -31,6 +30,9 @@ import { EventProtocol } from '@entity/EventProtocol'
|
|||||||
import { logger } from '@test/testSetup'
|
import { logger } from '@test/testSetup'
|
||||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||||
|
import { UserContact } from '@entity/UserContact'
|
||||||
|
import { OptInType } from '../enum/OptInType'
|
||||||
|
import { UserContactType } from '../enum/UserContactType'
|
||||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||||
|
|
||||||
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||||
@ -92,7 +94,7 @@ describe('UserResolver', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let result: any
|
let result: any
|
||||||
let emailOptIn: string
|
let emailVerificationCode: string
|
||||||
let user: User[]
|
let user: User[]
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@ -111,11 +113,11 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('valid input data', () => {
|
describe('valid input data', () => {
|
||||||
let loginEmailOptIn: LoginEmailOptIn[]
|
// let loginEmailOptIn: LoginEmailOptIn[]
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
user = await User.find()
|
user = await User.find({ relations: ['emailContact'] })
|
||||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
// loginEmailOptIn = await LoginEmailOptIn.find()
|
||||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
emailVerificationCode = user[0].emailContact.emailVerificationCode.toString()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('filling all tables', () => {
|
describe('filling all tables', () => {
|
||||||
@ -125,15 +127,16 @@ describe('UserResolver', () => {
|
|||||||
id: expect.any(Number),
|
id: expect.any(Number),
|
||||||
gradidoID: expect.any(String),
|
gradidoID: expect.any(String),
|
||||||
alias: null,
|
alias: null,
|
||||||
email: 'peter@lustig.de',
|
emailContact: expect.any(UserContact), // 'peter@lustig.de',
|
||||||
|
emailId: expect.any(Number),
|
||||||
firstName: 'Peter',
|
firstName: 'Peter',
|
||||||
lastName: 'Lustig',
|
lastName: 'Lustig',
|
||||||
password: '0',
|
password: '0',
|
||||||
pubKey: null,
|
pubKey: null,
|
||||||
privKey: null,
|
privKey: null,
|
||||||
emailHash: expect.any(Buffer),
|
// emailHash: expect.any(Buffer),
|
||||||
createdAt: expect.any(Date),
|
createdAt: expect.any(Date),
|
||||||
emailChecked: false,
|
// emailChecked: false,
|
||||||
passphrase: expect.any(String),
|
passphrase: expect.any(String),
|
||||||
language: 'de',
|
language: 'de',
|
||||||
isAdmin: null,
|
isAdmin: null,
|
||||||
@ -149,18 +152,21 @@ describe('UserResolver', () => {
|
|||||||
expect(verUUID).toEqual(4)
|
expect(verUUID).toEqual(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates an email optin', () => {
|
it('creates an email contact', () => {
|
||||||
expect(loginEmailOptIn).toEqual([
|
expect(user[0].emailContact).toEqual({
|
||||||
{
|
id: expect.any(Number),
|
||||||
id: expect.any(Number),
|
type: UserContactType.USER_CONTACT_EMAIL,
|
||||||
userId: user[0].id,
|
userId: user[0].id,
|
||||||
verificationCode: expect.any(String),
|
email: 'peter@lustig.de',
|
||||||
emailOptInTypeId: 1,
|
emailChecked: false,
|
||||||
createdAt: expect.any(Date),
|
emailVerificationCode: expect.any(String),
|
||||||
resendCount: 0,
|
emailOptInTypeId: OptInType.EMAIL_OPT_IN_REGISTER,
|
||||||
updatedAt: expect.any(Date),
|
emailResendCount: 0,
|
||||||
},
|
phone: null,
|
||||||
])
|
createdAt: expect.any(Date),
|
||||||
|
deletedAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -169,7 +175,7 @@ describe('UserResolver', () => {
|
|||||||
it('sends an account activation email', () => {
|
it('sends an account activation email', () => {
|
||||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||||
/{optin}/g,
|
/{optin}/g,
|
||||||
emailOptIn,
|
emailVerificationCode,
|
||||||
).replace(/{code}/g, '')
|
).replace(/{code}/g, '')
|
||||||
expect(sendAccountActivationEmail).toBeCalledWith({
|
expect(sendAccountActivationEmail).toBeCalledWith({
|
||||||
link: activationLink,
|
link: activationLink,
|
||||||
@ -227,13 +233,13 @@ describe('UserResolver', () => {
|
|||||||
mutation: createUser,
|
mutation: createUser,
|
||||||
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'it' },
|
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'it' },
|
||||||
})
|
})
|
||||||
await expect(User.find()).resolves.toEqual(
|
await expect(
|
||||||
expect.arrayContaining([
|
UserContact.findOne({ email: 'bibi@bloxberg.de' }, { relations: ['user'] }),
|
||||||
expect.objectContaining({
|
).resolves.toEqual(
|
||||||
email: 'bibi@bloxberg.de',
|
expect.objectContaining({
|
||||||
language: 'de',
|
email: 'bibi@bloxberg.de',
|
||||||
}),
|
user: expect.objectContaining({ language: 'de' }),
|
||||||
]),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -244,10 +250,12 @@ describe('UserResolver', () => {
|
|||||||
mutation: createUser,
|
mutation: createUser,
|
||||||
variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined },
|
variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined },
|
||||||
})
|
})
|
||||||
await expect(User.find()).resolves.toEqual(
|
await expect(User.find({ relations: ['emailContact'] })).resolves.toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
email: 'raeuber@hotzenplotz.de',
|
emailContact: expect.objectContaining({
|
||||||
|
email: 'raeuber@hotzenplotz.de',
|
||||||
|
}),
|
||||||
publisherId: null,
|
publisherId: null,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
@ -264,7 +272,7 @@ describe('UserResolver', () => {
|
|||||||
// activate account of admin Peter Lustig
|
// activate account of admin Peter Lustig
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: setPassword,
|
mutation: setPassword,
|
||||||
variables: { code: emailOptIn, password: 'Aa12345_' },
|
variables: { code: emailVerificationCode, password: 'Aa12345_' },
|
||||||
})
|
})
|
||||||
|
|
||||||
// make Peter Lustig Admin
|
// make Peter Lustig Admin
|
||||||
@ -298,9 +306,13 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('sets the contribution link id', async () => {
|
it('sets the contribution link id', async () => {
|
||||||
await expect(User.findOne({ email: 'ein@besucher.de' })).resolves.toEqual(
|
await expect(
|
||||||
|
UserContact.findOne({ email: 'ein@besucher.de' }, { relations: ['user'] }),
|
||||||
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
contributionLinkId: link.id,
|
user: expect.objectContaining({
|
||||||
|
contributionLinkId: link.id,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -389,8 +401,12 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('sets the referrer id to bob baumeister id', async () => {
|
it('sets the referrer id to bob baumeister id', async () => {
|
||||||
await expect(User.findOne({ email: 'which@ever.de' })).resolves.toEqual(
|
await expect(
|
||||||
expect.objectContaining({ referrerId: bob.data.login.id }),
|
UserContact.findOne({ email: 'which@ever.de' }, { relations: ['user'] }),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
user: expect.objectContaining({ referrerId: bob.data.login.id }),
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -413,7 +429,7 @@ describe('UserResolver', () => {
|
|||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
amount: 19.99,
|
amount: 19.99,
|
||||||
memo: `Kein Trick, keine Zauberrei,
|
memo: `Kein Trick, keine Zauberrei,
|
||||||
bei Gradidio sei dabei!`,
|
bei Gradidio sei dabei!`,
|
||||||
})
|
})
|
||||||
const transactionLink = await TransactionLink.findOneOrFail()
|
const transactionLink = await TransactionLink.findOneOrFail()
|
||||||
resetToken()
|
resetToken()
|
||||||
@ -444,20 +460,23 @@ bei Gradidio sei dabei!`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
let result: any
|
let result: any
|
||||||
let emailOptIn: string
|
let emailVerificationCode: string
|
||||||
|
|
||||||
describe('valid optin code and valid password', () => {
|
describe('valid optin code and valid password', () => {
|
||||||
let newUser: any
|
let newUser: User
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await mutate({ mutation: createUser, variables: createUserVariables })
|
await mutate({ mutation: createUser, variables: createUserVariables })
|
||||||
const loginEmailOptIn = await LoginEmailOptIn.find()
|
const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email })
|
||||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
emailVerificationCode = emailContact.emailVerificationCode.toString()
|
||||||
result = await mutate({
|
result = await mutate({
|
||||||
mutation: setPassword,
|
mutation: setPassword,
|
||||||
variables: { code: emailOptIn, password: 'Aa12345_' },
|
variables: { code: emailVerificationCode, password: 'Aa12345_' },
|
||||||
})
|
})
|
||||||
newUser = await User.find()
|
newUser = await User.findOneOrFail(
|
||||||
|
{ id: emailContact.userId },
|
||||||
|
{ relations: ['emailContact'] },
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -465,11 +484,11 @@ bei Gradidio sei dabei!`,
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('sets email checked to true', () => {
|
it('sets email checked to true', () => {
|
||||||
expect(newUser[0].emailChecked).toBeTruthy()
|
expect(newUser.emailContact.emailChecked).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates the password', () => {
|
it('updates the password', () => {
|
||||||
expect(newUser[0].password).toEqual('3917921995996627700')
|
expect(newUser.password).toEqual('3917921995996627700')
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -491,11 +510,11 @@ bei Gradidio sei dabei!`,
|
|||||||
describe('no valid password', () => {
|
describe('no valid password', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await mutate({ mutation: createUser, variables: createUserVariables })
|
await mutate({ mutation: createUser, variables: createUserVariables })
|
||||||
const loginEmailOptIn = await LoginEmailOptIn.find()
|
const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email })
|
||||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
emailVerificationCode = emailContact.emailVerificationCode.toString()
|
||||||
result = await mutate({
|
result = await mutate({
|
||||||
mutation: setPassword,
|
mutation: setPassword,
|
||||||
variables: { code: emailOptIn, password: 'not-valid' },
|
variables: { code: emailVerificationCode, password: 'not-valid' },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -562,6 +581,7 @@ bei Gradidio sei dabei!`,
|
|||||||
|
|
||||||
describe('no users in database', () => {
|
describe('no users in database', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
result = await query({ query: login, variables })
|
result = await query({ query: login, variables })
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -574,7 +594,9 @@ bei Gradidio sei dabei!`,
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error found', () => {
|
it('logs the error found', () => {
|
||||||
expect(logger.error).toBeCalledWith('User with email=bibi@bloxberg.de does not exist')
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'UserContact with email=bibi@bloxberg.de does not exists',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -759,46 +781,68 @@ bei Gradidio sei dabei!`,
|
|||||||
|
|
||||||
describe('forgotPassword', () => {
|
describe('forgotPassword', () => {
|
||||||
const variables = { email: 'bibi@bloxberg.de' }
|
const variables = { email: 'bibi@bloxberg.de' }
|
||||||
|
const emailCodeRequestTime = CONFIG.EMAIL_CODE_REQUEST_TIME
|
||||||
|
|
||||||
describe('user is not in DB', () => {
|
describe('user is not in DB', () => {
|
||||||
it('returns true', async () => {
|
describe('duration not expired', () => {
|
||||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
it('returns true', async () => {
|
||||||
expect.objectContaining({
|
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||||
data: {
|
expect.objectContaining({
|
||||||
forgotPassword: true,
|
data: {
|
||||||
},
|
forgotPassword: true,
|
||||||
}),
|
},
|
||||||
)
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('user exists in DB', () => {
|
describe('user exists in DB', () => {
|
||||||
let result: any
|
let emailContact: UserContact
|
||||||
let loginEmailOptIn: LoginEmailOptIn[]
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await userFactory(testEnv, bibiBloxberg)
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
await resetEntity(LoginEmailOptIn)
|
// await resetEntity(LoginEmailOptIn)
|
||||||
result = await mutate({ mutation: forgotPassword, variables })
|
emailContact = await UserContact.findOneOrFail(variables)
|
||||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await cleanDB()
|
await cleanDB()
|
||||||
|
CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns true', async () => {
|
describe('duration not expired', () => {
|
||||||
await expect(result).toEqual(
|
it('returns true', async () => {
|
||||||
expect.objectContaining({
|
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||||
data: {
|
expect.objectContaining({
|
||||||
forgotPassword: true,
|
errors: [
|
||||||
},
|
new GraphQLError(
|
||||||
}),
|
`email already sent less than ${printTimeDuration(
|
||||||
)
|
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||||
|
)} minutes ago`,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('duration reset to 0', () => {
|
||||||
|
it('returns true', async () => {
|
||||||
|
CONFIG.EMAIL_CODE_REQUEST_TIME = 0
|
||||||
|
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
forgotPassword: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sends reset password email', () => {
|
it('sends reset password email', () => {
|
||||||
expect(sendResetPasswordEmail).toBeCalledWith({
|
expect(sendResetPasswordEmail).toBeCalledWith({
|
||||||
link: activationLink(loginEmailOptIn[0]),
|
link: activationLink(emailContact.emailVerificationCode),
|
||||||
firstName: 'Bibi',
|
firstName: 'Bibi',
|
||||||
lastName: 'Bloxberg',
|
lastName: 'Bloxberg',
|
||||||
email: 'bibi@bloxberg.de',
|
email: 'bibi@bloxberg.de',
|
||||||
@ -807,7 +851,8 @@ bei Gradidio sei dabei!`,
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('request reset password again', () => {
|
describe('request reset password again', () => {
|
||||||
it('throws an error', async () => {
|
it('thows an error', async () => {
|
||||||
|
CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime
|
||||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
|
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
|
||||||
@ -823,11 +868,11 @@ bei Gradidio sei dabei!`,
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('queryOptIn', () => {
|
describe('queryOptIn', () => {
|
||||||
let loginEmailOptIn: LoginEmailOptIn[]
|
let emailContact: UserContact
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await userFactory(testEnv, bibiBloxberg)
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
emailContact = await UserContact.findOneOrFail({ email: bibiBloxberg.email })
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -842,8 +887,8 @@ bei Gradidio sei dabei!`,
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
// keep Whitspace in error message!
|
// keep Whitspace in error message!
|
||||||
new GraphQLError(`Could not find any entity of type "LoginEmailOptIn" matching: {
|
new GraphQLError(`Could not find any entity of type "UserContact" matching: {
|
||||||
"verificationCode": "not-valid"
|
"emailVerificationCode": "not-valid"
|
||||||
}`),
|
}`),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@ -856,7 +901,7 @@ bei Gradidio sei dabei!`,
|
|||||||
await expect(
|
await expect(
|
||||||
query({
|
query({
|
||||||
query: queryOptIn,
|
query: queryOptIn,
|
||||||
variables: { optIn: loginEmailOptIn[0].verificationCode.toString() },
|
variables: { optIn: emailContact.emailVerificationCode.toString() },
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { backendLogger as logger } from '@/server/logger'
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
|
||||||
import { Context, getUser } from '@/server/context'
|
import { Context, getUser } from '@/server/context'
|
||||||
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
||||||
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
|
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { User } from '@model/User'
|
import { User } from '@model/User'
|
||||||
import { User as DbUser } from '@entity/User'
|
import { User as DbUser } from '@entity/User'
|
||||||
|
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||||
import { communityDbUser } from '@/util/communityUser'
|
import { communityDbUser } from '@/util/communityUser'
|
||||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
||||||
@ -16,7 +16,6 @@ import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
|
|||||||
import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs'
|
import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs'
|
||||||
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
|
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
|
||||||
import { OptInType } from '@enum/OptInType'
|
import { OptInType } from '@enum/OptInType'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
|
||||||
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
||||||
@ -29,10 +28,12 @@ import {
|
|||||||
EventLogin,
|
EventLogin,
|
||||||
EventRedeemRegister,
|
EventRedeemRegister,
|
||||||
EventRegister,
|
EventRegister,
|
||||||
|
EventSendAccountMultiRegistrationEmail,
|
||||||
EventSendConfirmationEmail,
|
EventSendConfirmationEmail,
|
||||||
EventActivateAccount,
|
EventActivateAccount,
|
||||||
} from '@/event/Event'
|
} from '@/event/Event'
|
||||||
import { getUserCreation } from './util/creations'
|
import { getUserCreation } from './util/creations'
|
||||||
|
import { UserContactType } from '../enum/UserContactType'
|
||||||
import { UserRepository } from '@/typeorm/repository/User'
|
import { UserRepository } from '@/typeorm/repository/User'
|
||||||
import { SearchAdminUsersResult } from '@model/AdminUser'
|
import { SearchAdminUsersResult } from '@model/AdminUser'
|
||||||
import Paginated from '@arg/Paginated'
|
import Paginated from '@arg/Paginated'
|
||||||
@ -147,6 +148,7 @@ const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[
|
|||||||
return [encryptionKeyHash, encryptionKey]
|
return [encryptionKeyHash, encryptionKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
const getEmailHash = (email: string): Buffer => {
|
const getEmailHash = (email: string): Buffer => {
|
||||||
logger.trace('getEmailHash...')
|
logger.trace('getEmailHash...')
|
||||||
const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
|
const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
|
||||||
@ -154,6 +156,7 @@ const getEmailHash = (email: string): Buffer => {
|
|||||||
logger.debug(`getEmailHash...successful: ${emailHash}`)
|
logger.debug(`getEmailHash...successful: ${emailHash}`)
|
||||||
return emailHash
|
return emailHash
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
|
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
|
||||||
logger.trace('SecretKeyCryptographyEncrypt...')
|
logger.trace('SecretKeyCryptographyEncrypt...')
|
||||||
@ -178,6 +181,19 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B
|
|||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newEmailContact = (email: string, userId: number): DbUserContact => {
|
||||||
|
logger.trace(`newEmailContact...`)
|
||||||
|
const emailContact = new DbUserContact()
|
||||||
|
emailContact.email = email
|
||||||
|
emailContact.userId = userId
|
||||||
|
emailContact.type = UserContactType.USER_CONTACT_EMAIL
|
||||||
|
emailContact.emailChecked = false
|
||||||
|
emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
|
||||||
|
emailContact.emailVerificationCode = random(64)
|
||||||
|
logger.debug(`newEmailContact...successful: ${emailContact}`)
|
||||||
|
return emailContact
|
||||||
|
}
|
||||||
|
/*
|
||||||
const newEmailOptIn = (userId: number): LoginEmailOptIn => {
|
const newEmailOptIn = (userId: number): LoginEmailOptIn => {
|
||||||
logger.trace('newEmailOptIn...')
|
logger.trace('newEmailOptIn...')
|
||||||
const emailOptIn = new LoginEmailOptIn()
|
const emailOptIn = new LoginEmailOptIn()
|
||||||
@ -187,7 +203,8 @@ const newEmailOptIn = (userId: number): LoginEmailOptIn => {
|
|||||||
logger.debug(`newEmailOptIn...successful: ${emailOptIn}`)
|
logger.debug(`newEmailOptIn...successful: ${emailOptIn}`)
|
||||||
return emailOptIn
|
return emailOptIn
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
/*
|
||||||
// needed by AdminResolver
|
// needed by AdminResolver
|
||||||
// checks if given code exists and can be resent
|
// checks if given code exists and can be resent
|
||||||
// if optIn does not exits, it is created
|
// if optIn does not exits, it is created
|
||||||
@ -227,10 +244,44 @@ export const checkOptInCode = async (
|
|||||||
logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${user.id}`)
|
logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${user.id}`)
|
||||||
return optInCode
|
return optInCode
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
export const checkEmailVerificationCode = async (
|
||||||
|
emailContact: DbUserContact,
|
||||||
|
optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER,
|
||||||
|
): Promise<DbUserContact> => {
|
||||||
|
logger.info(`checkEmailVerificationCode... ${emailContact}`)
|
||||||
|
if (emailContact.updatedAt) {
|
||||||
|
if (!canEmailResend(emailContact.updatedAt)) {
|
||||||
|
logger.error(
|
||||||
|
`email already sent less than ${printTimeDuration(
|
||||||
|
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||||
|
)} minutes ago`,
|
||||||
|
)
|
||||||
|
throw new Error(
|
||||||
|
`email already sent less than ${printTimeDuration(
|
||||||
|
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||||
|
)} minutes ago`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
emailContact.updatedAt = new Date()
|
||||||
|
emailContact.emailResendCount++
|
||||||
|
} else {
|
||||||
|
logger.trace('create new EmailVerificationCode for userId=' + emailContact.userId)
|
||||||
|
emailContact.emailChecked = false
|
||||||
|
emailContact.emailVerificationCode = random(64)
|
||||||
|
}
|
||||||
|
emailContact.emailOptInTypeId = optInType
|
||||||
|
await DbUserContact.save(emailContact).catch(() => {
|
||||||
|
logger.error('Unable to save email verification code= ' + emailContact)
|
||||||
|
throw new Error('Unable to save email verification code.')
|
||||||
|
})
|
||||||
|
logger.debug(`checkEmailVerificationCode...successful: ${emailContact}`)
|
||||||
|
return emailContact
|
||||||
|
}
|
||||||
|
|
||||||
export const activationLink = (optInCode: LoginEmailOptIn): string => {
|
export const activationLink = (verificationCode: BigInt): string => {
|
||||||
logger.debug(`activationLink(${LoginEmailOptIn})...`)
|
logger.debug(`activationLink(${verificationCode})...`)
|
||||||
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString())
|
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, verificationCode.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
const newGradidoID = async (): Promise<string> => {
|
const newGradidoID = async (): Promise<string> => {
|
||||||
@ -273,15 +324,12 @@ export class UserResolver {
|
|||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
logger.info(`login with ${email}, ***, ${publisherId} ...`)
|
logger.info(`login with ${email}, ***, ${publisherId} ...`)
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => {
|
const dbUser = await findUserByEmail(email)
|
||||||
logger.error(`User with email=${email} does not exist`)
|
|
||||||
throw new Error('No user with this credentials')
|
|
||||||
})
|
|
||||||
if (dbUser.deletedAt) {
|
if (dbUser.deletedAt) {
|
||||||
logger.error('The User was permanently deleted in database.')
|
logger.error('The User was permanently deleted in database.')
|
||||||
throw new Error('This user was permanently deleted. Contact support for questions.')
|
throw new Error('This user was permanently deleted. Contact support for questions.')
|
||||||
}
|
}
|
||||||
if (!dbUser.emailChecked) {
|
if (!dbUser.emailContact.emailChecked) {
|
||||||
logger.error('The Users email is not validate yet.')
|
logger.error('The Users email is not validate yet.')
|
||||||
throw new Error('User email not validated')
|
throw new Error('User email not validated')
|
||||||
}
|
}
|
||||||
@ -306,7 +354,7 @@ export class UserResolver {
|
|||||||
logger.debug('login credentials valid...')
|
logger.debug('login credentials valid...')
|
||||||
|
|
||||||
const user = new User(dbUser, await getUserCreation(dbUser.id))
|
const user = new User(dbUser, await getUserCreation(dbUser.id))
|
||||||
logger.debug('user=' + user)
|
logger.debug(`user= ${JSON.stringify(user, null, 2)}`)
|
||||||
|
|
||||||
// Elopage Status & Stored PublisherId
|
// Elopage Status & Stored PublisherId
|
||||||
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
|
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
|
||||||
@ -324,7 +372,7 @@ export class UserResolver {
|
|||||||
const ev = new EventLogin()
|
const ev = new EventLogin()
|
||||||
ev.userId = user.id
|
ev.userId = user.id
|
||||||
eventProtocol.writeEvent(new Event().setEventLogin(ev))
|
eventProtocol.writeEvent(new Event().setEventLogin(ev))
|
||||||
logger.info('successful Login:' + user)
|
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,66 +401,72 @@ export class UserResolver {
|
|||||||
)
|
)
|
||||||
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
|
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
|
||||||
// default int publisher_id = 0;
|
// default int publisher_id = 0;
|
||||||
|
const event = new Event()
|
||||||
|
|
||||||
// Validate Language (no throw)
|
// Validate Language (no throw)
|
||||||
if (!language || !isLanguage(language)) {
|
if (!language || !isLanguage(language)) {
|
||||||
language = DEFAULT_LANGUAGE
|
language = DEFAULT_LANGUAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate email unique
|
// check if user with email still exists?
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
// TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes
|
if (await checkEmailExists(email)) {
|
||||||
const userFound = await DbUser.findOne({ email }, { withDeleted: true })
|
const foundUser = await findUserByEmail(email)
|
||||||
logger.info(`DbUser.findOne(email=${email}) = ${userFound}`)
|
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`)
|
||||||
|
|
||||||
if (userFound) {
|
if (foundUser) {
|
||||||
// ATTENTION: this logger-message will be exactly expected during tests
|
// ATTENTION: this logger-message will be exactly expected during tests
|
||||||
logger.info(`User already exists with this email=${email}`)
|
logger.info(`User already exists with this email=${email}`)
|
||||||
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
||||||
|
|
||||||
const user = new User(communityDbUser)
|
const user = new User(communityDbUser)
|
||||||
user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in?
|
user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in?
|
||||||
user.gradidoID = uuidv4()
|
user.gradidoID = uuidv4()
|
||||||
user.email = email
|
user.email = email
|
||||||
user.firstName = firstName
|
user.firstName = firstName
|
||||||
user.lastName = lastName
|
user.lastName = lastName
|
||||||
user.language = language
|
user.language = language
|
||||||
user.publisherId = publisherId
|
user.publisherId = publisherId
|
||||||
logger.debug('partly faked user=' + user)
|
logger.debug('partly faked user=' + user)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const emailSent = await sendAccountMultiRegistrationEmail({
|
const emailSent = await sendAccountMultiRegistrationEmail({
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
email,
|
email,
|
||||||
})
|
})
|
||||||
logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`)
|
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
|
||||||
/* uncomment this, when you need the activation link on the console */
|
eventSendAccountMultiRegistrationEmail.userId = foundUser.id
|
||||||
// In case EMails are disabled log the activation link for the user
|
eventProtocol.writeEvent(
|
||||||
if (!emailSent) {
|
event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail),
|
||||||
logger.debug(`Email not sent!`)
|
)
|
||||||
|
logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`)
|
||||||
|
/* uncomment this, when you need the activation link on the console */
|
||||||
|
// In case EMails are disabled log the activation link for the user
|
||||||
|
if (!emailSent) {
|
||||||
|
logger.debug(`Email not send!`)
|
||||||
|
}
|
||||||
|
logger.info('createUser() faked and send multi registration mail...')
|
||||||
|
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
logger.info('createUser() faked and send multi registration mail...')
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const passphrase = PassphraseGenerate()
|
const passphrase = PassphraseGenerate()
|
||||||
// const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
// const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
||||||
// const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
|
// const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
|
||||||
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
||||||
const emailHash = getEmailHash(email)
|
// const emailHash = getEmailHash(email)
|
||||||
const gradidoID = await newGradidoID()
|
const gradidoID = await newGradidoID()
|
||||||
|
|
||||||
const eventRegister = new EventRegister()
|
const eventRegister = new EventRegister()
|
||||||
const eventRedeemRegister = new EventRedeemRegister()
|
const eventRedeemRegister = new EventRedeemRegister()
|
||||||
const eventSendConfirmEmail = new EventSendConfirmationEmail()
|
const eventSendConfirmEmail = new EventSendConfirmationEmail()
|
||||||
const dbUser = new DbUser()
|
|
||||||
|
let dbUser = new DbUser()
|
||||||
dbUser.gradidoID = gradidoID
|
dbUser.gradidoID = gradidoID
|
||||||
dbUser.email = email
|
|
||||||
dbUser.firstName = firstName
|
dbUser.firstName = firstName
|
||||||
dbUser.lastName = lastName
|
dbUser.lastName = lastName
|
||||||
dbUser.emailHash = emailHash
|
|
||||||
dbUser.language = language
|
dbUser.language = language
|
||||||
dbUser.publisherId = publisherId
|
dbUser.publisherId = publisherId
|
||||||
dbUser.passphrase = passphrase.join(' ')
|
dbUser.passphrase = passphrase.join(' ')
|
||||||
@ -443,25 +497,38 @@ export class UserResolver {
|
|||||||
// loginUser.pubKey = keyPair[0]
|
// loginUser.pubKey = keyPair[0]
|
||||||
// loginUser.privKey = encryptedPrivkey
|
// loginUser.privKey = encryptedPrivkey
|
||||||
|
|
||||||
const event = new Event()
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
try {
|
try {
|
||||||
await queryRunner.manager.save(dbUser).catch((error) => {
|
dbUser = await queryRunner.manager.save(dbUser).catch((error) => {
|
||||||
logger.error('Error while saving dbUser', error)
|
logger.error('Error while saving dbUser', error)
|
||||||
throw new Error('error saving user')
|
throw new Error('error saving user')
|
||||||
})
|
})
|
||||||
|
let emailContact = newEmailContact(email, dbUser.id)
|
||||||
|
emailContact = await queryRunner.manager.save(emailContact).catch((error) => {
|
||||||
|
logger.error('Error while saving emailContact', error)
|
||||||
|
throw new Error('error saving email user contact')
|
||||||
|
})
|
||||||
|
|
||||||
|
dbUser.emailContact = emailContact
|
||||||
|
dbUser.emailId = emailContact.id
|
||||||
|
await queryRunner.manager.save(dbUser).catch((error) => {
|
||||||
|
logger.error('Error while updating dbUser', error)
|
||||||
|
throw new Error('error updating user')
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
const emailOptIn = newEmailOptIn(dbUser.id)
|
const emailOptIn = newEmailOptIn(dbUser.id)
|
||||||
await queryRunner.manager.save(emailOptIn).catch((error) => {
|
await queryRunner.manager.save(emailOptIn).catch((error) => {
|
||||||
logger.error('Error while saving emailOptIn', error)
|
logger.error('Error while saving emailOptIn', error)
|
||||||
throw new Error('error saving email opt in')
|
throw new Error('error saving email opt in')
|
||||||
})
|
})
|
||||||
|
*/
|
||||||
|
|
||||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||||
/{optin}/g,
|
/{optin}/g,
|
||||||
emailOptIn.verificationCode.toString(),
|
emailContact.emailVerificationCode.toString(),
|
||||||
).replace(/{code}/g, redeemCode ? '/' + redeemCode : '')
|
).replace(/{code}/g, redeemCode ? '/' + redeemCode : '')
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
@ -476,8 +543,6 @@ export class UserResolver {
|
|||||||
eventSendConfirmEmail.userId = dbUser.id
|
eventSendConfirmEmail.userId = dbUser.id
|
||||||
eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail))
|
eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail))
|
||||||
|
|
||||||
/* uncomment this, when you need the activation link on the console */
|
|
||||||
// In case EMails are disabled log the activation link for the user
|
|
||||||
if (!emailSent) {
|
if (!emailSent) {
|
||||||
logger.debug(`Account confirmation link: ${activationLink}`)
|
logger.debug(`Account confirmation link: ${activationLink}`)
|
||||||
}
|
}
|
||||||
@ -508,22 +573,29 @@ export class UserResolver {
|
|||||||
async forgotPassword(@Arg('email') email: string): Promise<boolean> {
|
async forgotPassword(@Arg('email') email: string): Promise<boolean> {
|
||||||
logger.info(`forgotPassword(${email})...`)
|
logger.info(`forgotPassword(${email})...`)
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
const user = await DbUser.findOne({ email })
|
const user = await findUserByEmail(email).catch(() => {
|
||||||
|
logger.warn(`fail on find UserContact per ${email}`)
|
||||||
|
})
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.warn(`no user found with ${email}`)
|
logger.warn(`no user found with ${email}`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// can be both types: REGISTER and RESET_PASSWORD
|
// can be both types: REGISTER and RESET_PASSWORD
|
||||||
let optInCode = await LoginEmailOptIn.findOne({
|
// let optInCode = await LoginEmailOptIn.findOne({
|
||||||
userId: user.id,
|
// userId: user.id,
|
||||||
})
|
// })
|
||||||
|
// let optInCode = user.emailContact.emailVerificationCode
|
||||||
|
const dbUserContact = await checkEmailVerificationCode(
|
||||||
|
user.emailContact,
|
||||||
|
OptInType.EMAIL_OPT_IN_RESET_PASSWORD,
|
||||||
|
)
|
||||||
|
|
||||||
optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
|
// optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
|
||||||
logger.info(`optInCode for ${email}=${optInCode}`)
|
logger.info(`optInCode for ${email}=${dbUserContact}`)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const emailSent = await sendResetPasswordEmailMailer({
|
const emailSent = await sendResetPasswordEmailMailer({
|
||||||
link: activationLink(optInCode),
|
link: activationLink(dbUserContact.emailVerificationCode),
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
email,
|
email,
|
||||||
@ -533,7 +605,7 @@ export class UserResolver {
|
|||||||
/* uncomment this, when you need the activation link on the console */
|
/* uncomment this, when you need the activation link on the console */
|
||||||
// In case EMails are disabled log the activation link for the user
|
// In case EMails are disabled log the activation link for the user
|
||||||
if (!emailSent) {
|
if (!emailSent) {
|
||||||
logger.debug(`Reset password link: ${activationLink(optInCode)}`)
|
logger.debug(`Reset password link: ${activationLink(dbUserContact.emailVerificationCode)}`)
|
||||||
}
|
}
|
||||||
logger.info(`forgotPassword(${email}) successful...`)
|
logger.info(`forgotPassword(${email}) successful...`)
|
||||||
|
|
||||||
@ -556,13 +628,22 @@ export class UserResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load code
|
// Load code
|
||||||
|
/*
|
||||||
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
|
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
|
||||||
logger.error('Could not login with emailVerificationCode')
|
logger.error('Could not login with emailVerificationCode')
|
||||||
throw new Error('Could not login with emailVerificationCode')
|
throw new Error('Could not login with emailVerificationCode')
|
||||||
})
|
})
|
||||||
logger.debug('optInCode loaded...')
|
*/
|
||||||
|
const userContact = await DbUserContact.findOneOrFail(
|
||||||
|
{ emailVerificationCode: code },
|
||||||
|
{ relations: ['user'] },
|
||||||
|
).catch(() => {
|
||||||
|
logger.error('Could not login with emailVerificationCode')
|
||||||
|
throw new Error('Could not login with emailVerificationCode')
|
||||||
|
})
|
||||||
|
logger.debug('userContact loaded...')
|
||||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||||
if (!isOptInValid(optInCode)) {
|
if (!isEmailVerificationCodeValid(userContact.updatedAt)) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||||
)
|
)
|
||||||
@ -570,14 +651,11 @@ export class UserResolver {
|
|||||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
logger.debug('optInCode is valid...')
|
logger.debug('EmailVerificationCode is valid...')
|
||||||
|
|
||||||
// load user
|
// load user
|
||||||
const user = await DbUser.findOneOrFail({ id: optInCode.userId }).catch(() => {
|
const user = userContact.user
|
||||||
logger.error('Could not find corresponding Login User')
|
logger.debug('user with EmailVerificationCode found...')
|
||||||
throw new Error('Could not find corresponding Login User')
|
|
||||||
})
|
|
||||||
logger.debug('user with optInCode found...')
|
|
||||||
|
|
||||||
// Generate Passphrase if needed
|
// Generate Passphrase if needed
|
||||||
if (!user.passphrase) {
|
if (!user.passphrase) {
|
||||||
@ -597,10 +675,10 @@ export class UserResolver {
|
|||||||
logger.debug('Passphrase is valid...')
|
logger.debug('Passphrase is valid...')
|
||||||
|
|
||||||
// Activate EMail
|
// Activate EMail
|
||||||
user.emailChecked = true
|
userContact.emailChecked = true
|
||||||
|
|
||||||
// Update Password
|
// Update Password
|
||||||
const passwordHash = SecretKeyCryptographyCreateKey(user.email, password) // return short and long hash
|
const passwordHash = SecretKeyCryptographyCreateKey(userContact.email, password) // return short and long hash
|
||||||
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
||||||
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
||||||
user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
|
user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
|
||||||
@ -610,7 +688,7 @@ export class UserResolver {
|
|||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
|
|
||||||
const event = new Event()
|
const event = new Event()
|
||||||
|
|
||||||
@ -620,17 +698,21 @@ export class UserResolver {
|
|||||||
logger.error('error saving user: ' + error)
|
logger.error('error saving user: ' + error)
|
||||||
throw new Error('error saving user: ' + error)
|
throw new Error('error saving user: ' + error)
|
||||||
})
|
})
|
||||||
|
// Save userContact
|
||||||
|
await queryRunner.manager.save(userContact).catch((error) => {
|
||||||
|
logger.error('error saving userContact: ' + error)
|
||||||
|
throw new Error('error saving userContact: ' + error)
|
||||||
|
})
|
||||||
|
|
||||||
await queryRunner.commitTransaction()
|
await queryRunner.commitTransaction()
|
||||||
|
logger.info('User and UserContact data written successfully...')
|
||||||
|
|
||||||
const eventActivateAccount = new EventActivateAccount()
|
const eventActivateAccount = new EventActivateAccount()
|
||||||
eventActivateAccount.userId = user.id
|
eventActivateAccount.userId = user.id
|
||||||
eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount))
|
eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount))
|
||||||
|
|
||||||
logger.info('User data written successfully...')
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
logger.error('Error on writing User data:' + e)
|
logger.error('Error on writing User and UserContact data:' + e)
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
@ -638,11 +720,11 @@ export class UserResolver {
|
|||||||
|
|
||||||
// Sign into Klicktipp
|
// Sign into Klicktipp
|
||||||
// TODO do we always signUp the user? How to handle things with old users?
|
// TODO do we always signUp the user? How to handle things with old users?
|
||||||
if (optInCode.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
|
if (userContact.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
|
||||||
try {
|
try {
|
||||||
await klicktippSignIn(user.email, user.language, user.firstName, user.lastName)
|
await klicktippSignIn(userContact.email, user.language, user.firstName, user.lastName)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`klicktippSignIn(${user.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
|
`klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error subscribe to klicktipp:' + e)
|
logger.error('Error subscribe to klicktipp:' + e)
|
||||||
@ -661,10 +743,10 @@ export class UserResolver {
|
|||||||
@Query(() => Boolean)
|
@Query(() => Boolean)
|
||||||
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
|
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
|
||||||
logger.info(`queryOptIn(${optIn})...`)
|
logger.info(`queryOptIn(${optIn})...`)
|
||||||
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn })
|
const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn })
|
||||||
logger.debug(`found optInCode=${optInCode}`)
|
logger.debug(`found optInCode=${userContact}`)
|
||||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||||
if (!isOptInValid(optInCode)) {
|
if (!isEmailVerificationCodeValid(userContact.updatedAt)) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||||
)
|
)
|
||||||
@ -712,7 +794,10 @@ export class UserResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This had some error cases defined - like missing private key. This is no longer checked.
|
// TODO: This had some error cases defined - like missing private key. This is no longer checked.
|
||||||
const oldPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, password)
|
const oldPasswordHash = SecretKeyCryptographyCreateKey(
|
||||||
|
userEntity.emailContact.email,
|
||||||
|
password,
|
||||||
|
)
|
||||||
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
|
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
|
||||||
logger.error(`Old password is invalid`)
|
logger.error(`Old password is invalid`)
|
||||||
throw new Error(`Old password is invalid`)
|
throw new Error(`Old password is invalid`)
|
||||||
@ -720,7 +805,10 @@ export class UserResolver {
|
|||||||
|
|
||||||
const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
|
const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
|
||||||
logger.debug('oldPassword decrypted...')
|
logger.debug('oldPassword decrypted...')
|
||||||
const newPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, passwordNew) // return short and long hash
|
const newPasswordHash = SecretKeyCryptographyCreateKey(
|
||||||
|
userEntity.emailContact.email,
|
||||||
|
passwordNew,
|
||||||
|
) // return short and long hash
|
||||||
logger.debug('newPasswordHash created...')
|
logger.debug('newPasswordHash created...')
|
||||||
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
|
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
|
||||||
logger.debug('PrivateKey encrypted...')
|
logger.debug('PrivateKey encrypted...')
|
||||||
@ -732,7 +820,7 @@ export class UserResolver {
|
|||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await queryRunner.manager.save(userEntity).catch((error) => {
|
await queryRunner.manager.save(userEntity).catch((error) => {
|
||||||
@ -757,12 +845,8 @@ export class UserResolver {
|
|||||||
@Query(() => Boolean)
|
@Query(() => Boolean)
|
||||||
async hasElopage(@Ctx() context: Context): Promise<boolean> {
|
async hasElopage(@Ctx() context: Context): Promise<boolean> {
|
||||||
logger.info(`hasElopage()...`)
|
logger.info(`hasElopage()...`)
|
||||||
const userEntity = context.user
|
const userEntity = getUser(context)
|
||||||
if (!userEntity) {
|
const elopageBuys = hasElopageBuys(userEntity.emailContact.email)
|
||||||
logger.info('missing context.user for EloPage-check')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const elopageBuys = hasElopageBuys(userEntity.email)
|
|
||||||
logger.debug(`has ElopageBuys = ${elopageBuys}`)
|
logger.debug(`has ElopageBuys = ${elopageBuys}`)
|
||||||
return elopageBuys
|
return elopageBuys
|
||||||
}
|
}
|
||||||
@ -798,19 +882,58 @@ export class UserResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function findUserByEmail(email: string): Promise<DbUser> {
|
||||||
|
const dbUserContact = await DbUserContact.findOneOrFail(
|
||||||
|
{ email: email },
|
||||||
|
{ withDeleted: true, relations: ['user'] },
|
||||||
|
).catch(() => {
|
||||||
|
logger.error(`UserContact with email=${email} does not exists`)
|
||||||
|
throw new Error('No user with this credentials')
|
||||||
|
})
|
||||||
|
const dbUser = dbUserContact.user
|
||||||
|
dbUser.emailContact = dbUserContact
|
||||||
|
return dbUser
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkEmailExists(email: string): Promise<boolean> {
|
||||||
|
const userContact = await DbUserContact.findOne({ email: email }, { withDeleted: true })
|
||||||
|
if (userContact) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => {
|
const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => {
|
||||||
const timeElapsed = Date.now() - new Date(optIn.updatedAt).getTime()
|
const timeElapsed = Date.now() - new Date(optIn.updatedAt).getTime()
|
||||||
// time is given in minutes
|
// time is given in minutes
|
||||||
return timeElapsed <= duration * 60 * 1000
|
return timeElapsed <= duration * 60 * 1000
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
const isTimeExpired = (updatedAt: Date, duration: number): boolean => {
|
||||||
|
const timeElapsed = Date.now() - new Date(updatedAt).getTime()
|
||||||
|
// time is given in minutes
|
||||||
|
return timeElapsed <= duration * 60 * 1000
|
||||||
|
}
|
||||||
|
/*
|
||||||
const isOptInValid = (optIn: LoginEmailOptIn): boolean => {
|
const isOptInValid = (optIn: LoginEmailOptIn): boolean => {
|
||||||
return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME)
|
return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
const isEmailVerificationCodeValid = (updatedAt: Date | null): boolean => {
|
||||||
|
if (updatedAt == null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_VALID_TIME)
|
||||||
|
}
|
||||||
|
/*
|
||||||
const canResendOptIn = (optIn: LoginEmailOptIn): boolean => {
|
const canResendOptIn = (optIn: LoginEmailOptIn): boolean => {
|
||||||
return !isTimeExpired(optIn, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
return !isTimeExpired(optIn, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
const canEmailResend = (updatedAt: Date): boolean => {
|
||||||
|
return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
||||||
|
}
|
||||||
|
|
||||||
const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => {
|
const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => {
|
||||||
if (time > 60) {
|
if (time > 60) {
|
||||||
|
|||||||
@ -15,14 +15,21 @@ export const validateContribution = (
|
|||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
creationDate: Date,
|
creationDate: Date,
|
||||||
): void => {
|
): void => {
|
||||||
logger.trace('isContributionValid', creations, amount, creationDate)
|
logger.trace('isContributionValid: ', creations, amount, creationDate)
|
||||||
const index = getCreationIndex(creationDate.getMonth())
|
const index = getCreationIndex(creationDate.getMonth())
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
|
logger.error(
|
||||||
|
'No information for available creations with the given creationDate=',
|
||||||
|
creationDate,
|
||||||
|
)
|
||||||
throw new Error('No information for available creations for the given date')
|
throw new Error('No information for available creations for the given date')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (amount.greaterThan(creations[index].toString())) {
|
if (amount.greaterThan(creations[index].toString())) {
|
||||||
|
logger.error(
|
||||||
|
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
||||||
|
)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
||||||
)
|
)
|
||||||
@ -41,7 +48,7 @@ export const getUserCreations = async (
|
|||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
|
|
||||||
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
||||||
logger.trace('getUserCreations dateFilter', dateFilter)
|
logger.trace('getUserCreations dateFilter=', dateFilter)
|
||||||
|
|
||||||
const unionString = includePending
|
const unionString = includePending
|
||||||
? `
|
? `
|
||||||
@ -51,6 +58,7 @@ export const getUserCreations = async (
|
|||||||
AND contribution_date >= ${dateFilter}
|
AND contribution_date >= ${dateFilter}
|
||||||
AND confirmed_at IS NULL AND deleted_at IS NULL`
|
AND confirmed_at IS NULL AND deleted_at IS NULL`
|
||||||
: ''
|
: ''
|
||||||
|
logger.trace('getUserCreations unionString=', unionString)
|
||||||
|
|
||||||
const unionQuery = await queryRunner.manager.query(`
|
const unionQuery = await queryRunner.manager.query(`
|
||||||
SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM
|
SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM
|
||||||
@ -62,6 +70,7 @@ export const getUserCreations = async (
|
|||||||
GROUP BY month, userId
|
GROUP BY month, userId
|
||||||
ORDER BY date DESC
|
ORDER BY date DESC
|
||||||
`)
|
`)
|
||||||
|
logger.trace('getUserCreations unionQuery=', unionQuery)
|
||||||
|
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
|
|
||||||
@ -82,6 +91,7 @@ export const getUserCreations = async (
|
|||||||
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
||||||
logger.trace('getUserCreation', id, includePending)
|
logger.trace('getUserCreation', id, includePending)
|
||||||
const creations = await getUserCreations([id], includePending)
|
const creations = await getUserCreations([id], includePending)
|
||||||
|
logger.trace('getUserCreation creations=', creations)
|
||||||
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -73,7 +73,7 @@ describe('sendEMail', () => {
|
|||||||
it('calls sendMail of transporter', () => {
|
it('calls sendMail of transporter', () => {
|
||||||
expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({
|
expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({
|
||||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||||
to: 'receiver@mail.org',
|
to: `${CONFIG.EMAIL_TEST_RECEIVER}`,
|
||||||
cc: 'support@gradido.net',
|
cc: 'support@gradido.net',
|
||||||
subject: 'Subject',
|
subject: 'Subject',
|
||||||
text: 'Text text text',
|
text: 'Text text text',
|
||||||
|
|||||||
@ -19,6 +19,12 @@ export const sendEMail = async (emailDef: {
|
|||||||
logger.info(`Emails are disabled via config...`)
|
logger.info(`Emails are disabled via config...`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (CONFIG.EMAIL_TEST_MODUS) {
|
||||||
|
logger.info(
|
||||||
|
`Testmodus=ON: change receiver from ${emailDef.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`,
|
||||||
|
)
|
||||||
|
emailDef.to = CONFIG.EMAIL_TEST_RECEIVER
|
||||||
|
}
|
||||||
const transporter = createTransport({
|
const transporter = createTransport({
|
||||||
host: CONFIG.EMAIL_SMTP_URL,
|
host: CONFIG.EMAIL_SMTP_URL,
|
||||||
port: Number(CONFIG.EMAIL_SMTP_PORT),
|
port: Number(CONFIG.EMAIL_SMTP_PORT),
|
||||||
|
|||||||
@ -11,7 +11,11 @@ export const contributionLinkFactory = async (
|
|||||||
const { mutate, query } = client
|
const { mutate, query } = client
|
||||||
|
|
||||||
// login as admin
|
// login as admin
|
||||||
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const user = await query({
|
||||||
|
query: login,
|
||||||
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
|
||||||
const variables = {
|
const variables = {
|
||||||
amount: contributionLink.amount,
|
amount: contributionLink.amount,
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
import { adminCreateContribution, confirmContribution } from '@/seeds/graphql/mutations'
|
import { adminCreateContribution, confirmContribution } from '@/seeds/graphql/mutations'
|
||||||
import { login } from '@/seeds/graphql/queries'
|
import { login } from '@/seeds/graphql/queries'
|
||||||
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
||||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||||
import { User } from '@entity/User'
|
|
||||||
import { Transaction } from '@entity/Transaction'
|
import { Transaction } from '@entity/Transaction'
|
||||||
import { Contribution } from '@entity/Contribution'
|
import { Contribution } from '@entity/Contribution'
|
||||||
|
import { findUserByEmail } from '@/graphql/resolver/UserResolver'
|
||||||
// import CONFIG from '@/config/index'
|
// import CONFIG from '@/config/index'
|
||||||
|
|
||||||
export const nMonthsBefore = (date: Date, months = 1): string => {
|
export const nMonthsBefore = (date: Date, months = 1): string => {
|
||||||
@ -19,29 +20,41 @@ export const creationFactory = async (
|
|||||||
creation: CreationInterface,
|
creation: CreationInterface,
|
||||||
): Promise<Contribution | void> => {
|
): Promise<Contribution | void> => {
|
||||||
const { mutate, query } = client
|
const { mutate, query } = client
|
||||||
|
logger.trace('creationFactory...')
|
||||||
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||||
|
logger.trace('creationFactory... after login')
|
||||||
// TODO it would be nice to have this mutation return the id
|
// TODO it would be nice to have this mutation return the id
|
||||||
await mutate({ mutation: adminCreateContribution, variables: { ...creation } })
|
await mutate({ mutation: adminCreateContribution, variables: { ...creation } })
|
||||||
|
logger.trace('creationFactory... after adminCreateContribution')
|
||||||
|
|
||||||
const user = await User.findOneOrFail({ where: { email: creation.email } })
|
const user = await findUserByEmail(creation.email) // userContact.user
|
||||||
|
|
||||||
const pendingCreation = await Contribution.findOneOrFail({
|
const pendingCreation = await Contribution.findOneOrFail({
|
||||||
where: { userId: user.id, amount: creation.amount },
|
where: { userId: user.id, amount: creation.amount },
|
||||||
order: { createdAt: 'DESC' },
|
order: { createdAt: 'DESC' },
|
||||||
})
|
})
|
||||||
|
logger.trace(
|
||||||
|
'creationFactory... after Contribution.findOneOrFail pendingCreation=',
|
||||||
|
pendingCreation,
|
||||||
|
)
|
||||||
if (creation.confirmed) {
|
if (creation.confirmed) {
|
||||||
|
logger.trace('creationFactory... creation.confirmed=', creation.confirmed)
|
||||||
await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } })
|
await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } })
|
||||||
|
logger.trace('creationFactory... after confirmContribution')
|
||||||
const confirmedCreation = await Contribution.findOneOrFail({ id: pendingCreation.id })
|
const confirmedCreation = await Contribution.findOneOrFail({ id: pendingCreation.id })
|
||||||
|
logger.trace(
|
||||||
|
'creationFactory... after Contribution.findOneOrFail confirmedCreation=',
|
||||||
|
confirmedCreation,
|
||||||
|
)
|
||||||
|
|
||||||
if (creation.moveCreationDate) {
|
if (creation.moveCreationDate) {
|
||||||
|
logger.trace('creationFactory... creation.moveCreationDate=', creation.moveCreationDate)
|
||||||
const transaction = await Transaction.findOneOrFail({
|
const transaction = await Transaction.findOneOrFail({
|
||||||
where: { userId: user.id, creationDate: new Date(creation.creationDate) },
|
where: { userId: user.id, creationDate: new Date(creation.creationDate) },
|
||||||
order: { balanceDate: 'DESC' },
|
order: { balanceDate: 'DESC' },
|
||||||
})
|
})
|
||||||
|
logger.trace('creationFactory... after Transaction.findOneOrFail transaction=', transaction)
|
||||||
|
|
||||||
if (transaction.decay.equals(0) && transaction.creationDate) {
|
if (transaction.decay.equals(0) && transaction.creationDate) {
|
||||||
confirmedCreation.contributionDate = new Date(
|
confirmedCreation.contributionDate = new Date(
|
||||||
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
||||||
@ -52,11 +65,17 @@ export const creationFactory = async (
|
|||||||
transaction.balanceDate = new Date(
|
transaction.balanceDate = new Date(
|
||||||
nMonthsBefore(transaction.balanceDate, creation.moveCreationDate),
|
nMonthsBefore(transaction.balanceDate, creation.moveCreationDate),
|
||||||
)
|
)
|
||||||
|
logger.trace('creationFactory... before transaction.save transaction=', transaction)
|
||||||
await transaction.save()
|
await transaction.save()
|
||||||
|
logger.trace(
|
||||||
|
'creationFactory... before confirmedCreation.save confirmedCreation=',
|
||||||
|
confirmedCreation,
|
||||||
|
)
|
||||||
await confirmedCreation.save()
|
await confirmedCreation.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
logger.trace('creationFactory... pendingCreation=', pendingCreation)
|
||||||
return pendingCreation
|
return pendingCreation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { createUser, setPassword } from '@/seeds/graphql/mutations'
|
import { createUser, setPassword } from '@/seeds/graphql/mutations'
|
||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
|
||||||
import { UserInterface } from '@/seeds/users/UserInterface'
|
import { UserInterface } from '@/seeds/users/UserInterface'
|
||||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||||
|
|
||||||
@ -15,17 +14,23 @@ export const userFactory = async (
|
|||||||
createUser: { id },
|
createUser: { id },
|
||||||
},
|
},
|
||||||
} = await mutate({ mutation: createUser, variables: user })
|
} = await mutate({ mutation: createUser, variables: user })
|
||||||
|
// console.log('creatUser:', { id }, { user })
|
||||||
|
// get user from database
|
||||||
|
let dbUser = await User.findOneOrFail({ id }, { relations: ['emailContact'] })
|
||||||
|
// console.log('dbUser:', dbUser)
|
||||||
|
|
||||||
|
const emailContact = dbUser.emailContact
|
||||||
|
// console.log('emailContact:', emailContact)
|
||||||
|
|
||||||
if (user.emailChecked) {
|
if (user.emailChecked) {
|
||||||
const optin = await LoginEmailOptIn.findOneOrFail({ userId: id })
|
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: setPassword,
|
mutation: setPassword,
|
||||||
variables: { password: 'Aa12345_', code: optin.verificationCode },
|
variables: { password: 'Aa12345_', code: emailContact.emailVerificationCode },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// get user from database
|
// get last changes of user from database
|
||||||
const dbUser = await User.findOneOrFail({ id })
|
dbUser = await User.findOneOrFail({ id })
|
||||||
|
|
||||||
if (user.createdAt || user.deletedAt || user.isAdmin) {
|
if (user.createdAt || user.deletedAt || user.isAdmin) {
|
||||||
if (user.createdAt) dbUser.createdAt = user.createdAt
|
if (user.createdAt) dbUser.createdAt = user.createdAt
|
||||||
@ -34,5 +39,8 @@ export const userFactory = async (
|
|||||||
await dbUser.save()
|
await dbUser.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get last changes of user from database
|
||||||
|
// dbUser = await User.findOneOrFail({ id }, { withDeleted: true })
|
||||||
|
|
||||||
return dbUser
|
return dbUser
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
import createServer from '../server/createServer'
|
import createServer from '../server/createServer'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|
||||||
@ -50,11 +51,14 @@ const run = async () => {
|
|||||||
const seedClient = createTestClient(server.apollo)
|
const seedClient = createTestClient(server.apollo)
|
||||||
const { con } = server
|
const { con } = server
|
||||||
await cleanDB()
|
await cleanDB()
|
||||||
|
logger.info('##seed## clean database successful...')
|
||||||
|
|
||||||
// seed the standard users
|
// seed the standard users
|
||||||
for (let i = 0; i < users.length; i++) {
|
for (let i = 0; i < users.length; i++) {
|
||||||
await userFactory(seedClient, users[i])
|
const dbUser = await userFactory(seedClient, users[i])
|
||||||
|
logger.info(`##seed## seed standard users[ ${i} ]= ${JSON.stringify(dbUser, null, 2)}`)
|
||||||
}
|
}
|
||||||
|
logger.info('##seed## seeding all standard users successful...')
|
||||||
|
|
||||||
// seed 100 random users
|
// seed 100 random users
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
@ -64,7 +68,9 @@ const run = async () => {
|
|||||||
email: internet.email(),
|
email: internet.email(),
|
||||||
language: datatype.boolean() ? 'en' : 'de',
|
language: datatype.boolean() ? 'en' : 'de',
|
||||||
})
|
})
|
||||||
|
logger.info(`##seed## seed ${i}. random user`)
|
||||||
}
|
}
|
||||||
|
logger.info('##seed## seeding all random users successful...')
|
||||||
|
|
||||||
// create GDD
|
// create GDD
|
||||||
for (let i = 0; i < creations.length; i++) {
|
for (let i = 0; i < creations.length; i++) {
|
||||||
@ -73,16 +79,19 @@ const run = async () => {
|
|||||||
// eslint-disable-next-line no-empty
|
// eslint-disable-next-line no-empty
|
||||||
while (new Date().getTime() < now + 1000) {} // we have to wait a little! quick fix for account sum problem of bob@baumeister.de, (see https://github.com/gradido/gradido/issues/1886)
|
while (new Date().getTime() < now + 1000) {} // we have to wait a little! quick fix for account sum problem of bob@baumeister.de, (see https://github.com/gradido/gradido/issues/1886)
|
||||||
}
|
}
|
||||||
|
logger.info('##seed## seeding all creations successful...')
|
||||||
|
|
||||||
// create Transaction Links
|
// create Transaction Links
|
||||||
for (let i = 0; i < transactionLinks.length; i++) {
|
for (let i = 0; i < transactionLinks.length; i++) {
|
||||||
await transactionLinkFactory(seedClient, transactionLinks[i])
|
await transactionLinkFactory(seedClient, transactionLinks[i])
|
||||||
}
|
}
|
||||||
|
logger.info('##seed## seeding all transactionLinks successful...')
|
||||||
|
|
||||||
// create Contribution Links
|
// create Contribution Links
|
||||||
for (let i = 0; i < contributionLinks.length; i++) {
|
for (let i = 0; i < contributionLinks.length; i++) {
|
||||||
await contributionLinkFactory(seedClient, contributionLinks[i])
|
await contributionLinkFactory(seedClient, contributionLinks[i])
|
||||||
}
|
}
|
||||||
|
logger.info('##seed## seeding all contributionLinks successful...')
|
||||||
|
|
||||||
await con.close()
|
await con.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export interface Context {
|
|||||||
setHeaders: { key: string; value: string }[]
|
setHeaders: { key: string; value: string }[]
|
||||||
role?: Role
|
role?: Role
|
||||||
user?: dbUser
|
user?: dbUser
|
||||||
|
clientRequestTime?: string
|
||||||
// hack to use less DB calls for Balance Resolver
|
// hack to use less DB calls for Balance Resolver
|
||||||
lastTransaction?: dbTransaction
|
lastTransaction?: dbTransaction
|
||||||
transactionCount?: number
|
transactionCount?: number
|
||||||
@ -18,14 +19,17 @@ export interface Context {
|
|||||||
|
|
||||||
const context = (args: ExpressContext): Context => {
|
const context = (args: ExpressContext): Context => {
|
||||||
const authorization = args.req.headers.authorization
|
const authorization = args.req.headers.authorization
|
||||||
let token: string | null = null
|
const clientRequestTime = args.req.headers.clientrequesttime
|
||||||
if (authorization) {
|
const context: Context = {
|
||||||
token = authorization.replace(/^Bearer /, '')
|
token: null,
|
||||||
}
|
|
||||||
const context = {
|
|
||||||
token,
|
|
||||||
setHeaders: [],
|
setHeaders: [],
|
||||||
}
|
}
|
||||||
|
if (authorization) {
|
||||||
|
context.token = authorization.replace(/^Bearer /, '')
|
||||||
|
}
|
||||||
|
if (clientRequestTime && typeof clientRequestTime === 'string') {
|
||||||
|
context.clientRequestTime = clientRequestTime
|
||||||
|
}
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -75,6 +75,9 @@ const createServer = async (
|
|||||||
logger,
|
logger,
|
||||||
})
|
})
|
||||||
apollo.applyMiddleware({ app, path: '/' })
|
apollo.applyMiddleware({ app, path: '/' })
|
||||||
|
logger.info(
|
||||||
|
`running with PRODUCTION=${CONFIG.PRODUCTION}, sending EMAIL enabled=${CONFIG.EMAIL} and EMAIL_TEST_MODUS=${CONFIG.EMAIL_TEST_MODUS} ...`,
|
||||||
|
)
|
||||||
logger.debug('createServer...successful')
|
logger.debug('createServer...successful')
|
||||||
return { apollo, app, con }
|
return { apollo, app, con }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,39 @@
|
|||||||
import { Brackets, EntityRepository, ObjectLiteral, Repository } from '@dbTools/typeorm'
|
import SearchUsersFilters from '@/graphql/arg/SearchUsersFilters'
|
||||||
import { User } from '@entity/User'
|
import { Brackets, EntityRepository, IsNull, Not, Repository } from '@dbTools/typeorm'
|
||||||
|
import { User as DbUser } from '@entity/User'
|
||||||
|
|
||||||
@EntityRepository(User)
|
@EntityRepository(DbUser)
|
||||||
export class UserRepository extends Repository<User> {
|
export class UserRepository extends Repository<DbUser> {
|
||||||
async findByPubkeyHex(pubkeyHex: string): Promise<User> {
|
async findByPubkeyHex(pubkeyHex: string): Promise<DbUser> {
|
||||||
return this.createQueryBuilder('user')
|
const dbUser = await this.createQueryBuilder('user')
|
||||||
|
.leftJoinAndSelect('user.emailContact', 'emailContact')
|
||||||
.where('hex(user.pubKey) = :pubkeyHex', { pubkeyHex })
|
.where('hex(user.pubKey) = :pubkeyHex', { pubkeyHex })
|
||||||
.getOneOrFail()
|
.getOneOrFail()
|
||||||
|
/*
|
||||||
|
const dbUser = await this.findOneOrFail(`hex(user.pubKey) = { pubkeyHex }`)
|
||||||
|
const emailContact = await this.query(
|
||||||
|
`SELECT * from user_contacts where id = { dbUser.emailId }`,
|
||||||
|
)
|
||||||
|
dbUser.emailContact = emailContact
|
||||||
|
*/
|
||||||
|
return dbUser
|
||||||
}
|
}
|
||||||
|
|
||||||
async findBySearchCriteriaPagedFiltered(
|
async findBySearchCriteriaPagedFiltered(
|
||||||
select: string[],
|
select: string[],
|
||||||
searchCriteria: string,
|
searchCriteria: string,
|
||||||
filterCriteria: ObjectLiteral[],
|
filters: SearchUsersFilters,
|
||||||
currentPage: number,
|
currentPage: number,
|
||||||
pageSize: number,
|
pageSize: number,
|
||||||
): Promise<[User[], number]> {
|
): Promise<[DbUser[], number]> {
|
||||||
const query = await this.createQueryBuilder('user')
|
const query = this.createQueryBuilder('user')
|
||||||
.select(select)
|
.select(select)
|
||||||
|
.leftJoinAndSelect('user.emailContact', 'emailContact')
|
||||||
.withDeleted()
|
.withDeleted()
|
||||||
.where(
|
.where(
|
||||||
new Brackets((qb) => {
|
new Brackets((qb) => {
|
||||||
qb.where(
|
qb.where(
|
||||||
'user.firstName like :name or user.lastName like :lastName or user.email like :email',
|
'user.firstName like :name or user.lastName like :lastName or emailContact.email like :email',
|
||||||
{
|
{
|
||||||
name: `%${searchCriteria}%`,
|
name: `%${searchCriteria}%`,
|
||||||
lastName: `%${searchCriteria}%`,
|
lastName: `%${searchCriteria}%`,
|
||||||
@ -31,9 +42,23 @@ export class UserRepository extends Repository<User> {
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
/*
|
||||||
filterCriteria.forEach((filter) => {
|
filterCriteria.forEach((filter) => {
|
||||||
query.andWhere(filter)
|
query.andWhere(filter)
|
||||||
})
|
})
|
||||||
|
*/
|
||||||
|
if (filters) {
|
||||||
|
if (filters.byActivated !== null) {
|
||||||
|
query.andWhere('emailContact.emailChecked = :value', { value: filters.byActivated })
|
||||||
|
// filterCriteria.push({ 'emailContact.emailChecked': filters.byActivated })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.byDeleted !== null) {
|
||||||
|
// filterCriteria.push({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
|
||||||
|
query.andWhere({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return query
|
return query
|
||||||
.take(pageSize)
|
.take(pageSize)
|
||||||
.skip((currentPage - 1) * pageSize)
|
.skip((currentPage - 1) * pageSize)
|
||||||
|
|||||||
@ -2,22 +2,26 @@
|
|||||||
|
|
||||||
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
|
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
|
||||||
import { User as dbUser } from '@entity/User'
|
import { User as dbUser } from '@entity/User'
|
||||||
|
import { UserContact } from '@entity/UserContact'
|
||||||
|
// import { UserContact as EmailContact } from '@entity/UserContact'
|
||||||
import { User } from '@model/User'
|
import { User } from '@model/User'
|
||||||
|
|
||||||
const communityDbUser: dbUser = {
|
const communityDbUser: dbUser = {
|
||||||
id: -1,
|
id: -1,
|
||||||
gradidoID: '11111111-2222-4333-4444-55555555',
|
gradidoID: '11111111-2222-4333-4444-55555555',
|
||||||
alias: '',
|
alias: '',
|
||||||
email: 'support@gradido.net',
|
// email: 'support@gradido.net',
|
||||||
|
emailContact: new UserContact(),
|
||||||
|
emailId: -1,
|
||||||
firstName: 'Gradido',
|
firstName: 'Gradido',
|
||||||
lastName: 'Akademie',
|
lastName: 'Akademie',
|
||||||
pubKey: Buffer.from(''),
|
pubKey: Buffer.from(''),
|
||||||
privKey: Buffer.from(''),
|
privKey: Buffer.from(''),
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
password: BigInt(0),
|
password: BigInt(0),
|
||||||
emailHash: Buffer.from(''),
|
// emailHash: Buffer.from(''),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
emailChecked: false,
|
// emailChecked: false,
|
||||||
language: '',
|
language: '',
|
||||||
isAdmin: null,
|
isAdmin: null,
|
||||||
publisherId: 0,
|
publisherId: 0,
|
||||||
|
|||||||
@ -7,16 +7,16 @@ export async function retrieveNotRegisteredEmails(): Promise<string[]> {
|
|||||||
if (!con) {
|
if (!con) {
|
||||||
throw new Error('No connection to database')
|
throw new Error('No connection to database')
|
||||||
}
|
}
|
||||||
const users = await User.find()
|
const users = await User.find({ relations: ['emailContact'] })
|
||||||
const notRegisteredUser = []
|
const notRegisteredUser = []
|
||||||
for (let i = 0; i < users.length; i++) {
|
for (let i = 0; i < users.length; i++) {
|
||||||
const user = users[i]
|
const user = users[i]
|
||||||
try {
|
try {
|
||||||
await getKlickTippUser(user.email)
|
await getKlickTippUser(user.emailContact.email)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notRegisteredUser.push(user.email)
|
notRegisteredUser.push(user.emailContact.email)
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(`${user.email}`)
|
console.log(`${user.emailContact.email}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await con.close()
|
await con.close()
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
import { LoginElopageBuys } from '@entity/LoginElopageBuys'
|
import { LoginElopageBuys } from '@entity/LoginElopageBuys'
|
||||||
import { UserResolver } from '@/graphql/resolver/UserResolver'
|
import { UserResolver } from '@/graphql/resolver/UserResolver'
|
||||||
import { User as dbUser } from '@entity/User'
|
import { UserContact as dbUserContact } from '@entity/UserContact'
|
||||||
|
|
||||||
export const elopageWebhook = async (req: any, res: any): Promise<void> => {
|
export const elopageWebhook = async (req: any, res: any): Promise<void> => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
@ -127,7 +127,8 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do we already have such a user?
|
// Do we already have such a user?
|
||||||
if ((await dbUser.count({ email })) !== 0) {
|
// if ((await dbUser.count({ email })) !== 0) {
|
||||||
|
if ((await dbUserContact.count({ email })) !== 0) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(`Did not create User - already exists with email: ${email}`)
|
console.log(`Did not create User - already exists with email: ${email}`)
|
||||||
return
|
return
|
||||||
|
|||||||
126
database/entity/0049-add_user_contacts_table/User.ts
Normal file
126
database/entity/0049-add_user_contacts_table/User.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
DeleteDateColumn,
|
||||||
|
OneToMany,
|
||||||
|
JoinColumn,
|
||||||
|
OneToOne,
|
||||||
|
} from 'typeorm'
|
||||||
|
import { Contribution } from '../Contribution'
|
||||||
|
import { ContributionMessage } from '../ContributionMessage'
|
||||||
|
import { UserContact } from '../UserContact'
|
||||||
|
|
||||||
|
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||||
|
export class User extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'gradido_id',
|
||||||
|
length: 36,
|
||||||
|
nullable: false,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
gradidoID: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'alias',
|
||||||
|
length: 20,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
alias: string
|
||||||
|
|
||||||
|
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
|
||||||
|
pubKey: Buffer
|
||||||
|
|
||||||
|
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
|
||||||
|
privKey: Buffer
|
||||||
|
|
||||||
|
/*
|
||||||
|
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||||
|
email: string
|
||||||
|
*/
|
||||||
|
@OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user)
|
||||||
|
@JoinColumn({ name: 'email_id' })
|
||||||
|
emailContact: UserContact
|
||||||
|
|
||||||
|
@Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||||
|
emailId: number | null
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'first_name',
|
||||||
|
length: 255,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
firstName: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'last_name',
|
||||||
|
length: 255,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
lastName: string
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deletedAt: Date | null
|
||||||
|
|
||||||
|
@Column({ type: 'bigint', default: 0, unsigned: true })
|
||||||
|
password: BigInt
|
||||||
|
|
||||||
|
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
|
||||||
|
createdAt: Date
|
||||||
|
/*
|
||||||
|
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
|
||||||
|
emailChecked: boolean
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
|
||||||
|
language: string
|
||||||
|
|
||||||
|
@Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
|
||||||
|
isAdmin: Date | null
|
||||||
|
|
||||||
|
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||||
|
referrerId?: number | null
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'contribution_link_id',
|
||||||
|
type: 'int',
|
||||||
|
unsigned: true,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
})
|
||||||
|
contributionLinkId?: number | null
|
||||||
|
|
||||||
|
@Column({ name: 'publisher_id', default: 0 })
|
||||||
|
publisherId: number
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'text',
|
||||||
|
name: 'passphrase',
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
})
|
||||||
|
passphrase: string
|
||||||
|
|
||||||
|
@OneToMany(() => Contribution, (contribution) => contribution.user)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
contributions?: Contribution[]
|
||||||
|
|
||||||
|
@OneToMany(() => ContributionMessage, (message) => message.user)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
messages?: ContributionMessage[]
|
||||||
|
|
||||||
|
@OneToMany(() => UserContact, (userContact: UserContact) => userContact.user)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
userContacts?: UserContact[]
|
||||||
|
}
|
||||||
60
database/entity/0049-add_user_contacts_table/UserContact.ts
Normal file
60
database/entity/0049-add_user_contacts_table/UserContact.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
DeleteDateColumn,
|
||||||
|
OneToOne,
|
||||||
|
} from 'typeorm'
|
||||||
|
import { User } from './User'
|
||||||
|
|
||||||
|
@Entity('user_contacts', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||||
|
export class UserContact extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'type',
|
||||||
|
length: 100,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
type: string
|
||||||
|
|
||||||
|
@OneToOne(() => User, (user) => user.emailContact)
|
||||||
|
user: User
|
||||||
|
|
||||||
|
@Column({ name: 'user_id', type: 'int', unsigned: true, nullable: false })
|
||||||
|
userId: number
|
||||||
|
|
||||||
|
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||||
|
email: string
|
||||||
|
|
||||||
|
@Column({ name: 'email_verification_code', type: 'bigint', unsigned: true, unique: true })
|
||||||
|
emailVerificationCode: BigInt
|
||||||
|
|
||||||
|
@Column({ name: 'email_opt_in_type_id' })
|
||||||
|
emailOptInTypeId: number
|
||||||
|
|
||||||
|
@Column({ name: 'email_resend_count' })
|
||||||
|
emailResendCount: number
|
||||||
|
|
||||||
|
// @Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
|
||||||
|
// emailHash: Buffer
|
||||||
|
|
||||||
|
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
|
||||||
|
emailChecked: boolean
|
||||||
|
|
||||||
|
@Column({ length: 255, unique: false, nullable: true, collation: 'utf8mb4_unicode_ci' })
|
||||||
|
phone: string
|
||||||
|
|
||||||
|
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false })
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@Column({ name: 'updated_at', nullable: true, default: null, type: 'datetime' })
|
||||||
|
updatedAt: Date | null
|
||||||
|
|
||||||
|
@DeleteDateColumn({ name: 'deleted_at', nullable: true })
|
||||||
|
deletedAt: Date | null
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export { User } from './0047-messages_tables/User'
|
export { User } from './0049-add_user_contacts_table/User'
|
||||||
|
|||||||
1
database/entity/UserContact.ts
Normal file
1
database/entity/UserContact.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { UserContact } from './0049-add_user_contacts_table/UserContact'
|
||||||
@ -5,6 +5,7 @@ import { Migration } from './Migration'
|
|||||||
import { Transaction } from './Transaction'
|
import { Transaction } from './Transaction'
|
||||||
import { TransactionLink } from './TransactionLink'
|
import { TransactionLink } from './TransactionLink'
|
||||||
import { User } from './User'
|
import { User } from './User'
|
||||||
|
import { UserContact } from './UserContact'
|
||||||
import { Contribution } from './Contribution'
|
import { Contribution } from './Contribution'
|
||||||
import { EventProtocol } from './EventProtocol'
|
import { EventProtocol } from './EventProtocol'
|
||||||
import { ContributionMessage } from './ContributionMessage'
|
import { ContributionMessage } from './ContributionMessage'
|
||||||
@ -20,4 +21,5 @@ export const entities = [
|
|||||||
User,
|
User,
|
||||||
EventProtocol,
|
EventProtocol,
|
||||||
ContributionMessage,
|
ContributionMessage,
|
||||||
|
UserContact,
|
||||||
]
|
]
|
||||||
|
|||||||
129
database/migrations/0049-add_user_contacts_table.ts
Normal file
129
database/migrations/0049-add_user_contacts_table.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
/* MIGRATION TO ADD GRADIDO_ID
|
||||||
|
*
|
||||||
|
* This migration adds new columns to the table `users` and creates the
|
||||||
|
* new table `user_contacts`
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
await queryFn(`
|
||||||
|
CREATE TABLE IF NOT EXISTS \`user_contacts\` (
|
||||||
|
\`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
\`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_resend_count\` int DEFAULT '0',
|
||||||
|
\`email_checked\` tinyint(4) NOT NULL DEFAULT 0,
|
||||||
|
\`phone\` varchar(255) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
|
||||||
|
\`created_at\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
\`updated_at\` datetime(3) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
\`deleted_at\` datetime(3) NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (\`id\`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`)
|
||||||
|
|
||||||
|
await queryFn('ALTER TABLE `users` ADD COLUMN `email_id` int(10) NULL AFTER `email`;')
|
||||||
|
// define datetime column with a precision of 3 milliseconds
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE `users` MODIFY COLUMN `created` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) AFTER `email_hash`;',
|
||||||
|
)
|
||||||
|
// define datetime column with a precision of 3 milliseconds
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE `users` MODIFY COLUMN `deletedAt` datetime(3) NULL DEFAULT NULL AFTER `last_name`;',
|
||||||
|
)
|
||||||
|
// define datetime column with a precision of 3 milliseconds
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE `users` MODIFY COLUMN `is_admin` datetime(3) NULL DEFAULT NULL AFTER `language`;',
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 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`)
|
||||||
|
for (const id in contacts) {
|
||||||
|
const contact = contacts[id]
|
||||||
|
await queryFn(
|
||||||
|
`UPDATE users as u SET u.email_id = "${contact.id}" WHERE u.id = "${contact.user_id}"`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// these steps comes after verification and test
|
||||||
|
await queryFn('ALTER TABLE users DROP COLUMN email;')
|
||||||
|
await queryFn('ALTER TABLE users DROP COLUMN email_checked;')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
// this step comes after verification and test
|
||||||
|
await queryFn('ALTER TABLE users ADD COLUMN email varchar(255) NULL AFTER privkey;')
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE users ADD COLUMN email_checked tinyint(4) NOT NULL DEFAULT 0 AFTER email;',
|
||||||
|
)
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE `users` MODIFY COLUMN `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `email_hash`;',
|
||||||
|
)
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE `users` MODIFY COLUMN `deletedAt` datetime NULL DEFAULT NULL AFTER `last_name`;',
|
||||||
|
)
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE `users` MODIFY COLUMN `is_admin` datetime NULL DEFAULT NULL AFTER `language`;',
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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`)
|
||||||
|
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}"`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await queryFn('ALTER TABLE users MODIFY COLUMN email varchar(255) NOT NULL UNIQUE;')
|
||||||
|
|
||||||
|
// write downgrade logic as parameter of queryFn
|
||||||
|
await queryFn(`DROP TABLE IF EXISTS user_contacts;`)
|
||||||
|
|
||||||
|
await queryFn('ALTER TABLE users DROP COLUMN email_id;')
|
||||||
|
}
|
||||||
@ -26,10 +26,11 @@ COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
|
|||||||
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
|
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
|
||||||
|
|
||||||
# backend
|
# backend
|
||||||
BACKEND_CONFIG_VERSION=v9.2022-07-07
|
BACKEND_CONFIG_VERSION=v10.2022-09-20
|
||||||
|
|
||||||
JWT_EXPIRES_IN=10m
|
JWT_EXPIRES_IN=10m
|
||||||
GDT_API_URL=https://gdt.gradido.net
|
GDT_API_URL=https://gdt.gradido.net
|
||||||
|
ENV_NAME=stage1
|
||||||
|
|
||||||
TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log
|
TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log
|
||||||
|
|
||||||
@ -40,6 +41,8 @@ KLICKTIPP_APIKEY_DE=
|
|||||||
KLICKTIPP_APIKEY_EN=
|
KLICKTIPP_APIKEY_EN=
|
||||||
|
|
||||||
EMAIL=true
|
EMAIL=true
|
||||||
|
EMAIL_TEST_MODUS=false
|
||||||
|
EMAIL_TEST_RECEIVER=test_team@gradido.net
|
||||||
EMAIL_USERNAME=peter@lustig.de
|
EMAIL_USERNAME=peter@lustig.de
|
||||||
EMAIL_SENDER=peter@lustig.de
|
EMAIL_SENDER=peter@lustig.de
|
||||||
EMAIL_PASSWORD=1234
|
EMAIL_PASSWORD=1234
|
||||||
|
|||||||
@ -131,6 +131,10 @@ envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/frontend/.env
|
|||||||
# Configure admin
|
# Configure admin
|
||||||
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/admin/.env.template > $PROJECT_ROOT/admin/.env
|
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/admin/.env.template > $PROJECT_ROOT/admin/.env
|
||||||
|
|
||||||
|
# create cronjob to delete yarn output in /tmp
|
||||||
|
# crontab -e
|
||||||
|
# hourly job: 0 * * * * find /tmp -name "yarn--*" -cmin +60 -exec rm -r {} \; > /dev/null
|
||||||
|
# daily job: 0 4 * * * find /tmp -name "yarn--*" -ctime +1 -exec rm -r {} \; > /dev/null
|
||||||
# Start gradido
|
# Start gradido
|
||||||
# Note: on first startup some errors will occur - nothing serious
|
# Note: on first startup some errors will occur - nothing serious
|
||||||
./start.sh
|
./start.sh
|
||||||
@ -95,5 +95,13 @@
|
|||||||
> cp .env.dist .env
|
> cp .env.dist .env
|
||||||
> nano .env
|
> nano .env
|
||||||
>> Adjust values accordingly
|
>> 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
|
# TODO the install.sh is not yet ready to run directly - consider to use it as pattern to do it manually
|
||||||
> ./install.sh
|
> ./install.sh
|
||||||
|
|||||||
@ -59,3 +59,4 @@ networks:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db_test_vol:
|
db_test_vol:
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
To introduce the Gradido-ID base on the requirement to identify an user account per technical key instead of using an email-address. Such a technical key ensures an exact identification of an user account without giving detailed information for possible missusage.
|
The introduction of the Gradido-ID base on the requirement to identify an user account per technical key instead of using an email-address. Such a technical key ensures an exact identification of an user account without giving detailed information for possible missusage.
|
||||||
|
|
||||||
Additionally the Gradido-ID allows to administrade any user account data like changing the email address or define several email addresses without any side effects on the identification of the user account.
|
Additionally the Gradido-ID allows to administrade any user account data like changing the email address or define several email addresses without any side effects on the identification of the user account.
|
||||||
|
|
||||||
@ -22,12 +22,12 @@ The second step is to decribe all concerning business logic processes, which hav
|
|||||||
|
|
||||||
The entity users has to be changed by adding the following columns.
|
The entity users has to be changed by adding the following columns.
|
||||||
|
|
||||||
| Column | Type | Description |
|
| Column | Type | Description |
|
||||||
| ------------------------ | ------ | -------------------------------------------------------------------------------------- |
|
| ------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
| gradidoID | String | technical unique key of the user as UUID (version 4) |
|
| gradidoID | String | technical unique key of the user as UUID (version 4) |
|
||||||
| alias | String | a business unique key of the user |
|
| alias | String | a business unique key of the user |
|
||||||
| passphraseEncryptionType | int | defines the type of encrypting the passphrase: 1 = email (default), 2 = gradidoID, ... |
|
| passphraseEncryptionType | int | defines the type of encrypting the passphrase: 1 = email (default), 2 = gradidoID, ... |
|
||||||
| emailID | int | technical foreign key to the new entity Contact |
|
| emailID | int | technical foreign key to the entry with type Email and contactChannel=maincontact of the new entity UserContacts |
|
||||||
|
|
||||||
##### Email vs emailID
|
##### Email vs emailID
|
||||||
|
|
||||||
@ -39,14 +39,21 @@ The preferred and proper solution will be to add a new column `Users.emailId `as
|
|||||||
|
|
||||||
A new entity `UserContacts `is introduced to store several contacts of different types like email, telephone or other kinds of contact addresses.
|
A new entity `UserContacts `is introduced to store several contacts of different types like email, telephone or other kinds of contact addresses.
|
||||||
|
|
||||||
| Column | Type | Description |
|
| Column | Type | Description |
|
||||||
| --------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| id | int | the technical key of a contact entity |
|
| id | int | the technical key of a contact entity |
|
||||||
| type | int | Defines the type of contact entry as enum: Email, Phone, etc |
|
| type | int | Defines the type of contact entry as enum: Email, Phone, etc |
|
||||||
| usersID | int | Defines the foreign key to the `Users` table |
|
| userID | int | Defines the foreign key to the `Users` table |
|
||||||
| email | String | defines the address of a contact entry of type Email |
|
| email | String | defines the address of a contact entry of type Email |
|
||||||
| phone | String | defines the address of a contact entry of type Phone |
|
| emailVerificationCode | unsinged bigint(20) | unique code to verify email or password reset |
|
||||||
| contactChannels | String | define the contact channel as comma separated list for which this entry is confirmed by the user e.g. main contact (default), infomail, contracting, advertisings, ... |
|
| emailOptInType | int | REGISTER=1, RESET_PASSWORD=2 |
|
||||||
|
| emailResendCount | int | counter how often the email was resend |
|
||||||
|
| emailChecked | boolean | flag if email is verified and confirmed |
|
||||||
|
| createdAt | DateTime | point of time the Contact was created |
|
||||||
|
| updatedAt | DateTime | point of time the Contact was updated |
|
||||||
|
| deletedAt | DateTime | point of time the Contact was soft deleted |
|
||||||
|
| phone | String | defines the address of a contact entry of type Phone |
|
||||||
|
| contactChannels | String | define the contact channel as comma separated list for which this entry is confirmed by the user e.g. main contact (default), infomail, contracting, advertisings, ... |
|
||||||
|
|
||||||
### Database-Migration
|
### Database-Migration
|
||||||
|
|
||||||
@ -58,18 +65,24 @@ In a one-time migration create for each entry of the `Users `tabel an unique UUI
|
|||||||
|
|
||||||
#### Primary Email Contact
|
#### Primary Email Contact
|
||||||
|
|
||||||
In a one-time migration read for each entry of the `Users `table the `Users.id` and `Users.email` and create for it a new entry in the `UsersContact `table, by initializing the contact-values with:
|
In a one-time migration read for each entry of the `Users `table the `Users.id` and `Users.email`, select from the table `login_email_opt_in` the entry with the `login_email_opt_in.user_id` = `Users.id` and create a new entry in the `UsersContact `table, by initializing the contact-values with:
|
||||||
|
|
||||||
* id = new technical key
|
* id = new technical key
|
||||||
* type = Enum-Email
|
* type = Enum-Email
|
||||||
* userID = `Users.id`
|
* userID = `Users.id`
|
||||||
* email = `Users.email`
|
* email = `Users.email`
|
||||||
|
* emailVerifyCode = `login_email_opt_in.verification_code`
|
||||||
|
* emailOptInType = `login_email_opt_in.email_opt_in_type_id`
|
||||||
|
* emailResendCount = `login_email_opt_in.resent_count`
|
||||||
|
* emailChecked = `Users.emailChecked`
|
||||||
|
* createdAt = `login_email_opt_in.created_at`
|
||||||
|
* updatedAt = `login_email_opt_in.updated_at`
|
||||||
* phone = null
|
* phone = null
|
||||||
* usedChannel = Enum-"main contact"
|
* usedChannel = Enum-"main contact"
|
||||||
|
|
||||||
and update the `Users `entry with `Users.emailId = UsersContact.Id` and `Users.passphraseEncryptionType = 1`
|
and update the `Users `entry with `Users.emailId = UsersContact.Id` and `Users.passphraseEncryptionType = 1`
|
||||||
|
|
||||||
After this one-time migration the column `Users.email` can be deleted.
|
After this one-time migration and a verification, which ensures that all data are migrated, then the columns `Users.email`, `Users.emailChecked`, `Users.emailHash` and the table `login_email_opt_in` can be deleted.
|
||||||
|
|
||||||
### Adaption of BusinessLogic
|
### Adaption of BusinessLogic
|
||||||
|
|
||||||
@ -109,7 +122,7 @@ The logic of change password has to be adapted by
|
|||||||
|
|
||||||
* read the users email address from the `UsersContact `table
|
* read the users email address from the `UsersContact `table
|
||||||
* give the email address as input for the password decryption of the existing password
|
* give the email address as input for the password decryption of the existing password
|
||||||
* use the `Users.userID` as input for the password encryption fo the new password
|
* use the `Users.userID` as input for the password encryption for the new password
|
||||||
* change the `Users.passphraseEnrycptionType` to the new value =2
|
* change the `Users.passphraseEnrycptionType` to the new value =2
|
||||||
* if the `Users.passphraseEncryptionType` = 2, then
|
* if the `Users.passphraseEncryptionType` = 2, then
|
||||||
|
|
||||||
@ -129,11 +142,17 @@ A new logic has to be introduced to search the user identity per different input
|
|||||||
A new mapping logic will be necessary to allow using unmigrated APIs like GDT-servers api. So it must be possible to give this identity-mapping logic the following input to get the respective output:
|
A new mapping logic will be necessary to allow using unmigrated APIs like GDT-servers api. So it must be possible to give this identity-mapping logic the following input to get the respective output:
|
||||||
|
|
||||||
* email -> userID
|
* email -> userID
|
||||||
|
* email -> gradidoID
|
||||||
* email -> alias
|
* email -> alias
|
||||||
|
* userID -> gradidoID
|
||||||
* userID -> email
|
* userID -> email
|
||||||
* userID -> alias
|
* userID -> alias
|
||||||
|
* alias -> gradidoID
|
||||||
* alias -> email
|
* alias -> email
|
||||||
* alias -> userID
|
* alias -> userID
|
||||||
|
* gradidoID -> email
|
||||||
|
* gradidoID -> userID
|
||||||
|
* gradidoID -> alias
|
||||||
|
|
||||||
#### GDT-Access
|
#### GDT-Access
|
||||||
|
|
||||||
|
|||||||
7
e2e-tests/README.md
Normal file
7
e2e-tests/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Gradido end-to-end tests
|
||||||
|
|
||||||
|
This is still WIP.
|
||||||
|
|
||||||
|
For automated end-to-end testing one of the frameworks Cypress or Playwright will be utilized.
|
||||||
|
|
||||||
|
For more details on how to run them, see the subfolders' README instructions.
|
||||||
4
e2e-tests/cypress/.gitignore
vendored
Normal file
4
e2e-tests/cypress/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
tests/node_modules/
|
||||||
|
tests/cypress/screenshots/
|
||||||
|
tests/cypress/videos/
|
||||||
|
tests/cucumber-messages.ndjson
|
||||||
36
e2e-tests/cypress/Dockerfile
Normal file
36
e2e-tests/cypress/Dockerfile
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
###############################################################################
|
||||||
|
# Dockerfile to create a ready-to-use Cypress Docker image for end-to-end
|
||||||
|
# testing.
|
||||||
|
#
|
||||||
|
# Based on the images containing several browsers, provided by Cypress.io
|
||||||
|
# (https://github.com/cypress-io/cypress-docker-images/tree/master/browsers)
|
||||||
|
# this Dockerfile is based a slim Linux Dockerfile using Node.js 16.14.2.
|
||||||
|
#
|
||||||
|
# Here the latest stable versions of the browsers Chromium and Firefox are
|
||||||
|
# installed before installing Cypress.
|
||||||
|
###############################################################################
|
||||||
|
FROM cypress/base:16.14.2-slim
|
||||||
|
|
||||||
|
ARG DOCKER_WORKDIR=/tests/
|
||||||
|
WORKDIR $DOCKER_WORKDIR
|
||||||
|
|
||||||
|
# install dependencies
|
||||||
|
RUN apt-get -qq update > /dev/null && \
|
||||||
|
apt-get -qq install -y bzip2 mplayer wget > /dev/null
|
||||||
|
|
||||||
|
# install Chromium browser
|
||||||
|
RUN apt-get -qq install -y chromium > /dev/null
|
||||||
|
|
||||||
|
# install Firefox browser
|
||||||
|
RUN wget --no-verbose -O /tmp/firefox.tar.bz2 "https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US" && \
|
||||||
|
tar -C /opt -xjf /tmp/firefox.tar.bz2 && \
|
||||||
|
rm /tmp/firefox.tar.bz2 && \
|
||||||
|
ln -fs /opt/firefox/firefox /usr/bin/firefox
|
||||||
|
|
||||||
|
# clean up
|
||||||
|
RUN rm -rf /var/lib/apt/lists/* && apt-get -qq clean > /dev/null
|
||||||
|
|
||||||
|
COPY tests/package.json tests/yarn.lock $DOCKER_WORKDIR
|
||||||
|
|
||||||
|
RUN yarn install
|
||||||
|
COPY tests/ $DOCKER_WORKDIR
|
||||||
24
e2e-tests/cypress/README.md
Normal file
24
e2e-tests/cypress/README.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Gradido End-to-End Testing with [Cypress](https://www.cypress.io/) (CI-ready via Docker)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
to boot up the DEV system, before running the test.
|
||||||
|
|
||||||
|
## Execute the test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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
|
||||||
|
```
|
||||||
1
e2e-tests/cypress/tests/.eslintignore
Normal file
1
e2e-tests/cypress/tests/.eslintignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
24
e2e-tests/cypress/tests/.eslintrc.js
Normal file
24
e2e-tests/cypress/tests/.eslintrc.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["cypress", "prettier", "@typescript-eslint"],
|
||||||
|
extends: [
|
||||||
|
"standard",
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"no-console": ["error"],
|
||||||
|
"no-debugger": "error",
|
||||||
|
"prettier/prettier": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
htmlWhitespaceSensitivity: "ignore",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
64
e2e-tests/cypress/tests/cypress.config.ts
Normal file
64
e2e-tests/cypress/tests/cypress.config.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { defineConfig } from "cypress";
|
||||||
|
import { addCucumberPreprocessorPlugin } from "@badeball/cypress-cucumber-preprocessor";
|
||||||
|
import browserify from "@badeball/cypress-cucumber-preprocessor/browserify";
|
||||||
|
|
||||||
|
async function setupNodeEvents(
|
||||||
|
on: Cypress.PluginEvents,
|
||||||
|
config: Cypress.PluginConfigOptions
|
||||||
|
): Promise<Cypress.PluginConfigOptions> {
|
||||||
|
await addCucumberPreprocessorPlugin(on, config);
|
||||||
|
|
||||||
|
on(
|
||||||
|
"file:preprocessor",
|
||||||
|
browserify(config, {
|
||||||
|
typescript: require.resolve("typescript"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
on("after:run", (results) => {
|
||||||
|
if (results) {
|
||||||
|
// results will be undefined in interactive mode
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(results.status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
e2e: {
|
||||||
|
specPattern: "**/*.feature",
|
||||||
|
excludeSpecPattern: "*.js",
|
||||||
|
baseUrl: "http://localhost:3000",
|
||||||
|
chromeWebSecurity: false,
|
||||||
|
supportFile: "cypress/support/index.ts",
|
||||||
|
viewportHeight: 720,
|
||||||
|
viewportWidth: 1280,
|
||||||
|
retries: {
|
||||||
|
runMode: 2,
|
||||||
|
openMode: 0,
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
backendURL: "http://localhost:4000",
|
||||||
|
loginQuery: `query ($email: String!, $password: String!, $publisherId: Int) {
|
||||||
|
login(email: $email, password: $password, publisherId: $publisherId) {
|
||||||
|
email
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
language
|
||||||
|
klickTipp {
|
||||||
|
newsletterState
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
hasElopage
|
||||||
|
publisherId
|
||||||
|
isAdmin
|
||||||
|
creation
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
setupNodeEvents,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
Feature: User authentication
|
||||||
|
As a user
|
||||||
|
I want to be able to sign in - only with valid credentials
|
||||||
|
In order to be able to posts and do other contributions as myself
|
||||||
|
Furthermore I want to be able to stay logged in and logout again
|
||||||
|
|
||||||
|
# TODO for these pre-conditions utilize seeding or API check, if user exists in test system
|
||||||
|
# Background:
|
||||||
|
# Given the following "users" are in the database:
|
||||||
|
# | email | password | name |
|
||||||
|
# | bibi@bloxberg.de | Aa12345_ | Bibi Bloxberg |
|
||||||
|
|
||||||
|
Scenario: Log in successfully
|
||||||
|
Given the browser navigates to page "/login"
|
||||||
|
When the user submits the credentials "bibi@bloxberg.de" "Aa12345_"
|
||||||
|
Then the user is logged in with username "Bibi Bloxberg"
|
||||||
|
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
Feature: User profile - change password
|
||||||
|
As a user
|
||||||
|
I want the option to change my password on my profile page.
|
||||||
|
|
||||||
|
Background:
|
||||||
|
# TODO for these pre-conditions utilize seeding or API check, if user exists in test system
|
||||||
|
# Given the following "users" are in the database:
|
||||||
|
# | email | password | name |
|
||||||
|
# | bibi@bloxberg.de | Aa12345_ | Bibi Bloxberg | |
|
||||||
|
|
||||||
|
# TODO instead of credentials use the name of an user object (see seeds in backend)
|
||||||
|
Given the user is logged in as "bibi@bloxberg.de" "Aa12345_"
|
||||||
|
|
||||||
|
Scenario: Change password successfully
|
||||||
|
Given the browser navigates to page "/profile"
|
||||||
|
And the user opens the change password menu
|
||||||
|
When the user fills the password form with:
|
||||||
|
| Old password | Aa12345_ |
|
||||||
|
| New password | 12345Aa_ |
|
||||||
|
| Repeat new password | 12345Aa_ |
|
||||||
|
And the user submits the password form
|
||||||
|
And the user is presented a "success" message
|
||||||
|
And the user logs out
|
||||||
|
Then the user submits the credentials "bibi@bloxberg.de" "Aa12345_"
|
||||||
|
And the user cannot login
|
||||||
|
But the user submits the credentials "bibi@bloxberg.de" "12345Aa_"
|
||||||
|
And the user is logged in with username "Bibi Bloxberg"
|
||||||
30
e2e-tests/cypress/tests/cypress/e2e/models/LoginPage.ts
Normal file
30
e2e-tests/cypress/tests/cypress/e2e/models/LoginPage.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
export class LoginPage {
|
||||||
|
// selectors
|
||||||
|
emailInput = "#Email-input-field";
|
||||||
|
passwordInput = "#Password-input-field";
|
||||||
|
submitBtn = "[type=submit]";
|
||||||
|
emailHint = "#vee_Email";
|
||||||
|
passwordHint = "#vee_Password";
|
||||||
|
|
||||||
|
goto() {
|
||||||
|
cy.visit("/");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
enterEmail(email: string) {
|
||||||
|
cy.get(this.emailInput).clear().type(email);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
enterPassword(password: string) {
|
||||||
|
cy.get(this.passwordInput).clear().type(password);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitLogin() {
|
||||||
|
cy.get(this.submitBtn).click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
e2e-tests/cypress/tests/cypress/e2e/models/OverviewPage.ts
Normal file
10
e2e-tests/cypress/tests/cypress/e2e/models/OverviewPage.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
export class OverviewPage {
|
||||||
|
navbarName = '[data-test="navbar-item-username"]';
|
||||||
|
|
||||||
|
goto() {
|
||||||
|
cy.visit("/overview");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
e2e-tests/cypress/tests/cypress/e2e/models/ProfilePage.ts
Normal file
35
e2e-tests/cypress/tests/cypress/e2e/models/ProfilePage.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
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";
|
||||||
|
submitNewPasswordBtn = "[data-test=submit-new-password-btn]";
|
||||||
|
|
||||||
|
goto() {
|
||||||
|
cy.visit("/profile");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
enterOldPassword(password: string) {
|
||||||
|
cy.get(this.oldPasswordInput).clear().type(password);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
enterNewPassword(password: string) {
|
||||||
|
cy.get(this.newPasswordInput).clear().type(password);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
enterRepeatPassword(password: string) {
|
||||||
|
cy.get(this.newPasswordRepeatInput).clear().type(password);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitPasswordForm() {
|
||||||
|
cy.get(this.submitNewPasswordBtn).click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
e2e-tests/cypress/tests/cypress/e2e/models/SideNavMenu.ts
Normal file
17
e2e-tests/cypress/tests/cypress/e2e/models/SideNavMenu.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
export class SideNavMenu {
|
||||||
|
// selectors
|
||||||
|
profileMenu = "[data-test=profile-menu]";
|
||||||
|
logoutMenu = "[data-test=logout-menu]";
|
||||||
|
|
||||||
|
openUserProfile() {
|
||||||
|
cy.get(this.profileMenu).click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
cy.get(this.logoutMenu).click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
e2e-tests/cypress/tests/cypress/e2e/models/Toasts.ts
Normal file
7
e2e-tests/cypress/tests/cypress/e2e/models/Toasts.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
export class Toasts {
|
||||||
|
// selectors
|
||||||
|
toastTitle = ".gdd-toaster-title";
|
||||||
|
toastMessage = ".gdd-toaster-body";
|
||||||
|
}
|
||||||
7
e2e-tests/cypress/tests/cypress/fixtures/users.json
Normal file
7
e2e-tests/cypress/tests/cypress/fixtures/users.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"email": "bibi@bloxberg.de",
|
||||||
|
"password": "Aa12345_",
|
||||||
|
"name": "Bibi Bloxberg"
|
||||||
|
}
|
||||||
|
}
|
||||||
38
e2e-tests/cypress/tests/cypress/support/e2e.ts
Normal file
38
e2e-tests/cypress/tests/cypress/support/e2e.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import jwtDecode from "jwt-decode";
|
||||||
|
|
||||||
|
Cypress.Commands.add("login", (email, password) => {
|
||||||
|
cy.clearLocalStorage("vuex");
|
||||||
|
|
||||||
|
cy.request({
|
||||||
|
method: "POST",
|
||||||
|
url: Cypress.env("backendURL"),
|
||||||
|
body: {
|
||||||
|
operationName: null,
|
||||||
|
variables: {
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
},
|
||||||
|
query: Cypress.env("loginQuery"),
|
||||||
|
},
|
||||||
|
}).then(async (response) => {
|
||||||
|
const token = response.headers.token;
|
||||||
|
let tokenTime;
|
||||||
|
|
||||||
|
// to avoid JWT InvalidTokenError, the decoding of the token is wrapped
|
||||||
|
// in a try-catch block (see
|
||||||
|
// https://github.com/auth0/jwt-decode/issues/65#issuecomment-395493807)
|
||||||
|
try {
|
||||||
|
tokenTime = jwtDecode(token).exp;
|
||||||
|
} catch (tokenDecodingError) {
|
||||||
|
cy.log("JWT decoding error: ", tokenDecodingError);
|
||||||
|
}
|
||||||
|
|
||||||
|
const vuexToken = {
|
||||||
|
token: token,
|
||||||
|
tokenTime: tokenTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
cy.visit("/");
|
||||||
|
window.localStorage.setItem("vuex", JSON.stringify(vuexToken));
|
||||||
|
});
|
||||||
|
});
|
||||||
14
e2e-tests/cypress/tests/cypress/support/index.ts
Normal file
14
e2e-tests/cypress/tests/cypress/support/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-namespace */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
import "./e2e";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable<Subject> {
|
||||||
|
login(email: string, password: string): Chainable<any>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
import { Given, Then, When } from "@badeball/cypress-cucumber-preprocessor";
|
||||||
|
import { LoginPage } from "../../e2e/models/LoginPage";
|
||||||
|
import { OverviewPage } from "../../e2e/models/OverviewPage";
|
||||||
|
import { SideNavMenu } from "../../e2e/models/SideNavMenu";
|
||||||
|
import { Toasts } from "../../e2e/models/Toasts";
|
||||||
|
|
||||||
|
Given("the browser navigates to page {string}", (page: string) => {
|
||||||
|
cy.visit(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
// login-related
|
||||||
|
|
||||||
|
Given(
|
||||||
|
"the user is logged in as {string} {string}",
|
||||||
|
(email: string, password: string) => {
|
||||||
|
cy.login(email, password);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Then("the user is logged in with username {string}", (username: string) => {
|
||||||
|
const overviewPage = new OverviewPage();
|
||||||
|
cy.url().should("include", "/overview");
|
||||||
|
cy.get(overviewPage.navbarName).should("contain", username);
|
||||||
|
});
|
||||||
|
|
||||||
|
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."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
When(
|
||||||
|
"the user submits the credentials {string} {string}",
|
||||||
|
(email: string, password: string) => {
|
||||||
|
const loginPage = new LoginPage();
|
||||||
|
loginPage.enterEmail(email);
|
||||||
|
loginPage.enterPassword(password);
|
||||||
|
loginPage.submitLogin();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// logout
|
||||||
|
|
||||||
|
Then("the user logs out", () => {
|
||||||
|
const sideNavMenu = new SideNavMenu();
|
||||||
|
sideNavMenu.logout();
|
||||||
|
});
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { When } from "@badeball/cypress-cucumber-preprocessor";
|
||||||
|
import { LoginPage } from "../../e2e/models/LoginPage";
|
||||||
|
|
||||||
|
When("the user submits no credentials", () => {
|
||||||
|
const loginPage = new LoginPage();
|
||||||
|
loginPage.submitLogin();
|
||||||
|
});
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { And, When } from "@badeball/cypress-cucumber-preprocessor";
|
||||||
|
import { ProfilePage } from "../../e2e/models/ProfilePage";
|
||||||
|
import { Toasts } from "../../e2e/models/Toasts";
|
||||||
|
|
||||||
|
const profilePage = new ProfilePage();
|
||||||
|
|
||||||
|
And("the user opens the change password menu", () => {
|
||||||
|
cy.get(profilePage.openChangePassword).click();
|
||||||
|
cy.get(profilePage.newPasswordRepeatInput).should("be.visible");
|
||||||
|
cy.get(profilePage.submitNewPasswordBtn).should("be.disabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
When("the user fills the password form with:", (table) => {
|
||||||
|
table = table.rowsHash();
|
||||||
|
profilePage.enterOldPassword(table["Old password"]);
|
||||||
|
profilePage.enterNewPassword(table["New password"]);
|
||||||
|
profilePage.enterRepeatPassword(table["Repeat new password"]);
|
||||||
|
cy.get(profilePage.submitNewPasswordBtn).should("be.enabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
And("the user submits the password form", () => {
|
||||||
|
profilePage.submitPasswordForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
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."
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { And, When } from "@badeball/cypress-cucumber-preprocessor";
|
||||||
|
import { RegistrationPage } from "../../e2e/models/RegistrationPage";
|
||||||
|
|
||||||
|
const registrationPage = new RegistrationPage();
|
||||||
|
|
||||||
|
When(
|
||||||
|
"the user fills name and email {string} {string} {string}",
|
||||||
|
(firstname: string, lastname: string, email: string) => {
|
||||||
|
const registrationPage = new RegistrationPage();
|
||||||
|
registrationPage.enterFirstname(firstname);
|
||||||
|
registrationPage.enterLastname(lastname);
|
||||||
|
registrationPage.enterEmail(email);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
And("the user agrees to the privacy policy", () => {
|
||||||
|
registrationPage.checkPrivacyCheckbox();
|
||||||
|
});
|
||||||
|
|
||||||
|
And("the user submits the registration form", () => {
|
||||||
|
registrationPage.submitRegistrationPage();
|
||||||
|
cy.get(registrationPage.RegistrationThanxHeadline).should("be.visible");
|
||||||
|
cy.get(registrationPage.RegistrationThanxText).should("be.visible");
|
||||||
|
});
|
||||||
39
e2e-tests/cypress/tests/package.json
Normal file
39
e2e-tests/cypress/tests/package.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "gradido-e2e-tests-cypress",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "End-to-end tests with Cypress",
|
||||||
|
"main": "yarn run cypress run",
|
||||||
|
"repository": "https://github.com/gradido/gradido/e2e-tests/cypress",
|
||||||
|
"author": "Mathias Lenz",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"private": false,
|
||||||
|
"cypress-cucumber-preprocessor": {
|
||||||
|
"nonGlobalStepDefinitions": true,
|
||||||
|
"json": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"cypress": "cypress run",
|
||||||
|
"lint": "eslint --max-warnings=0 --ext .js,.ts ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@badeball/cypress-cucumber-preprocessor": "^12.0.0",
|
||||||
|
"@cypress/browserify-preprocessor": "^3.0.2",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.38.0",
|
||||||
|
"@typescript-eslint/parser": "^5.38.0",
|
||||||
|
"cypress": "^10.4.0",
|
||||||
|
"eslint": "^8.23.1",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-config-standard": "^16.0.3",
|
||||||
|
"eslint-loader": "^4.0.2",
|
||||||
|
"eslint-plugin-cypress": "^2.12.1",
|
||||||
|
"eslint-plugin-import": "^2.23.4",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"eslint-plugin-promise": "^5.1.0",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"typescript": "^4.7.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
e2e-tests/cypress/tests/tsconfig.json
Normal file
10
e2e-tests/cypress/tests/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2016",
|
||||||
|
"lib": ["es6", "dom"],
|
||||||
|
"baseUrl": "../node_modules",
|
||||||
|
"types": ["cypress", "node"],
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"]
|
||||||
|
}
|
||||||
5169
e2e-tests/cypress/tests/yarn.lock
Normal file
5169
e2e-tests/cypress/tests/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
42
e2e-tests/playwright/Dockerfile
Normal file
42
e2e-tests/playwright/Dockerfile
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
###############################################################################
|
||||||
|
# Dockerfile to create a ready-to-use Playwright Docker image for end-to-end
|
||||||
|
# testing.
|
||||||
|
#
|
||||||
|
# To avoid hardcoded versoning of Playwright, this Dockerfile is a custom
|
||||||
|
# version of the ready-to-use Dockerfile privided by Playwright developement
|
||||||
|
# (https://github.com/microsoft/playwright/blob/main/utils/docker/Dockerfile.focal)
|
||||||
|
#
|
||||||
|
# Here the latest stable versions of the browsers Chromium, Firefox, and Webkit
|
||||||
|
# (Safari) are installed, icluding all dependencies based on Ubuntu specified by
|
||||||
|
# Playwright developement.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
FROM ubuntu:focal
|
||||||
|
|
||||||
|
# set a timezone for the Playwright browser dependency installation
|
||||||
|
ARG TZ=Europe/Berlin
|
||||||
|
|
||||||
|
ARG DOCKER_WORKDIR=/tests/
|
||||||
|
WORKDIR $DOCKER_WORKDIR
|
||||||
|
|
||||||
|
# package manager preparation
|
||||||
|
RUN apt-get -qq update && apt-get install -qq -y curl gpg > /dev/null
|
||||||
|
# for Node.js
|
||||||
|
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash -
|
||||||
|
# for Yarn
|
||||||
|
RUN curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
||||||
|
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||||
|
|
||||||
|
# install node v16 and Yarn
|
||||||
|
RUN apt-get -qq update && apt-get install -qq -y nodejs yarn
|
||||||
|
|
||||||
|
COPY tests/package.json tests/yarn.lock $DOCKER_WORKDIR
|
||||||
|
|
||||||
|
# install Playwright with all dependencies
|
||||||
|
# for the browsers chromium, firefox, and webkit
|
||||||
|
RUN yarn install && yarn playwright install --with-deps
|
||||||
|
|
||||||
|
# clean up
|
||||||
|
RUN rm -rf /var/lib/apt/lists/* && apt-get -qq clean
|
||||||
|
|
||||||
|
COPY tests/ $DOCKER_WORKDIR
|
||||||
24
e2e-tests/playwright/README.md
Normal file
24
e2e-tests/playwright/README.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Gradido End-to-End Testing with [Playwright](https://playwright.dev/) (CI-ready via Docker)
|
||||||
|
|
||||||
|
|
||||||
|
A sample setup to show-case Playwright (using Typescript) as an end-to-end testing tool for Gradido runniing 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
to boot up the DEV system, before running the test.
|
||||||
|
|
||||||
|
## Execute the test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# build a Docker image from the Dockerfile
|
||||||
|
docker build -t gradido_e2e-tests-playwright .
|
||||||
|
|
||||||
|
# run the Docker container and execute the given tests
|
||||||
|
docker run -it --network=host gradido_e2e-tests-playwright yarn playwright-e2e-tests
|
||||||
|
```
|
||||||
8
e2e-tests/playwright/tests/global-setup.ts
Normal file
8
e2e-tests/playwright/tests/global-setup.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { FullConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
async function globalSetup(config: FullConfig) {
|
||||||
|
process.env.EMAIL = 'bibi@bloxberg.de';
|
||||||
|
process.env.PASSWORD = 'Aa12345_';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default globalSetup;
|
||||||
15
e2e-tests/playwright/tests/gradido_login.spec.ts
Normal file
15
e2e-tests/playwright/tests/gradido_login.spec.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { LoginPage } from './models/login_page';
|
||||||
|
import { WelcomePage } from './models/welcome_page';
|
||||||
|
|
||||||
|
|
||||||
|
test('Gradido login test (happy path)', async ({ page }) => {
|
||||||
|
const { EMAIL, PASSWORD } = process.env;
|
||||||
|
const loginPage = new LoginPage(page);
|
||||||
|
await loginPage.goto();
|
||||||
|
await loginPage.enterEmail(EMAIL);
|
||||||
|
await loginPage.enterPassword(PASSWORD);
|
||||||
|
await loginPage.submitLogin();
|
||||||
|
// assertions
|
||||||
|
await expect(page).toHaveURL('./overview');
|
||||||
|
});
|
||||||
33
e2e-tests/playwright/tests/models/login_page.ts
Normal file
33
e2e-tests/playwright/tests/models/login_page.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { expect, test, Locator, Page } from '@playwright/test';
|
||||||
|
|
||||||
|
export class LoginPage {
|
||||||
|
readonly page: Page;
|
||||||
|
readonly url: string;
|
||||||
|
readonly emailInput: Locator;
|
||||||
|
readonly passwordInput: Locator;
|
||||||
|
readonly submitBtn: Locator;
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.page = page;
|
||||||
|
this.url = './login';
|
||||||
|
this.emailInput = page.locator('id=Email-input-field');
|
||||||
|
this.passwordInput = page.locator('id=Password-input-field');
|
||||||
|
this.submitBtn = page.locator('text=Login');
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto() {
|
||||||
|
await this.page.goto(this.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async enterEmail(email: string) {
|
||||||
|
await this.emailInput.fill(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
async enterPassword(password: string) {
|
||||||
|
await this.passwordInput.fill(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitLogin() {
|
||||||
|
await this.submitBtn.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
13
e2e-tests/playwright/tests/models/welcome_page.ts
Normal file
13
e2e-tests/playwright/tests/models/welcome_page.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { expect, Locator, Page } from '@playwright/test';
|
||||||
|
|
||||||
|
export class WelcomePage {
|
||||||
|
readonly page: Page;
|
||||||
|
readonly url: string;
|
||||||
|
readonly profileLink: Locator;
|
||||||
|
|
||||||
|
constructor(page: Page){
|
||||||
|
this.page = page;
|
||||||
|
this.url = './overview';
|
||||||
|
this.profileLink = page.locator('href=/profile');
|
||||||
|
}
|
||||||
|
}
|
||||||
21
e2e-tests/playwright/tests/playwright.config.ts
Normal file
21
e2e-tests/playwright/tests/playwright.config.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
const config: PlaywrightTestConfig = {
|
||||||
|
globalSetup: require.resolve('./global-setup'),
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
locale: 'de-DE',
|
||||||
|
reporter: process.env.CI ? 'github' : 'list',
|
||||||
|
retries: 1,
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
testDir: '.',
|
||||||
|
timeout: 30000,
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
video: 'never',
|
||||||
|
viewport: { width: 1280, height: 720 },
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.URL || 'http://localhost:3000',
|
||||||
|
browserName: 'webkit',
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
@ -12,6 +12,7 @@
|
|||||||
atLeastOneSpecialCharater: true,
|
atLeastOneSpecialCharater: true,
|
||||||
noWhitespaceCharacters: true,
|
noWhitespaceCharacters: true,
|
||||||
}"
|
}"
|
||||||
|
id="new-password-input-field"
|
||||||
:label="register ? $t('form.password') : $t('form.password_new')"
|
:label="register ? $t('form.password') : $t('form.password_new')"
|
||||||
:showAllErrors="true"
|
:showAllErrors="true"
|
||||||
:immediate="true"
|
:immediate="true"
|
||||||
|
|||||||
@ -14,7 +14,12 @@
|
|||||||
<b-icon v-if="pending" icon="three-dots" animation="cylon"></b-icon>
|
<b-icon v-if="pending" icon="three-dots" animation="cylon"></b-icon>
|
||||||
<div v-else>{{ pending ? $t('em-dash') : balance | amount }} {{ $t('GDD') }}</div>
|
<div v-else>{{ pending ? $t('em-dash') : balance | amount }} {{ $t('GDD') }}</div>
|
||||||
</b-nav-item>
|
</b-nav-item>
|
||||||
<b-nav-item to="/profile" right class="d-none d-sm-none d-md-none d-lg-flex shadow-lg">
|
<b-nav-item
|
||||||
|
to="/profile"
|
||||||
|
right
|
||||||
|
class="d-none d-sm-none d-md-none d-lg-flex shadow-lg"
|
||||||
|
data-test="navbar-item-username"
|
||||||
|
>
|
||||||
<small>
|
<small>
|
||||||
{{ $store.state.firstName }} {{ $store.state.lastName }}
|
{{ $store.state.firstName }} {{ $store.state.lastName }}
|
||||||
<b>{{ $store.state.email }}</b>
|
<b>{{ $store.state.email }}</b>
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
<b-icon icon="people" aria-hidden="true"></b-icon>
|
<b-icon icon="people" aria-hidden="true"></b-icon>
|
||||||
{{ $t('navigation.community') }}
|
{{ $t('navigation.community') }}
|
||||||
</b-nav-item>
|
</b-nav-item>
|
||||||
<b-nav-item to="/profile" class="mb-3">
|
<b-nav-item to="/profile" class="mb-3" data-test="profile-menu">
|
||||||
<b-icon icon="gear" aria-hidden="true"></b-icon>
|
<b-icon icon="gear" aria-hidden="true"></b-icon>
|
||||||
{{ $t('navigation.profile') }}
|
{{ $t('navigation.profile') }}
|
||||||
</b-nav-item>
|
</b-nav-item>
|
||||||
@ -48,7 +48,7 @@
|
|||||||
<b-icon icon="shield-check" aria-hidden="true"></b-icon>
|
<b-icon icon="shield-check" aria-hidden="true"></b-icon>
|
||||||
{{ $t('navigation.admin_area') }}
|
{{ $t('navigation.admin_area') }}
|
||||||
</b-nav-item>
|
</b-nav-item>
|
||||||
<b-nav-item class="mb-3" @click="$emit('logout')">
|
<b-nav-item class="mb-3" @click="$emit('logout')" data-test="logout-menu">
|
||||||
<b-icon icon="power" aria-hidden="true"></b-icon>
|
<b-icon icon="power" aria-hidden="true"></b-icon>
|
||||||
{{ $t('navigation.logout') }}
|
{{ $t('navigation.logout') }}
|
||||||
</b-nav-item>
|
</b-nav-item>
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
<a
|
<a
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
@click="showPassword ? (showPassword = !showPassword) : cancelEdit()"
|
@click="showPassword ? (showPassword = !showPassword) : cancelEdit()"
|
||||||
|
data-test="open-password-change-form"
|
||||||
>
|
>
|
||||||
<span class="pointer mr-3">{{ $t('settings.password.change-password') }}</span>
|
<span class="pointer mr-3">{{ $t('settings.password.change-password') }}</span>
|
||||||
<b-icon v-if="showPassword" class="pointer ml-3" icon="pencil"></b-icon>
|
<b-icon v-if="showPassword" class="pointer ml-3" icon="pencil"></b-icon>
|
||||||
@ -36,6 +37,7 @@
|
|||||||
:variant="disabled ? 'light' : 'success'"
|
:variant="disabled ? 'light' : 'success'"
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
|
data-test="submit-new-password-btn"
|
||||||
>
|
>
|
||||||
{{ $t('form.save') }}
|
{{ $t('form.save') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ const authLink = new ApolloLink((operation, forward) => {
|
|||||||
operation.setContext({
|
operation.setContext({
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
|
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
|
||||||
|
clientRequestTime: new Date().toString(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return forward(operation).map((response) => {
|
return forward(operation).map((response) => {
|
||||||
|
|||||||
@ -98,6 +98,7 @@ describe('apolloProvider', () => {
|
|||||||
expect(setContextMock).toBeCalledWith({
|
expect(setContextMock).toBeCalledWith({
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: 'Bearer some-token',
|
Authorization: 'Bearer some-token',
|
||||||
|
clientRequestTime: expect.any(String),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -113,6 +114,7 @@ describe('apolloProvider', () => {
|
|||||||
expect(setContextMock).toBeCalledWith({
|
expect(setContextMock).toBeCalledWith({
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: '',
|
Authorization: '',
|
||||||
|
clientRequestTime: expect.any(String),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
"release": "scripts/release.sh"
|
"release": "scripts/release.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"auto-changelog": "^2.4.0"
|
"auto-changelog": "^2.4.0",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,6 +81,11 @@ uglify-js@^3.1.4:
|
|||||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.1.tgz#2749d4b8b5b7d67460b4a418023ff73c3fefa60a"
|
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.1.tgz#2749d4b8b5b7d67460b4a418023ff73c3fefa60a"
|
||||||
integrity sha512-EWhx3fHy3M9JbaeTnO+rEqzCe1wtyQClv6q3YWq0voOj4E+bMZBErVS1GAHPDiRGONYq34M1/d8KuQMgvi6Gjw==
|
integrity sha512-EWhx3fHy3M9JbaeTnO+rEqzCe1wtyQClv6q3YWq0voOj4E+bMZBErVS1GAHPDiRGONYq34M1/d8KuQMgvi6Gjw==
|
||||||
|
|
||||||
|
uuid@^8.3.2:
|
||||||
|
version "8.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
|
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||||
|
|
||||||
webidl-conversions@^3.0.0:
|
webidl-conversions@^3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user