mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 2179-bug-wrong-month-for-contribution-near-turn-of-month
This commit is contained in:
commit
901e85e90b
@ -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.')
|
||||||
|
|||||||
@ -8,10 +8,7 @@ import { Context, getUser } from '@/server/context'
|
|||||||
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
||||||
import { getCustomRepository, getConnection } from '@dbTools/typeorm'
|
import { getCustomRepository, getConnection } from '@dbTools/typeorm'
|
||||||
|
|
||||||
import {
|
import { sendTransactionReceivedEmail } from '@/mailer/sendTransactionReceivedEmail'
|
||||||
sendTransactionLinkRedeemedEmail,
|
|
||||||
sendTransactionReceivedEmail,
|
|
||||||
} from '@/mailer/sendTransactionReceivedEmail'
|
|
||||||
|
|
||||||
import { Transaction } from '@model/Transaction'
|
import { Transaction } from '@model/Transaction'
|
||||||
import { TransactionList } from '@model/TransactionList'
|
import { TransactionList } from '@model/TransactionList'
|
||||||
@ -38,6 +35,8 @@ 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'
|
||||||
|
|
||||||
export const executeTransaction = async (
|
export const executeTransaction = async (
|
||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
@ -81,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
|
||||||
@ -151,10 +150,9 @@ 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,
|
||||||
memo,
|
|
||||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||||
})
|
})
|
||||||
if (transactionLink) {
|
if (transactionLink) {
|
||||||
@ -163,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,
|
||||||
@ -187,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(
|
||||||
@ -309,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,13 +1,19 @@
|
|||||||
/* 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 { createUser, setPassword, forgotPassword, updateUserInfos } from '@/seeds/graphql/mutations'
|
import {
|
||||||
|
createUser,
|
||||||
|
setPassword,
|
||||||
|
forgotPassword,
|
||||||
|
updateUserInfos,
|
||||||
|
createContribution,
|
||||||
|
confirmContribution,
|
||||||
|
} 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'
|
||||||
@ -15,15 +21,19 @@ import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegi
|
|||||||
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
||||||
import { printTimeDuration, activationLink } from './UserResolver'
|
import { printTimeDuration, activationLink } from './UserResolver'
|
||||||
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
||||||
// import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
|
import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
|
||||||
import { ContributionLink } from '@model/ContributionLink'
|
import { ContributionLink } from '@model/ContributionLink'
|
||||||
// import { TransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink } from '@entity/TransactionLink'
|
||||||
|
|
||||||
import { EventProtocolType } from '@/event/EventProtocolType'
|
import { EventProtocolType } from '@/event/EventProtocolType'
|
||||||
import { EventProtocol } from '@entity/EventProtocol'
|
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 { klicktippSignIn } from '@/apis/KlicktippController'
|
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||||
|
|
||||||
@ -84,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 () => {
|
||||||
@ -103,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', () => {
|
||||||
@ -117,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,
|
||||||
@ -141,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,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -161,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,
|
||||||
@ -219,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' }),
|
||||||
]),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -236,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,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
@ -248,13 +264,15 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('redeem codes', () => {
|
describe('redeem codes', () => {
|
||||||
|
let result: any
|
||||||
|
let link: ContributionLink
|
||||||
|
|
||||||
describe('contribution link', () => {
|
describe('contribution link', () => {
|
||||||
let link: ContributionLink
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// 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
|
||||||
@ -277,16 +295,24 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
resetToken()
|
resetToken()
|
||||||
await mutate({
|
result = await mutate({
|
||||||
mutation: createUser,
|
mutation: createUser,
|
||||||
variables: { ...variables, email: 'ein@besucher.de', redeemCode: 'CL-' + link.code },
|
variables: { ...variables, email: 'ein@besucher.de', redeemCode: 'CL-' + link.code },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
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,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -299,6 +325,99 @@ describe('UserResolver', () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('stores the redeem register event in the database', () => {
|
||||||
|
expect(EventProtocol.find()).resolves.toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: EventProtocolType.REDEEM_REGISTER,
|
||||||
|
userId: result.data.createUser.id,
|
||||||
|
contributionId: link.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('transaction link', () => {
|
||||||
|
let contribution: any
|
||||||
|
let bob: any
|
||||||
|
let transactionLink: TransactionLink
|
||||||
|
let newUser: any
|
||||||
|
|
||||||
|
const bobData = {
|
||||||
|
email: 'bob@baumeister.de',
|
||||||
|
password: 'Aa12345_',
|
||||||
|
publisherId: 1234,
|
||||||
|
}
|
||||||
|
|
||||||
|
const peterData = {
|
||||||
|
email: 'peter@lustig.de',
|
||||||
|
password: 'Aa12345_',
|
||||||
|
publisherId: 1234,
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await userFactory(testEnv, peterLustig)
|
||||||
|
await userFactory(testEnv, bobBaumeister)
|
||||||
|
await query({ query: login, variables: bobData })
|
||||||
|
|
||||||
|
// create contribution as user bob
|
||||||
|
contribution = await mutate({
|
||||||
|
mutation: createContribution,
|
||||||
|
variables: { amount: 1000, memo: 'testing', creationDate: new Date().toISOString() },
|
||||||
|
})
|
||||||
|
|
||||||
|
// login as admin
|
||||||
|
await query({ query: login, variables: peterData })
|
||||||
|
|
||||||
|
// confirm the contribution
|
||||||
|
contribution = await mutate({
|
||||||
|
mutation: confirmContribution,
|
||||||
|
variables: { id: contribution.data.createContribution.id },
|
||||||
|
})
|
||||||
|
|
||||||
|
// login as user bob
|
||||||
|
bob = await query({ query: login, variables: bobData })
|
||||||
|
|
||||||
|
// create transaction link
|
||||||
|
await transactionLinkFactory(testEnv, {
|
||||||
|
email: 'bob@baumeister.de',
|
||||||
|
amount: 19.99,
|
||||||
|
memo: `testing transaction link`,
|
||||||
|
})
|
||||||
|
|
||||||
|
transactionLink = await TransactionLink.findOneOrFail()
|
||||||
|
|
||||||
|
resetToken()
|
||||||
|
|
||||||
|
// create new user using transaction link of bob
|
||||||
|
newUser = await mutate({
|
||||||
|
mutation: createUser,
|
||||||
|
variables: {
|
||||||
|
...variables,
|
||||||
|
email: 'which@ever.de',
|
||||||
|
redeemCode: transactionLink.code,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets the referrer id to bob baumeister id', async () => {
|
||||||
|
await expect(
|
||||||
|
UserContact.findOne({ email: 'which@ever.de' }, { relations: ['user'] }),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
user: expect.objectContaining({ referrerId: bob.data.login.id }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('stores the redeem register event in the database', async () => {
|
||||||
|
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: EventProtocolType.REDEEM_REGISTER,
|
||||||
|
userId: newUser.data.createUser.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
/* A transaction link requires GDD on account
|
/* A transaction link requires GDD on account
|
||||||
@ -310,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()
|
||||||
@ -319,14 +438,14 @@ bei Gradidio sei dabei!`,
|
|||||||
variables: { ...variables, email: 'neuer@user.de', redeemCode: transactionLink.code },
|
variables: { ...variables, email: 'neuer@user.de', redeemCode: transactionLink.code },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets the referrer id to Peter Lustigs id', async () => {
|
it('sets the referrer id to Peter Lustigs id', async () => {
|
||||||
await expect(User.findOne({ email: 'neuer@user.de' })).resolves.toEqual(expect.objectContaining({
|
await expect(User.findOne({ email: 'neuer@user.de' })).resolves.toEqual(expect.objectContaining({
|
||||||
referrerId: user[0].id,
|
referrerId: user[0].id,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
*/
|
*/
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -341,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 () => {
|
||||||
@ -362,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')
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -388,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' },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -459,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 })
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -471,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',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -656,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',
|
||||||
@ -704,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')],
|
||||||
@ -720,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 () => {
|
||||||
@ -739,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"
|
||||||
}`),
|
}`),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@ -753,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}`)
|
||||||
}
|
}
|
||||||
@ -494,10 +559,10 @@ export class UserResolver {
|
|||||||
|
|
||||||
if (redeemCode) {
|
if (redeemCode) {
|
||||||
eventRedeemRegister.userId = dbUser.id
|
eventRedeemRegister.userId = dbUser.id
|
||||||
eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
|
await eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
|
||||||
} else {
|
} else {
|
||||||
eventRegister.userId = dbUser.id
|
eventRegister.userId = dbUser.id
|
||||||
eventProtocol.writeEvent(event.setEventRegister(eventRegister))
|
await eventProtocol.writeEvent(event.setEventRegister(eventRegister))
|
||||||
}
|
}
|
||||||
|
|
||||||
return new User(dbUser)
|
return new User(dbUser)
|
||||||
@ -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),
|
||||||
|
|||||||
44
backend/src/mailer/sendTransactionLinkRedeemed.test.ts
Normal file
44
backend/src/mailer/sendTransactionLinkRedeemed.test.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { sendEMail } from './sendEMail'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { sendTransactionLinkRedeemedEmail } from './sendTransactionLinkRedeemed'
|
||||||
|
|
||||||
|
jest.mock('./sendEMail', () => {
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
sendEMail: jest.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('sendTransactionLinkRedeemedEmail', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await sendTransactionLinkRedeemedEmail({
|
||||||
|
email: 'bibi@bloxberg.de',
|
||||||
|
senderFirstName: 'Peter',
|
||||||
|
senderLastName: 'Lustig',
|
||||||
|
recipientFirstName: 'Bibi',
|
||||||
|
recipientLastName: 'Bloxberg',
|
||||||
|
senderEmail: 'peter@lustig.de',
|
||||||
|
amount: new Decimal(42.0),
|
||||||
|
memo: 'Vielen Dank dass Du dabei bist',
|
||||||
|
overviewURL: 'http://localhost/overview',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls sendEMail', () => {
|
||||||
|
expect(sendEMail).toBeCalledWith({
|
||||||
|
to: `Bibi Bloxberg <bibi@bloxberg.de>`,
|
||||||
|
subject: 'Gradido-Link wurde eingelöst',
|
||||||
|
text:
|
||||||
|
expect.stringContaining('Hallo Bibi Bloxberg') &&
|
||||||
|
expect.stringContaining(
|
||||||
|
'Peter Lustig (peter@lustig.de) hat soeben deinen Link eingelöst.',
|
||||||
|
) &&
|
||||||
|
expect.stringContaining('Betrag: 42,00 GDD,') &&
|
||||||
|
expect.stringContaining('Memo: Vielen Dank dass Du dabei bist') &&
|
||||||
|
expect.stringContaining(
|
||||||
|
'Details zur Transaktion findest du in deinem Gradido-Konto: http://localhost/overview',
|
||||||
|
) &&
|
||||||
|
expect.stringContaining('Bitte antworte nicht auf diese E-Mail!'),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
28
backend/src/mailer/sendTransactionLinkRedeemed.ts
Normal file
28
backend/src/mailer/sendTransactionLinkRedeemed.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { sendEMail } from './sendEMail'
|
||||||
|
import { transactionLinkRedeemed } from './text/transactionLinkRedeemed'
|
||||||
|
|
||||||
|
export const sendTransactionLinkRedeemedEmail = (data: {
|
||||||
|
email: string
|
||||||
|
senderFirstName: string
|
||||||
|
senderLastName: string
|
||||||
|
recipientFirstName: string
|
||||||
|
recipientLastName: string
|
||||||
|
senderEmail: string
|
||||||
|
amount: Decimal
|
||||||
|
memo: string
|
||||||
|
overviewURL: string
|
||||||
|
}): Promise<boolean> => {
|
||||||
|
logger.info(
|
||||||
|
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName},
|
||||||
|
<${data.email}>,
|
||||||
|
subject=${transactionLinkRedeemed.de.subject},
|
||||||
|
text=${transactionLinkRedeemed.de.text(data)}`,
|
||||||
|
)
|
||||||
|
return sendEMail({
|
||||||
|
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`,
|
||||||
|
subject: transactionLinkRedeemed.de.subject,
|
||||||
|
text: transactionLinkRedeemed.de.text(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -19,7 +19,6 @@ describe('sendTransactionReceivedEmail', () => {
|
|||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
senderEmail: 'bibi@bloxberg.de',
|
senderEmail: 'bibi@bloxberg.de',
|
||||||
amount: new Decimal(42.0),
|
amount: new Decimal(42.0),
|
||||||
memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
|
||||||
overviewURL: 'http://localhost/overview',
|
overviewURL: 'http://localhost/overview',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -33,7 +32,6 @@ describe('sendTransactionReceivedEmail', () => {
|
|||||||
expect.stringContaining('42,00 GDD') &&
|
expect.stringContaining('42,00 GDD') &&
|
||||||
expect.stringContaining('Bibi Bloxberg') &&
|
expect.stringContaining('Bibi Bloxberg') &&
|
||||||
expect.stringContaining('(bibi@bloxberg.de)') &&
|
expect.stringContaining('(bibi@bloxberg.de)') &&
|
||||||
expect.stringContaining('Vielen herzlichen Dank für den neuen Hexenbesen!') &&
|
|
||||||
expect.stringContaining('http://localhost/overview'),
|
expect.stringContaining('http://localhost/overview'),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { backendLogger as logger } from '@/server/logger'
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { sendEMail } from './sendEMail'
|
import { sendEMail } from './sendEMail'
|
||||||
import { transactionLinkRedeemed, transactionReceived } from './text/transactionReceived'
|
import { transactionReceived } from './text/transactionReceived'
|
||||||
|
|
||||||
export const sendTransactionReceivedEmail = (data: {
|
export const sendTransactionReceivedEmail = (data: {
|
||||||
senderFirstName: string
|
senderFirstName: string
|
||||||
@ -11,7 +11,6 @@ export const sendTransactionReceivedEmail = (data: {
|
|||||||
email: string
|
email: string
|
||||||
senderEmail: string
|
senderEmail: string
|
||||||
amount: Decimal
|
amount: Decimal
|
||||||
memo: string
|
|
||||||
overviewURL: string
|
overviewURL: string
|
||||||
}): Promise<boolean> => {
|
}): Promise<boolean> => {
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -26,27 +25,3 @@ export const sendTransactionReceivedEmail = (data: {
|
|||||||
text: transactionReceived.de.text(data),
|
text: transactionReceived.de.text(data),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendTransactionLinkRedeemedEmail = (data: {
|
|
||||||
email: string
|
|
||||||
senderFirstName: string
|
|
||||||
senderLastName: string
|
|
||||||
recipientFirstName: string
|
|
||||||
recipientLastName: string
|
|
||||||
senderEmail: string
|
|
||||||
amount: Decimal
|
|
||||||
memo: string
|
|
||||||
overviewURL: string
|
|
||||||
}): Promise<boolean> => {
|
|
||||||
logger.info(
|
|
||||||
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName},
|
|
||||||
<${data.email}>,
|
|
||||||
subject=${transactionLinkRedeemed.de.subject},
|
|
||||||
text=${transactionLinkRedeemed.de.text(data)}`,
|
|
||||||
)
|
|
||||||
return sendEMail({
|
|
||||||
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`,
|
|
||||||
subject: transactionLinkRedeemed.de.subject,
|
|
||||||
text: transactionLinkRedeemed.de.text(data),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@ -14,9 +14,10 @@ export const contributionConfirmed = {
|
|||||||
}): string =>
|
}): string =>
|
||||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
|
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
|
||||||
|
|
||||||
Dein Gradido Schöpfungsantrag "${data.contributionMemo}" wurde soeben von ${data.senderFirstName} ${
|
Dein eingereichter Gemeinwohl-Beitrag "${data.contributionMemo}" wurde soeben von ${
|
||||||
data.senderLastName
|
data.senderFirstName
|
||||||
} bestätigt.
|
} ${data.senderLastName} bestätigt.
|
||||||
|
|
||||||
Betrag: ${data.contributionAmount.toFixed(2).replace('.', ',')} GDD
|
Betrag: ${data.contributionAmount.toFixed(2).replace('.', ',')} GDD
|
||||||
|
|
||||||
Bitte antworte nicht auf diese E-Mail!
|
Bitte antworte nicht auf diese E-Mail!
|
||||||
|
|||||||
@ -14,17 +14,15 @@ export const contributionMessageReceived = {
|
|||||||
}): string =>
|
}): string =>
|
||||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
|
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
|
||||||
|
|
||||||
Du hast soeben zu deinem eingereichten Gradido Schöpfungsantrag "${data.contributionMemo}" eine Rückfrage von ${data.senderFirstName} ${data.senderLastName} erhalten.
|
du hast soeben zu deinem eingereichten Gemeinwohl-Beitrag "${data.contributionMemo}" eine Rückfrage von ${data.senderFirstName} ${data.senderLastName} erhalten.
|
||||||
Die Rückfrage lautet:
|
|
||||||
|
|
||||||
${data.message}
|
Bitte beantworte die Rückfrage in deinem Gradido-Konto im Menü "Gemeinschaft" im Tab "Meine Beiträge zum Gemeinwohl"!
|
||||||
|
|
||||||
|
Link zu deinem Konto: ${data.overviewURL}
|
||||||
|
|
||||||
Bitte antworte nicht auf diese E-Mail!
|
Bitte antworte nicht auf diese E-Mail!
|
||||||
|
|
||||||
Mit freundlichen Grüßen,
|
Mit freundlichen Grüßen,
|
||||||
dein Gradido-Team
|
dein Gradido-Team`,
|
||||||
|
|
||||||
|
|
||||||
Link zu deinem Konto: ${data.overviewURL}`,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
33
backend/src/mailer/text/transactionLinkRedeemed.ts
Normal file
33
backend/src/mailer/text/transactionLinkRedeemed.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
|
export const transactionLinkRedeemed = {
|
||||||
|
de: {
|
||||||
|
subject: 'Gradido-Link wurde eingelöst',
|
||||||
|
text: (data: {
|
||||||
|
email: string
|
||||||
|
senderFirstName: string
|
||||||
|
senderLastName: string
|
||||||
|
recipientFirstName: string
|
||||||
|
recipientLastName: string
|
||||||
|
senderEmail: string
|
||||||
|
amount: Decimal
|
||||||
|
memo: string
|
||||||
|
overviewURL: string
|
||||||
|
}): string =>
|
||||||
|
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
||||||
|
|
||||||
|
${data.senderFirstName} ${data.senderLastName} (${
|
||||||
|
data.senderEmail
|
||||||
|
}) hat soeben deinen Link eingelöst.
|
||||||
|
|
||||||
|
Betrag: ${data.amount.toFixed(2).replace('.', ',')} GDD,
|
||||||
|
Memo: ${data.memo}
|
||||||
|
|
||||||
|
Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL}
|
||||||
|
|
||||||
|
Bitte antworte nicht auf diese E-Mail!
|
||||||
|
|
||||||
|
Mit freundlichen Grüßen,
|
||||||
|
dein Gradido-Team`,
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -11,7 +11,6 @@ export const transactionReceived = {
|
|||||||
email: string
|
email: string
|
||||||
senderEmail: string
|
senderEmail: string
|
||||||
amount: Decimal
|
amount: Decimal
|
||||||
memo: string
|
|
||||||
overviewURL: string
|
overviewURL: string
|
||||||
}): string =>
|
}): string =>
|
||||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
||||||
@ -19,47 +18,12 @@ export const transactionReceived = {
|
|||||||
Du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${
|
Du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${
|
||||||
data.senderLastName
|
data.senderLastName
|
||||||
} (${data.senderEmail}) erhalten.
|
} (${data.senderEmail}) erhalten.
|
||||||
${data.senderFirstName} ${data.senderLastName} schreibt:
|
|
||||||
|
|
||||||
${data.memo}
|
Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL}
|
||||||
|
|
||||||
Bitte antworte nicht auf diese E-Mail!
|
Bitte antworte nicht auf diese E-Mail!
|
||||||
|
|
||||||
Mit freundlichen Grüßen,
|
Mit freundlichen Grüßen,
|
||||||
dein Gradido-Team
|
dein Gradido-Team`,
|
||||||
|
|
||||||
|
|
||||||
Link zu deinem Konto: ${data.overviewURL}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const transactionLinkRedeemed = {
|
|
||||||
de: {
|
|
||||||
subject: 'Gradido link eingelösst',
|
|
||||||
text: (data: {
|
|
||||||
email: string
|
|
||||||
senderFirstName: string
|
|
||||||
senderLastName: string
|
|
||||||
recipientFirstName: string
|
|
||||||
recipientLastName: string
|
|
||||||
senderEmail: string
|
|
||||||
amount: Decimal
|
|
||||||
memo: string
|
|
||||||
overviewURL: string
|
|
||||||
}): string =>
|
|
||||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
|
||||||
|
|
||||||
${data.senderFirstName} ${data.senderLastName} (${
|
|
||||||
data.senderEmail
|
|
||||||
}) hat soeben deinen Link eingelösst.
|
|
||||||
Betrag: ${data.amount.toFixed(2).replace('.', ',')} GDD,
|
|
||||||
Memo: ${data.memo}
|
|
||||||
|
|
||||||
Bitte antworte nicht auf diese E-Mail!
|
|
||||||
|
|
||||||
Mit freundlichen Grüßen,
|
|
||||||
dein Gradido-Team
|
|
||||||
|
|
||||||
Link zu deinem Konto: ${data.overviewURL}`,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -1,122 +1,122 @@
|
|||||||
version: "3.4"
|
version: "3.4"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
########################################################
|
########################################################
|
||||||
# FRONTEND #############################################
|
# FRONTEND #############################################
|
||||||
########################################################
|
########################################################
|
||||||
frontend:
|
frontend:
|
||||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||||
image: gradido/frontend:local-development
|
image: gradido/frontend:local-development
|
||||||
build:
|
build:
|
||||||
target: development
|
target: development
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV="development"
|
- NODE_ENV="development"
|
||||||
# - DEBUG=true
|
# - DEBUG=true
|
||||||
volumes:
|
volumes:
|
||||||
# This makes sure the docker container has its own node modules.
|
# This makes sure the docker container has its own node modules.
|
||||||
# Therefore it is possible to have a different node version on the host machine
|
# Therefore it is possible to have a different node version on the host machine
|
||||||
- frontend_node_modules:/app/node_modules
|
- frontend_node_modules:/app/node_modules
|
||||||
# bind the local folder to the docker to allow live reload
|
# bind the local folder to the docker to allow live reload
|
||||||
- ./frontend:/app
|
- ./frontend:/app
|
||||||
|
|
||||||
########################################################
|
########################################################
|
||||||
# ADMIN INTERFACE ######################################
|
# ADMIN INTERFACE ######################################
|
||||||
########################################################
|
########################################################
|
||||||
admin:
|
admin:
|
||||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||||
image: gradido/admin:local-development
|
image: gradido/admin:local-development
|
||||||
build:
|
build:
|
||||||
target: development
|
target: development
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV="development"
|
- NODE_ENV="development"
|
||||||
# - DEBUG=true
|
# - DEBUG=true
|
||||||
volumes:
|
volumes:
|
||||||
# This makes sure the docker container has its own node modules.
|
# This makes sure the docker container has its own node modules.
|
||||||
# Therefore it is possible to have a different node version on the host machine
|
# Therefore it is possible to have a different node version on the host machine
|
||||||
- admin_node_modules:/app/node_modules
|
- admin_node_modules:/app/node_modules
|
||||||
# bind the local folder to the docker to allow live reload
|
# bind the local folder to the docker to allow live reload
|
||||||
- ./admin:/app
|
- ./admin:/app
|
||||||
|
|
||||||
########################################################
|
########################################################
|
||||||
# BACKEND ##############################################
|
# BACKEND ##############################################
|
||||||
########################################################
|
########################################################
|
||||||
backend:
|
backend:
|
||||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||||
image: gradido/backend:local-development
|
image: gradido/backend:local-development
|
||||||
build:
|
build:
|
||||||
target: development
|
target: development
|
||||||
networks:
|
networks:
|
||||||
- external-net
|
- external-net
|
||||||
- internal-net
|
- internal-net
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV="development"
|
- NODE_ENV="development"
|
||||||
volumes:
|
volumes:
|
||||||
# This makes sure the docker container has its own node modules.
|
# This makes sure the docker container has its own node modules.
|
||||||
# Therefore it is possible to have a different node version on the host machine
|
# Therefore it is possible to have a different node version on the host machine
|
||||||
- backend_node_modules:/app/node_modules
|
- backend_node_modules:/app/node_modules
|
||||||
- backend_database_node_modules:/database/node_modules
|
- backend_database_node_modules:/database/node_modules
|
||||||
- backend_database_build:/database/build
|
- backend_database_build:/database/build
|
||||||
# bind the local folder to the docker to allow live reload
|
# bind the local folder to the docker to allow live reload
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
- ./database:/database
|
- ./database:/database
|
||||||
|
|
||||||
########################################################
|
########################################################
|
||||||
# DATABASE ##############################################
|
# DATABASE ##############################################
|
||||||
########################################################
|
########################################################
|
||||||
database:
|
database:
|
||||||
# we always run on production here since else the service lingers
|
# we always run on production here since else the service lingers
|
||||||
# feel free to change this behaviour if it seems useful
|
# feel free to change this behaviour if it seems useful
|
||||||
# Due to problems with the volume caching the built files
|
# Due to problems with the volume caching the built files
|
||||||
# we changed this to test build. This keeps the service running.
|
# we changed this to test build. This keeps the service running.
|
||||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||||
image: gradido/database:local-test_up
|
image: gradido/database:local-test_up
|
||||||
build:
|
build:
|
||||||
target: test_up
|
target: test_up
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV="development"
|
- NODE_ENV="development"
|
||||||
volumes:
|
volumes:
|
||||||
# This makes sure the docker container has its own node modules.
|
# This makes sure the docker container has its own node modules.
|
||||||
# Therefore it is possible to have a different node version on the host machine
|
# Therefore it is possible to have a different node version on the host machine
|
||||||
- database_node_modules:/app/node_modules
|
- database_node_modules:/app/node_modules
|
||||||
- database_build:/app/build
|
- database_build:/app/build
|
||||||
# bind the local folder to the docker to allow live reload
|
# bind the local folder to the docker to allow live reload
|
||||||
- ./database:/app
|
- ./database:/app
|
||||||
|
|
||||||
#########################################################
|
#########################################################
|
||||||
## MARIADB ##############################################
|
## MARIADB ##############################################
|
||||||
#########################################################
|
#########################################################
|
||||||
mariadb:
|
mariadb:
|
||||||
networks:
|
networks:
|
||||||
- internal-net
|
- internal-net
|
||||||
- external-net
|
- external-net
|
||||||
|
|
||||||
#########################################################
|
#########################################################
|
||||||
## NGINX ################################################
|
## NGINX ################################################
|
||||||
#########################################################
|
#########################################################
|
||||||
# nginx:
|
# nginx:
|
||||||
|
|
||||||
#########################################################
|
#########################################################
|
||||||
## PHPMYADMIN ###########################################
|
## PHPMYADMIN ###########################################
|
||||||
#########################################################
|
#########################################################
|
||||||
phpmyadmin:
|
phpmyadmin:
|
||||||
image: phpmyadmin
|
image: phpmyadmin
|
||||||
environment:
|
environment:
|
||||||
- PMA_ARBITRARY=1
|
- PMA_ARBITRARY=1
|
||||||
#restart: always
|
#restart: always
|
||||||
ports:
|
ports:
|
||||||
- 8074:80
|
- 8074:80
|
||||||
networks:
|
networks:
|
||||||
- internal-net
|
- internal-net
|
||||||
- external-net
|
- external-net
|
||||||
volumes:
|
volumes:
|
||||||
- /sessions
|
- /sessions
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
frontend_node_modules:
|
frontend_node_modules:
|
||||||
admin_node_modules:
|
admin_node_modules:
|
||||||
backend_node_modules:
|
backend_node_modules:
|
||||||
backend_database_node_modules:
|
backend_database_node_modules:
|
||||||
backend_database_build:
|
backend_database_build:
|
||||||
database_node_modules:
|
database_node_modules:
|
||||||
database_build:
|
database_build:
|
||||||
|
|||||||
@ -1,61 +1,62 @@
|
|||||||
version: "3.4"
|
version: "3.4"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
########################################################
|
########################################################
|
||||||
# BACKEND ##############################################
|
# BACKEND ##############################################
|
||||||
########################################################
|
########################################################
|
||||||
backend:
|
backend:
|
||||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||||
image: gradido/backend:test
|
image: gradido/backend:test
|
||||||
build:
|
build:
|
||||||
target: test
|
target: test
|
||||||
networks:
|
networks:
|
||||||
- external-net
|
- external-net
|
||||||
- internal-net
|
- internal-net
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV="test"
|
- NODE_ENV="test"
|
||||||
- DB_HOST=mariadb
|
- DB_HOST=mariadb
|
||||||
|
|
||||||
########################################################
|
########################################################
|
||||||
# DATABASE #############################################
|
# DATABASE #############################################
|
||||||
########################################################
|
########################################################
|
||||||
database:
|
database:
|
||||||
build:
|
build:
|
||||||
context: ./database
|
context: ./database
|
||||||
target: test_up
|
target: test_up
|
||||||
# restart: always # this is very dangerous, but worth a test for the delayed mariadb startup at first run
|
# restart: always # this is very dangerous, but worth a test for the delayed mariadb startup at first run
|
||||||
|
|
||||||
#########################################################
|
#########################################################
|
||||||
## MARIADB ##############################################
|
## MARIADB ##############################################
|
||||||
#########################################################
|
#########################################################
|
||||||
mariadb:
|
mariadb:
|
||||||
networks:
|
networks:
|
||||||
- internal-net
|
- internal-net
|
||||||
- external-net
|
- external-net
|
||||||
volumes:
|
volumes:
|
||||||
- db_test_vol:/var/lib/mysql
|
- db_test_vol:/var/lib/mysql
|
||||||
|
|
||||||
#########################################################
|
#########################################################
|
||||||
## PHPMYADMIN ###########################################
|
## PHPMYADMIN ###########################################
|
||||||
#########################################################
|
#########################################################
|
||||||
phpmyadmin:
|
phpmyadmin:
|
||||||
image: phpmyadmin
|
image: phpmyadmin
|
||||||
environment:
|
environment:
|
||||||
- PMA_ARBITRARY=1
|
- PMA_ARBITRARY=1
|
||||||
#restart: always
|
#restart: always
|
||||||
ports:
|
ports:
|
||||||
- 8074:80
|
- 8074:80
|
||||||
networks:
|
networks:
|
||||||
- internal-net
|
- internal-net
|
||||||
- external-net
|
- external-net
|
||||||
volumes:
|
volumes:
|
||||||
- /sessions
|
- /sessions
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
external-net:
|
external-net:
|
||||||
internal-net:
|
internal-net:
|
||||||
internal: true
|
internal: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db_test_vol:
|
db_test_vol:
|
||||||
|
|
||||||
|
|||||||
@ -6,30 +6,41 @@ With the business event protocol the gradido application will capture and persis
|
|||||||
|
|
||||||
The different event types will be defined as Enum. The following list is a first draft and will grow with further event types in the future.
|
The different event types will be defined as Enum. The following list is a first draft and will grow with further event types in the future.
|
||||||
|
|
||||||
| EventType | Value | Description |
|
| EventType | Description |
|
||||||
| ----------------------------------- | ----- | ------------------------------------------------------------------------------------------------------ |
|
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| BasicEvent | 0 | the basic event is the root of all further extending event types |
|
| BasicEvent | the basic event is the root of all further extending event types |
|
||||||
| VisitGradidoEvent | 10 | if a user visits a gradido page without login or register |
|
| VisitGradidoEvent | if a user visits a gradido page without login or register; possible as soon as a request-response-loop for the first page will be invoked |
|
||||||
| RegisterEvent | 20 | the user presses the register button |
|
| RegisterEvent | the user presses the register button |
|
||||||
| RedeemRegisterEvent | 21 | the user presses the register button initiated by the redeem link |
|
| LoginEvent | the user presses the login button |
|
||||||
| InActiveAccountEvent | 22 | the systems create an inactive account during the register process |
|
| VerifyRedeemEvent | the user presses a redeem link independent from transaction or contribution redeem |
|
||||||
| SendConfirmEmailEvent | 23 | the system send a confirmation email to the user during the register process |
|
| RedeemRegisterEvent | the user presses the register-button initiated by the redeem link |
|
||||||
| ConfirmEmailEvent | 24 | the user confirms his email during the register process |
|
| RedeemLoginEvent | the user presses the login-button initiated by the redeem link |
|
||||||
| RegisterEmailKlickTippEvent | 25 | the system registers the confirmed email at klicktipp |
|
| ActivateAccountEvent | the system activates the users account after a successful confirmEmail-Event or during a reactivation of a deactivated account |
|
||||||
| LoginEvent | 30 | the user presses the login button |
|
| InActiveAccountEvent | the systems creates an inactive account during the register process or an active account will be reset to inactive |
|
||||||
| RedeemLoginEvent | 31 | the user presses the login button initiated by the redeem link |
|
| SetPasswordEvent | the system sets a new password after ConfirmEmailEvent or SendForgotPasswordEvent |
|
||||||
| ActivateAccountEvent | 32 | the system activates the users account during the first login process |
|
| RegisterEmailKlickTippEvent | the system registers the confirmed email at klicktipp |
|
||||||
| PasswordChangeEvent | 33 | the user changes his password |
|
| PasswordChangeEvent | the user changes his password in his Profile |
|
||||||
| TransactionSendEvent | 40 | the user creates a transaction and sends it online |
|
| TransactionSendEvent | the user creates a transaction and sends it online; paired with TransactionReceiveEvent |
|
||||||
| TransactionSendRedeemEvent | 41 | the user creates a transaction and sends it per redeem link |
|
| TransactionLinkCreateEvent | the user creates a transaction link |
|
||||||
| TransactionRepeateRedeemEvent | 42 | the user recreates a redeem link of a still open transaction |
|
| TransactionReceiveEvent | the user receives a transaction from an other user and posts the amount on his account; paired with TransactionSendEvent |
|
||||||
| TransactionCreationEvent | 50 | the user receives a creation transaction for his confirmed contribution |
|
| TransactionLinkRedeemEvent | the user activates the redeem link and receives the transaction and posts the amount on his account |
|
||||||
| TransactionReceiveEvent | 51 | the user receives a transaction from an other user and posts the amount on his account |
|
| ContributionCreateEvent | the user enters his contribution and asks for confirmation |
|
||||||
| TransactionReceiveRedeemEvent | 52 | the user activates the redeem link and receives the transaction and posts the amount on his account |
|
| ContributionConfirmEvent | the admin user confirms a contribution of an other user (for future multi confirmation from several users) |
|
||||||
| ContributionCreateEvent | 60 | the user enters his contribution and asks for confirmation |
|
| ContributionDenyEvent | the admin user denies a contribution of an other user |
|
||||||
| ContributionConfirmEvent | 61 | the user confirms a contribution of an other user (for future multi confirmation from several users) |
|
| ContributionLinkDefineEvent | the admin user defines a contributionLink, which could be send per Link/QR-Code on an other medium |
|
||||||
| ContributionLinkDefineEvent | 70 | the admin user defines a contributionLink, which could be send per Link/QR-Code on an other medium |
|
| ContributionLinkRedeemEvent | the user activates a received contributionLink to create a contribution entry for the contributionLink |
|
||||||
| ContributionLinkActivateRedeemEvent | 71 | the user activates a received contributionLink to create a contribution entry for the contributionLink |
|
| UserCreateContributionMessageEvent | the user captures a new message for a contribution |
|
||||||
|
| AdminCreateContributionMessageEvent | the admin user captures a new message for a contribution |
|
||||||
|
| LogoutEvent | the user invokes a logout |
|
||||||
|
| SendConfirmEmailEvent | the system sends a confirmation email to the user during the registration process |
|
||||||
|
| SendAccountMultiRegistrationEmailEvent | the system sends a info email to the user, that an other user tries to register with his existing email address |
|
||||||
|
| SendForgotPasswordEmailEvent | the system sends the forgot password email including a special link to start the forgot password process |
|
||||||
|
| SendTransactionSendEmailEvent | the system sends an email to inform the user about his transaction was sent to an other user |
|
||||||
|
| SendTransactionReceiveEmailEvent | the system sends an email to inform the user about a received transaction from an other user |
|
||||||
|
| SendAddedContributionEmailEvent | the system sends an email to inform the user about the creation of his captured contribution |
|
||||||
|
| SendContributionConfirmEmailEvent | the system sends an email to inform the user about the confirmation of his contribution |
|
||||||
|
| SendTransactionLinkRedeemEmailEvent | the system sends an email to the user, who created the transactionlink, that the link was redeemed |
|
||||||
|
| | |
|
||||||
|
|
||||||
## EventProtocol - Entity
|
## EventProtocol - Entity
|
||||||
|
|
||||||
@ -49,32 +60,44 @@ The business events will be stored in database in the new table `EventProtocol`.
|
|||||||
|
|
||||||
## Event Types
|
## Event Types
|
||||||
|
|
||||||
The following table lists for each event type the mandatory attributes, which have to be initialized at event occurence and to be written in the database event protocol table:
|
The following table lists for each event type the mapping between old and new key, the mandatory attributes, which have to be initialized at event occurence and to be written in the database event protocol table:
|
||||||
|
|
||||||
| EventType | id | type | createdAt | userID | XuserID | XCommunityID | transactionID | contribID | amount |
|
| EventType - old key | EventType - new key | id | type | createdAt | userID | XuserID | XCommunityID | transactionID | contribID | amount |
|
||||||
| :---------------------------------- | :-: | :--: | :-------: | :----: | :-----: | :----------: | :-----------: | :-------: | :----: |
|
| :-------------------------------- | :------------------------------------- | :-: | :--: | :-------: | :----: | :-----: | :----------: | :-----------: | :-------: | :----: |
|
||||||
| BasicEvent | x | x | x | | | | | | |
|
| BASIC | BasicEvent | x | x | x | | | | | | |
|
||||||
| VisitGradidoEvent | x | x | x | | | | | | |
|
| VISIT_GRADIDO | VisitGradidoEvent | x | x | x | | | | | | |
|
||||||
| RegisterEvent | x | x | x | x | | | | | |
|
| REGISTER | RegisterEvent | x | x | x | x | | | | | |
|
||||||
| RedeemRegisterEvent | x | x | x | x | | | (x) | (x) | |
|
| LOGIN | LoginEvent | x | x | x | x | | | | | |
|
||||||
| InActiveAccountEvent | x | x | x | x | | | | | |
|
| | VerifyRedeemEvent | | | | | | | | | |
|
||||||
| SendConfirmEmailEvent | x | x | x | x | | | | | |
|
| REDEEM_REGISTER | RedeemRegisterEvent | x | x | x | x | | | (x) | (x) | |
|
||||||
| ConfirmEmailEvent | x | x | x | x | | | | | |
|
| REDEEM_LOGIN | RedeemLoginEvent | x | x | x | x | | | (x) | (x) | |
|
||||||
| RegisterEmailKlickTippEvent | x | x | x | x | | | | | |
|
| ACTIVATE_ACCOUNT | ActivateAccountEvent | x | x | x | x | | | | | |
|
||||||
| LoginEvent | x | x | x | x | | | | | |
|
| INACTIVE_ACCOUNT | InActiveAccountEvent | x | x | x | x | | | | | |
|
||||||
| RedeemLoginEvent | x | x | x | x | | | (x) | (x) | |
|
| CONFIRM_EMAIL | SetPasswordEvent | x | x | x | x | | | | | |
|
||||||
| ActivateAccountEvent | x | x | x | x | | | | | |
|
| REGISTER_EMAIL_KLICKTIPP | RegisterEmailKlickTippEvent | x | x | x | x | | | | | |
|
||||||
| PasswordChangeEvent | x | x | x | x | | | | | |
|
| PASSWORD_CHANGE | PasswordChangeEvent | x | x | x | x | | | | | |
|
||||||
| TransactionSendEvent | x | x | x | x | x | x | x | | x |
|
| TRANSACTION_SEND | TransactionSendEvent | x | x | x | x | x | x | x | | x |
|
||||||
| TransactionSendRedeemEvent | x | x | x | x | x | x | x | | x |
|
| TRANSACTION_CREATION | TransactionLinkCreateEvent | x | x | x | x | | | x | | x |
|
||||||
| TransactionRepeateRedeemEvent | x | x | x | x | x | x | x | | x |
|
| TRANSACTION_RECEIVE | TransactionReceiveEvent | x | x | x | x | x | x | x | | x |
|
||||||
| TransactionCreationEvent | x | x | x | x | | | x | | x |
|
| TRANSACTION_SEND_REDEEM | TransactionLinkRedeemEvent | x | x | x | x | x | x | x | | x |
|
||||||
| TransactionReceiveEvent | x | x | x | x | x | x | x | | x |
|
| CONTRIBUTION_CREATE | ContributionCreateEvent | x | x | x | x | | | | x | x |
|
||||||
| TransactionReceiveRedeemEvent | x | x | x | x | x | x | x | | x |
|
| CONTRIBUTION_CONFIRM | ContributionConfirmEvent | x | x | x | x | x | x | | x | x |
|
||||||
| ContributionCreateEvent | x | x | x | x | | | | x | x |
|
| | ContributionDenyEvent | x | x | x | x | x | x | | x | x |
|
||||||
| ContributionConfirmEvent | x | x | x | x | x | x | | x | x |
|
| CONTRIBUTION_LINK_DEFINE | ContributionLinkDefineEvent | x | x | x | x | | | | | x |
|
||||||
| ContributionLinkDefineEvent | x | x | x | x | | | | | x |
|
| CONTRIBUTION_LINK_ACTIVATE_REDEEM | ContributionLinkRedeemEvent | x | x | x | x | | | | x | x |
|
||||||
| ContributionLinkActivateRedeemEvent | x | x | x | x | | | | x | x |
|
| | UserCreateContributionMessageEvent | x | x | x | x | | | | x | x |
|
||||||
|
| | AdminCreateContributionMessageEvent | x | x | x | x | | | | x | x |
|
||||||
|
| | LogoutEvent | x | x | x | x | | | | x | x |
|
||||||
|
| SEND_CONFIRMATION_EMAIL | SendConfirmEmailEvent | x | x | x | x | | | | | |
|
||||||
|
| | SendAccountMultiRegistrationEmailEvent | x | x | x | x | | | | | |
|
||||||
|
| | SendForgotPasswordEmailEvent | x | x | x | x | | | | | |
|
||||||
|
| | SendTransactionSendEmailEvent | x | x | x | x | x | x | x | | x |
|
||||||
|
| | SendTransactionReceiveEmailEvent | x | x | x | x | x | x | x | | x |
|
||||||
|
| | SendAddedContributionEmailEvent | x | x | x | x | | | | x | x |
|
||||||
|
| | SendContributionConfirmEmailEvent | x | x | x | x | | | | x | x |
|
||||||
|
| | SendTransactionLinkRedeemEmailEvent | x | x | x | x | x | x | x | | x |
|
||||||
|
| TRANSACTION_REPEATE_REDEEM | - | | | | | | | | | |
|
||||||
|
| TRANSACTION_RECEIVE_REDEEM | - | | | | | | | | | |
|
||||||
|
|
||||||
## Event creation
|
## Event creation
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
|
<redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
|
||||||
<b-jumbotron>
|
<b-jumbotron>
|
||||||
<div class="mb-3 text-center">
|
<div class="mb-3 text-center">
|
||||||
<b-button variant="primary" @click="$emit('redeem-link', linkData.amount)" size="lg">
|
<b-button variant="primary" @click="$emit('mutation-link', linkData.amount)" size="lg">
|
||||||
{{ $t('gdd_per_link.redeem') }}
|
{{ $t('gdd_per_link.redeem') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -178,7 +178,6 @@
|
|||||||
"no-redeem": "Du darfst deinen eigenen Link nicht einlösen!",
|
"no-redeem": "Du darfst deinen eigenen Link nicht einlösen!",
|
||||||
"not-copied": "Dein Gerät lässt das Kopieren leider nicht zu! Bitte kopiere den Link von Hand!",
|
"not-copied": "Dein Gerät lässt das Kopieren leider nicht zu! Bitte kopiere den Link von Hand!",
|
||||||
"redeem": "Einlösen",
|
"redeem": "Einlösen",
|
||||||
"redeem-text": "Willst du den Betrag jetzt einlösen?",
|
|
||||||
"redeemed": "Erfolgreich eingelöst! Deinem Konto wurden {n} GDD gutgeschrieben.",
|
"redeemed": "Erfolgreich eingelöst! Deinem Konto wurden {n} GDD gutgeschrieben.",
|
||||||
"redeemed-at": "Der Link wurde bereits am {date} eingelöst.",
|
"redeemed-at": "Der Link wurde bereits am {date} eingelöst.",
|
||||||
"redeemed-title": "eingelöst",
|
"redeemed-title": "eingelöst",
|
||||||
|
|||||||
@ -178,7 +178,6 @@
|
|||||||
"no-redeem": "You not allowed to redeem your own link!",
|
"no-redeem": "You not allowed to redeem your own link!",
|
||||||
"not-copied": "Unfortunately, your device does not allow copying! Please copy the link by hand!",
|
"not-copied": "Unfortunately, your device does not allow copying! Please copy the link by hand!",
|
||||||
"redeem": "Redeem",
|
"redeem": "Redeem",
|
||||||
"redeem-text": "Do you want to redeem the amount now?",
|
|
||||||
"redeemed": "Successfully redeemed! Your account has been credited with {n} GDD.",
|
"redeemed": "Successfully redeemed! Your account has been credited with {n} GDD.",
|
||||||
"redeemed-at": "The link was already redeemed on {date}.",
|
"redeemed-at": "The link was already redeemed on {date}.",
|
||||||
"redeemed-title": "redeemed",
|
"redeemed-title": "redeemed",
|
||||||
|
|||||||
@ -180,7 +180,6 @@
|
|||||||
"no-redeem": "No puedes canjear tu propio enlace!",
|
"no-redeem": "No puedes canjear tu propio enlace!",
|
||||||
"not-copied": "¡Desafortunadamente, su dispositivo no permite copiar! Copie el enlace manualmente!",
|
"not-copied": "¡Desafortunadamente, su dispositivo no permite copiar! Copie el enlace manualmente!",
|
||||||
"redeem": "Canjear",
|
"redeem": "Canjear",
|
||||||
"redeem-text": "¿Quieres canjear el importe ahora?",
|
|
||||||
"redeemed": "¡Canjeado con éxito! Tu cuenta ha sido acreditada con {n} GDD.",
|
"redeemed": "¡Canjeado con éxito! Tu cuenta ha sido acreditada con {n} GDD.",
|
||||||
"redeemed-at": "El enlace ya se canjeó el {date}.",
|
"redeemed-at": "El enlace ya se canjeó el {date}.",
|
||||||
"redeemed-title": "canjeado",
|
"redeemed-title": "canjeado",
|
||||||
|
|||||||
@ -180,7 +180,6 @@
|
|||||||
"no-redeem": "Vous n´êtes pas autorisé à percevoir votre propre lien!",
|
"no-redeem": "Vous n´êtes pas autorisé à percevoir votre propre lien!",
|
||||||
"not-copied": "Malheureusement votre appareil ne permet pas de copier! Veuillez copier le lien manuellement svp!",
|
"not-copied": "Malheureusement votre appareil ne permet pas de copier! Veuillez copier le lien manuellement svp!",
|
||||||
"redeem": "Encaisser",
|
"redeem": "Encaisser",
|
||||||
"redeem-text": "Voulez-vous percevoir le montant maintenant?",
|
|
||||||
"redeemed": "Encaissé avec succès! Votre compte est crédité de {n} GDD.",
|
"redeemed": "Encaissé avec succès! Votre compte est crédité de {n} GDD.",
|
||||||
"redeemed-at": "Le lien a déjà été perçu le {date}.",
|
"redeemed-at": "Le lien a déjà été perçu le {date}.",
|
||||||
"redeemed-title": "encaisser",
|
"redeemed-title": "encaisser",
|
||||||
|
|||||||
@ -180,7 +180,6 @@
|
|||||||
"no-redeem": "Je mag je eigen link niet inwisselen!",
|
"no-redeem": "Je mag je eigen link niet inwisselen!",
|
||||||
"not-copied": "Jouw apparaat laat het kopiëren helaas niet toe! Kopieer de link alsjeblieft met de hand!",
|
"not-copied": "Jouw apparaat laat het kopiëren helaas niet toe! Kopieer de link alsjeblieft met de hand!",
|
||||||
"redeem": "Inwisselen",
|
"redeem": "Inwisselen",
|
||||||
"redeem-text": "Wil je het bedrag nu inwisselen?",
|
|
||||||
"redeemed": "Succesvol ingewisseld! Op jouw rekening werden {n} GDD bijgeschreven.",
|
"redeemed": "Succesvol ingewisseld! Op jouw rekening werden {n} GDD bijgeschreven.",
|
||||||
"redeemed-at": "De link werd al op {date} ingewisseld.",
|
"redeemed-at": "De link werd al op {date} ingewisseld.",
|
||||||
"redeemed-title": "ingewisseld",
|
"redeemed-title": "ingewisseld",
|
||||||
|
|||||||
@ -282,19 +282,11 @@ describe('TransactionLink', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('redeem link with success', () => {
|
describe('redeem link with success', () => {
|
||||||
let spy
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
|
||||||
apolloMutateMock.mockResolvedValue()
|
apolloMutateMock.mockResolvedValue()
|
||||||
spy.mockImplementation(() => Promise.resolve(true))
|
|
||||||
await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click')
|
await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens the modal', () => {
|
|
||||||
expect(spy).toBeCalledWith('gdd_per_link.redeem-text')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('calls the API', () => {
|
it('calls the API', () => {
|
||||||
expect(apolloMutateMock).toBeCalledWith(
|
expect(apolloMutateMock).toBeCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -316,37 +308,9 @@ describe('TransactionLink', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('cancel redeem link', () => {
|
|
||||||
let spy
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
|
||||||
apolloMutateMock.mockResolvedValue()
|
|
||||||
spy.mockImplementation(() => Promise.resolve(false))
|
|
||||||
await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not call the API', () => {
|
|
||||||
expect(apolloMutateMock).not.toBeCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not toasts a success message', () => {
|
|
||||||
expect(mocks.$t).not.toBeCalledWith('gdd_per_link.redeemed', { n: '22' })
|
|
||||||
expect(toastSuccessSpy).not.toBeCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not push the route', () => {
|
|
||||||
expect(routerPushMock).not.toBeCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('redeem link with error', () => {
|
describe('redeem link with error', () => {
|
||||||
let spy
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
|
||||||
apolloMutateMock.mockRejectedValue({ message: 'Oh Noo!' })
|
apolloMutateMock.mockRejectedValue({ message: 'Oh Noo!' })
|
||||||
spy.mockImplementation(() => Promise.resolve(true))
|
|
||||||
await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click')
|
await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
<redeem-valid
|
<redeem-valid
|
||||||
:linkData="linkData"
|
:linkData="linkData"
|
||||||
:isContributionLink="isContributionLink"
|
:isContributionLink="isContributionLink"
|
||||||
@redeem-link="redeemLink"
|
@mutation-link="mutationLink"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -98,11 +98,6 @@ export default {
|
|||||||
this.$router.push('/overview')
|
this.$router.push('/overview')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
redeemLink(amount) {
|
|
||||||
this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.redeem-text')).then((value) => {
|
|
||||||
if (value) this.mutationLink(amount)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isContributionLink() {
|
isContributionLink() {
|
||||||
|
|||||||
@ -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