mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge pull request #2058 from gradido/1798-feature-gradidoid-1-adapt-and-migrate-database-schema
GradidoID 1: adapt and migrate database schema
This commit is contained in:
commit
22845c6264
@ -10,7 +10,7 @@ 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
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export class EventRegister extends EventBasicUserId {}
|
|||||||
export class EventRedeemRegister extends EventBasicRedeem {}
|
export class EventRedeemRegister extends EventBasicRedeem {}
|
||||||
export class EventInactiveAccount extends EventBasicUserId {}
|
export class EventInactiveAccount extends EventBasicUserId {}
|
||||||
export class EventSendConfirmationEmail extends EventBasicUserId {}
|
export class EventSendConfirmationEmail extends EventBasicUserId {}
|
||||||
|
export class EventSendAccountMultiRegistrationEmail extends EventBasicUserId {}
|
||||||
export class EventConfirmationEmail extends EventBasicUserId {}
|
export class EventConfirmationEmail extends EventBasicUserId {}
|
||||||
export class EventRegisterEmailKlicktipp extends EventBasicUserId {}
|
export class EventRegisterEmailKlicktipp extends EventBasicUserId {}
|
||||||
export class EventLogin extends EventBasicUserId {}
|
export class EventLogin extends EventBasicUserId {}
|
||||||
@ -113,6 +114,15 @@ export class Event {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setEventSendAccountMultiRegistrationEmail(
|
||||||
|
ev: EventSendAccountMultiRegistrationEmail,
|
||||||
|
): Event {
|
||||||
|
this.setByBasicUser(ev.userId)
|
||||||
|
this.type = EventProtocolType.SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
public setEventConfirmationEmail(ev: EventConfirmationEmail): Event {
|
public setEventConfirmationEmail(ev: EventConfirmationEmail): Event {
|
||||||
this.setByBasicUser(ev.userId)
|
this.setByBasicUser(ev.userId)
|
||||||
this.type = EventProtocolType.CONFIRM_EMAIL
|
this.type = EventProtocolType.CONFIRM_EMAIL
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export enum EventProtocolType {
|
|||||||
REDEEM_REGISTER = 'REDEEM_REGISTER',
|
REDEEM_REGISTER = 'REDEEM_REGISTER',
|
||||||
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
|
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
|
||||||
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
|
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
|
||||||
|
SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL',
|
||||||
CONFIRM_EMAIL = 'CONFIRM_EMAIL',
|
CONFIRM_EMAIL = 'CONFIRM_EMAIL',
|
||||||
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
|
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
|
||||||
LOGIN = 'LOGIN',
|
LOGIN = 'LOGIN',
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
|
|||||||
|
|
||||||
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
|
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
|
||||||
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
|
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
|
||||||
const userRepository = await getCustomRepository(UserRepository)
|
const userRepository = getCustomRepository(UserRepository)
|
||||||
try {
|
try {
|
||||||
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
||||||
context.user = user
|
context.user = user
|
||||||
|
|||||||
11
backend/src/graphql/enum/UserContactType.ts
Normal file
11
backend/src/graphql/enum/UserContactType.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { registerEnumType } from 'type-graphql'
|
||||||
|
|
||||||
|
export enum UserContactType {
|
||||||
|
USER_CONTACT_EMAIL = 'EMAIL',
|
||||||
|
USER_CONTACT_PHONE = 'PHONE',
|
||||||
|
}
|
||||||
|
|
||||||
|
registerEnumType(UserContactType, {
|
||||||
|
name: 'UserContactType', // this one is mandatory
|
||||||
|
description: 'Type of the user contact', // this one is optional
|
||||||
|
})
|
||||||
@ -13,7 +13,7 @@ export class UnconfirmedContribution {
|
|||||||
this.date = contribution.contributionDate
|
this.date = contribution.contributionDate
|
||||||
this.firstName = user ? user.firstName : ''
|
this.firstName = user ? user.firstName : ''
|
||||||
this.lastName = user ? user.lastName : ''
|
this.lastName = user ? user.lastName : ''
|
||||||
this.email = user ? user.email : ''
|
this.email = user ? user.emailContact.email : ''
|
||||||
this.moderator = contribution.moderatorId
|
this.moderator = contribution.moderatorId
|
||||||
this.creation = creations
|
this.creation = creations
|
||||||
this.state = contribution.contributionStatus
|
this.state = contribution.contributionStatus
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { KlickTipp } from './KlickTipp'
|
|||||||
import { User as dbUser } from '@entity/User'
|
import { User as dbUser } from '@entity/User'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { FULL_CREATION_AVAILABLE } from '../resolver/const/const'
|
import { FULL_CREATION_AVAILABLE } from '../resolver/const/const'
|
||||||
|
import { UserContact } from './UserContact'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class User {
|
export class User {
|
||||||
@ -10,12 +11,16 @@ export class User {
|
|||||||
this.id = user.id
|
this.id = user.id
|
||||||
this.gradidoID = user.gradidoID
|
this.gradidoID = user.gradidoID
|
||||||
this.alias = user.alias
|
this.alias = user.alias
|
||||||
this.email = user.email
|
this.emailId = user.emailId
|
||||||
|
if (user.emailContact) {
|
||||||
|
this.email = user.emailContact.email
|
||||||
|
this.emailContact = new UserContact(user.emailContact)
|
||||||
|
this.emailChecked = user.emailContact.emailChecked
|
||||||
|
}
|
||||||
this.firstName = user.firstName
|
this.firstName = user.firstName
|
||||||
this.lastName = user.lastName
|
this.lastName = user.lastName
|
||||||
this.deletedAt = user.deletedAt
|
this.deletedAt = user.deletedAt
|
||||||
this.createdAt = user.createdAt
|
this.createdAt = user.createdAt
|
||||||
this.emailChecked = user.emailChecked
|
|
||||||
this.language = user.language
|
this.language = user.language
|
||||||
this.publisherId = user.publisherId
|
this.publisherId = user.publisherId
|
||||||
this.isAdmin = user.isAdmin
|
this.isAdmin = user.isAdmin
|
||||||
@ -34,12 +39,18 @@ export class User {
|
|||||||
gradidoID: string
|
gradidoID: string
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
alias: string
|
alias?: string
|
||||||
|
|
||||||
|
@Field(() => Number, { nullable: true })
|
||||||
|
emailId: number | null
|
||||||
|
|
||||||
// TODO privacy issue here
|
// TODO privacy issue here
|
||||||
@Field(() => String)
|
@Field(() => String, { nullable: true })
|
||||||
email: string
|
email: string
|
||||||
|
|
||||||
|
@Field(() => UserContact)
|
||||||
|
emailContact: UserContact
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
firstName: string | null
|
firstName: string | null
|
||||||
|
|
||||||
|
|||||||
@ -6,11 +6,11 @@ import { User } from '@entity/User'
|
|||||||
export class UserAdmin {
|
export class UserAdmin {
|
||||||
constructor(user: User, creation: Decimal[], hasElopage: boolean, emailConfirmationSend: string) {
|
constructor(user: User, creation: Decimal[], hasElopage: boolean, emailConfirmationSend: string) {
|
||||||
this.userId = user.id
|
this.userId = user.id
|
||||||
this.email = user.email
|
this.email = user.emailContact.email
|
||||||
this.firstName = user.firstName
|
this.firstName = user.firstName
|
||||||
this.lastName = user.lastName
|
this.lastName = user.lastName
|
||||||
this.creation = creation
|
this.creation = creation
|
||||||
this.emailChecked = user.emailChecked
|
this.emailChecked = user.emailContact.emailChecked
|
||||||
this.hasElopage = hasElopage
|
this.hasElopage = hasElopage
|
||||||
this.deletedAt = user.deletedAt
|
this.deletedAt = user.deletedAt
|
||||||
this.emailConfirmationSend = emailConfirmationSend
|
this.emailConfirmationSend = emailConfirmationSend
|
||||||
|
|||||||
56
backend/src/graphql/model/UserContact.ts
Normal file
56
backend/src/graphql/model/UserContact.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { ObjectType, Field } from 'type-graphql'
|
||||||
|
import { UserContact as dbUserContact } from '@entity/UserContact'
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class UserContact {
|
||||||
|
constructor(userContact: dbUserContact) {
|
||||||
|
this.id = userContact.id
|
||||||
|
this.type = userContact.type
|
||||||
|
this.userId = userContact.userId
|
||||||
|
this.email = userContact.email
|
||||||
|
// this.emailVerificationCode = userContact.emailVerificationCode
|
||||||
|
this.emailOptInTypeId = userContact.emailOptInTypeId
|
||||||
|
this.emailResendCount = userContact.emailResendCount
|
||||||
|
this.emailChecked = userContact.emailChecked
|
||||||
|
this.phone = userContact.phone
|
||||||
|
this.createdAt = userContact.createdAt
|
||||||
|
this.updatedAt = userContact.updatedAt
|
||||||
|
this.deletedAt = userContact.deletedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
type: string
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
userId: number
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
email: string
|
||||||
|
|
||||||
|
// @Field(() => BigInt, { nullable: true })
|
||||||
|
// emailVerificationCode: BigInt | null
|
||||||
|
|
||||||
|
@Field(() => Number, { nullable: true })
|
||||||
|
emailOptInTypeId: number | null
|
||||||
|
|
||||||
|
@Field(() => Number, { nullable: true })
|
||||||
|
emailResendCount: number | null
|
||||||
|
|
||||||
|
@Field(() => Boolean)
|
||||||
|
emailChecked: boolean
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
phone: string | null
|
||||||
|
|
||||||
|
@Field(() => Date)
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@Field(() => Date, { nullable: true })
|
||||||
|
updatedAt: Date | null
|
||||||
|
|
||||||
|
@Field(() => Date, { nullable: true })
|
||||||
|
deletedAt: Date | null
|
||||||
|
}
|
||||||
@ -1126,7 +1126,9 @@ describe('AdminResolver', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('Could not find user with email: bob@baumeister.de')],
|
errors: [
|
||||||
|
new GraphQLError('Could not find UserContact with email: bob@baumeister.de'),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -1516,6 +1518,7 @@ describe('AdminResolver', () => {
|
|||||||
)
|
)
|
||||||
await expect(r2).resolves.toEqual(
|
await expect(r2).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
// data: { confirmContribution: true },
|
||||||
errors: [new GraphQLError('Creation was not successful.')],
|
errors: [new GraphQLError('Creation was not successful.')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -4,8 +4,6 @@ import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx, Int } from 'type
|
|||||||
import {
|
import {
|
||||||
getCustomRepository,
|
getCustomRepository,
|
||||||
IsNull,
|
IsNull,
|
||||||
Not,
|
|
||||||
ObjectLiteral,
|
|
||||||
getConnection,
|
getConnection,
|
||||||
In,
|
In,
|
||||||
MoreThan,
|
MoreThan,
|
||||||
@ -32,7 +30,6 @@ import { TransactionRepository } from '@repository/Transaction'
|
|||||||
import { calculateDecay } from '@/util/decay'
|
import { calculateDecay } from '@/util/decay'
|
||||||
import { Contribution } from '@entity/Contribution'
|
import { Contribution } from '@entity/Contribution'
|
||||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
|
||||||
import { User as dbUser } from '@entity/User'
|
import { User as dbUser } from '@entity/User'
|
||||||
import { User } from '@model/User'
|
import { User } from '@model/User'
|
||||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||||
@ -44,7 +41,7 @@ import Paginated from '@arg/Paginated'
|
|||||||
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
|
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
|
||||||
import { Order } from '@enum/Order'
|
import { Order } from '@enum/Order'
|
||||||
import { communityUser } from '@/util/communityUser'
|
import { communityUser } from '@/util/communityUser'
|
||||||
import { checkOptInCode, activationLink, printTimeDuration } from './UserResolver'
|
import { findUserByEmail, activationLink, printTimeDuration } from './UserResolver'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
@ -62,6 +59,7 @@ import {
|
|||||||
MEMO_MAX_CHARS,
|
MEMO_MAX_CHARS,
|
||||||
MEMO_MIN_CHARS,
|
MEMO_MIN_CHARS,
|
||||||
} from './const/const'
|
} from './const/const'
|
||||||
|
import { UserContact } from '@entity/UserContact'
|
||||||
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
|
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
|
||||||
import ContributionMessageArgs from '@arg/ContributionMessageArgs'
|
import ContributionMessageArgs from '@arg/ContributionMessageArgs'
|
||||||
import { ContributionMessageType } from '@enum/MessageType'
|
import { ContributionMessageType } from '@enum/MessageType'
|
||||||
@ -81,24 +79,12 @@ export class AdminResolver {
|
|||||||
{ searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs,
|
{ searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs,
|
||||||
): Promise<SearchUsersResult> {
|
): Promise<SearchUsersResult> {
|
||||||
const userRepository = getCustomRepository(UserRepository)
|
const userRepository = getCustomRepository(UserRepository)
|
||||||
|
|
||||||
const filterCriteria: ObjectLiteral[] = []
|
|
||||||
if (filters) {
|
|
||||||
if (filters.byActivated !== null) {
|
|
||||||
filterCriteria.push({ emailChecked: filters.byActivated })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.byDeleted !== null) {
|
|
||||||
filterCriteria.push({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userFields = [
|
const userFields = [
|
||||||
'id',
|
'id',
|
||||||
'firstName',
|
'firstName',
|
||||||
'lastName',
|
'lastName',
|
||||||
'email',
|
'emailId',
|
||||||
'emailChecked',
|
'emailContact',
|
||||||
'deletedAt',
|
'deletedAt',
|
||||||
'isAdmin',
|
'isAdmin',
|
||||||
]
|
]
|
||||||
@ -107,7 +93,7 @@ export class AdminResolver {
|
|||||||
return 'user.' + fieldName
|
return 'user.' + fieldName
|
||||||
}),
|
}),
|
||||||
searchText,
|
searchText,
|
||||||
filterCriteria,
|
filters,
|
||||||
currentPage,
|
currentPage,
|
||||||
pageSize,
|
pageSize,
|
||||||
)
|
)
|
||||||
@ -124,32 +110,18 @@ export class AdminResolver {
|
|||||||
const adminUsers = await Promise.all(
|
const adminUsers = await Promise.all(
|
||||||
users.map(async (user) => {
|
users.map(async (user) => {
|
||||||
let emailConfirmationSend = ''
|
let emailConfirmationSend = ''
|
||||||
if (!user.emailChecked) {
|
if (!user.emailContact.emailChecked) {
|
||||||
const emailOptIn = await LoginEmailOptIn.findOne(
|
if (user.emailContact.updatedAt) {
|
||||||
{
|
emailConfirmationSend = user.emailContact.updatedAt.toISOString()
|
||||||
userId: user.id,
|
} else {
|
||||||
},
|
emailConfirmationSend = user.emailContact.createdAt.toISOString()
|
||||||
{
|
|
||||||
order: {
|
|
||||||
updatedAt: 'DESC',
|
|
||||||
createdAt: 'DESC',
|
|
||||||
},
|
|
||||||
select: ['updatedAt', 'createdAt'],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (emailOptIn) {
|
|
||||||
if (emailOptIn.updatedAt) {
|
|
||||||
emailConfirmationSend = emailOptIn.updatedAt.toISOString()
|
|
||||||
} else {
|
|
||||||
emailConfirmationSend = emailOptIn.createdAt.toISOString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const userCreations = creations.find((c) => c.id === user.id)
|
const userCreations = creations.find((c) => c.id === user.id)
|
||||||
const adminUser = new UserAdmin(
|
const adminUser = new UserAdmin(
|
||||||
user,
|
user,
|
||||||
userCreations ? userCreations.creations : FULL_CREATION_AVAILABLE,
|
userCreations ? userCreations.creations : FULL_CREATION_AVAILABLE,
|
||||||
await hasElopageBuys(user.email),
|
await hasElopageBuys(user.emailContact.email),
|
||||||
emailConfirmationSend,
|
emailConfirmationSend,
|
||||||
)
|
)
|
||||||
return adminUser
|
return adminUser
|
||||||
@ -245,24 +217,39 @@ export class AdminResolver {
|
|||||||
@Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs,
|
@Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs,
|
||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<Decimal[]> {
|
): Promise<Decimal[]> {
|
||||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
logger.info(
|
||||||
if (!user) {
|
`adminCreateContribution(email=${email}, amount=${amount}, memo=${memo}, creationDate=${creationDate})`,
|
||||||
|
)
|
||||||
|
const emailContact = await UserContact.findOne({
|
||||||
|
where: { email },
|
||||||
|
withDeleted: true,
|
||||||
|
relations: ['user'],
|
||||||
|
})
|
||||||
|
if (!emailContact) {
|
||||||
|
logger.error(`Could not find user with email: ${email}`)
|
||||||
throw new Error(`Could not find user with email: ${email}`)
|
throw new Error(`Could not find user with email: ${email}`)
|
||||||
}
|
}
|
||||||
if (user.deletedAt) {
|
if (emailContact.deletedAt) {
|
||||||
|
logger.error('This emailContact was deleted. Cannot create a contribution.')
|
||||||
|
throw new Error('This emailContact was deleted. Cannot create a contribution.')
|
||||||
|
}
|
||||||
|
if (emailContact.user.deletedAt) {
|
||||||
|
logger.error('This user was deleted. Cannot create a contribution.')
|
||||||
throw new Error('This user was deleted. Cannot create a contribution.')
|
throw new Error('This user was deleted. Cannot create a contribution.')
|
||||||
}
|
}
|
||||||
if (!user.emailChecked) {
|
if (!emailContact.emailChecked) {
|
||||||
|
logger.error('Contribution could not be saved, Email is not activated')
|
||||||
throw new Error('Contribution could not be saved, Email is not activated')
|
throw new Error('Contribution could not be saved, Email is not activated')
|
||||||
}
|
}
|
||||||
const moderator = getUser(context)
|
const moderator = getUser(context)
|
||||||
logger.trace('moderator: ', moderator.id)
|
logger.trace('moderator: ', moderator.id)
|
||||||
const creations = await getUserCreation(user.id)
|
const creations = await getUserCreation(emailContact.userId)
|
||||||
logger.trace('creations', creations)
|
logger.trace('creations:', creations)
|
||||||
const creationDateObj = new Date(creationDate)
|
const creationDateObj = new Date(creationDate)
|
||||||
|
logger.trace('creationDateObj:', creationDateObj)
|
||||||
validateContribution(creations, amount, creationDateObj)
|
validateContribution(creations, amount, creationDateObj)
|
||||||
const contribution = Contribution.create()
|
const contribution = Contribution.create()
|
||||||
contribution.userId = user.id
|
contribution.userId = emailContact.userId
|
||||||
contribution.amount = amount
|
contribution.amount = amount
|
||||||
contribution.createdAt = new Date()
|
contribution.createdAt = new Date()
|
||||||
contribution.contributionDate = creationDateObj
|
contribution.contributionDate = creationDateObj
|
||||||
@ -273,7 +260,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
logger.trace('contribution to save', contribution)
|
logger.trace('contribution to save', contribution)
|
||||||
await Contribution.save(contribution)
|
await Contribution.save(contribution)
|
||||||
return getUserCreation(user.id)
|
return getUserCreation(emailContact.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTIONS])
|
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTIONS])
|
||||||
@ -309,11 +296,22 @@ export class AdminResolver {
|
|||||||
@Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs,
|
@Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs,
|
||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<AdminUpdateContribution> {
|
): Promise<AdminUpdateContribution> {
|
||||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
const emailContact = await UserContact.findOne({
|
||||||
|
where: { email },
|
||||||
|
withDeleted: true,
|
||||||
|
relations: ['user'],
|
||||||
|
})
|
||||||
|
if (!emailContact) {
|
||||||
|
logger.error(`Could not find UserContact with email: ${email}`)
|
||||||
|
throw new Error(`Could not find UserContact with email: ${email}`)
|
||||||
|
}
|
||||||
|
const user = emailContact.user
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`Could not find user with email: ${email}`)
|
logger.error(`Could not find User to emailContact: ${email}`)
|
||||||
|
throw new Error(`Could not find User to emailContact: ${email}`)
|
||||||
}
|
}
|
||||||
if (user.deletedAt) {
|
if (user.deletedAt) {
|
||||||
|
logger.error(`User was deleted (${email})`)
|
||||||
throw new Error(`User was deleted (${email})`)
|
throw new Error(`User was deleted (${email})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,14 +322,17 @@ export class AdminResolver {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!contributionToUpdate) {
|
if (!contributionToUpdate) {
|
||||||
|
logger.error('No contribution found to given id.')
|
||||||
throw new Error('No contribution found to given id.')
|
throw new Error('No contribution found to given id.')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contributionToUpdate.userId !== user.id) {
|
if (contributionToUpdate.userId !== user.id) {
|
||||||
|
logger.error('user of the pending contribution and send user does not correspond')
|
||||||
throw new Error('user of the pending contribution and send user does not correspond')
|
throw new Error('user of the pending contribution and send user does not correspond')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contributionToUpdate.moderatorId === null) {
|
if (contributionToUpdate.moderatorId === null) {
|
||||||
|
logger.error('An admin is not allowed to update a user contribution.')
|
||||||
throw new Error('An admin is not allowed to update a user contribution.')
|
throw new Error('An admin is not allowed to update a user contribution.')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,7 +378,11 @@ export class AdminResolver {
|
|||||||
|
|
||||||
const userIds = contributions.map((p) => p.userId)
|
const userIds = contributions.map((p) => p.userId)
|
||||||
const userCreations = await getUserCreations(userIds)
|
const userCreations = await getUserCreations(userIds)
|
||||||
const users = await dbUser.find({ where: { id: In(userIds) }, withDeleted: true })
|
const users = await dbUser.find({
|
||||||
|
where: { id: In(userIds) },
|
||||||
|
withDeleted: true,
|
||||||
|
relations: ['emailContact'],
|
||||||
|
})
|
||||||
|
|
||||||
return contributions.map((contribution) => {
|
return contributions.map((contribution) => {
|
||||||
const user = users.find((u) => u.id === contribution.userId)
|
const user = users.find((u) => u.id === contribution.userId)
|
||||||
@ -396,6 +401,7 @@ export class AdminResolver {
|
|||||||
async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise<boolean> {
|
async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise<boolean> {
|
||||||
const contribution = await Contribution.findOne(id)
|
const contribution = await Contribution.findOne(id)
|
||||||
if (!contribution) {
|
if (!contribution) {
|
||||||
|
logger.error(`Contribution not found for given id: ${id}`)
|
||||||
throw new Error('Contribution not found for given id.')
|
throw new Error('Contribution not found for given id.')
|
||||||
}
|
}
|
||||||
contribution.contributionStatus = ContributionStatus.DELETED
|
contribution.contributionStatus = ContributionStatus.DELETED
|
||||||
@ -412,15 +418,22 @@ export class AdminResolver {
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const contribution = await Contribution.findOne(id)
|
const contribution = await Contribution.findOne(id)
|
||||||
if (!contribution) {
|
if (!contribution) {
|
||||||
|
logger.error(`Contribution not found for given id: ${id}`)
|
||||||
throw new Error('Contribution not found to given id.')
|
throw new Error('Contribution not found to given id.')
|
||||||
}
|
}
|
||||||
const moderatorUser = getUser(context)
|
const moderatorUser = getUser(context)
|
||||||
if (moderatorUser.id === contribution.userId)
|
if (moderatorUser.id === contribution.userId) {
|
||||||
|
logger.error('Moderator can not confirm own contribution')
|
||||||
throw new Error('Moderator can not confirm own contribution')
|
throw new Error('Moderator can not confirm own contribution')
|
||||||
|
}
|
||||||
const user = await dbUser.findOneOrFail({ id: contribution.userId }, { withDeleted: true })
|
const user = await dbUser.findOneOrFail(
|
||||||
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a contribution.')
|
{ id: contribution.userId },
|
||||||
|
{ withDeleted: true, relations: ['emailContact'] },
|
||||||
|
)
|
||||||
|
if (user.deletedAt) {
|
||||||
|
logger.error('This user was deleted. Cannot confirm a contribution.')
|
||||||
|
throw new Error('This user was deleted. Cannot confirm a contribution.')
|
||||||
|
}
|
||||||
const creations = await getUserCreation(contribution.userId, false)
|
const creations = await getUserCreation(contribution.userId, false)
|
||||||
validateContribution(creations, contribution.amount, contribution.contributionDate)
|
validateContribution(creations, contribution.amount, contribution.contributionDate)
|
||||||
|
|
||||||
@ -428,7 +441,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
|
||||||
try {
|
try {
|
||||||
const lastTransaction = await queryRunner.manager
|
const lastTransaction = await queryRunner.manager
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
@ -477,7 +490,7 @@ export class AdminResolver {
|
|||||||
senderLastName: moderatorUser.lastName,
|
senderLastName: moderatorUser.lastName,
|
||||||
recipientFirstName: user.firstName,
|
recipientFirstName: user.firstName,
|
||||||
recipientLastName: user.lastName,
|
recipientLastName: user.lastName,
|
||||||
recipientEmail: user.email,
|
recipientEmail: user.emailContact.email,
|
||||||
contributionMemo: contribution.memo,
|
contributionMemo: contribution.memo,
|
||||||
contributionAmount: contribution.amount,
|
contributionAmount: contribution.amount,
|
||||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||||
@ -517,32 +530,35 @@ export class AdminResolver {
|
|||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
|
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
const user = await dbUser.findOneOrFail({ email: email })
|
// const user = await dbUser.findOne({ id: emailContact.userId })
|
||||||
|
const user = await findUserByEmail(email)
|
||||||
// can be both types: REGISTER and RESET_PASSWORD
|
if (!user) {
|
||||||
let optInCode = await LoginEmailOptIn.findOne({
|
logger.error(`Could not find User to emailContact: ${email}`)
|
||||||
where: { userId: user.id },
|
throw new Error(`Could not find User to emailContact: ${email}`)
|
||||||
order: { updatedAt: 'DESC' },
|
}
|
||||||
})
|
if (user.deletedAt) {
|
||||||
|
logger.error(`User with emailContact: ${email} is deleted.`)
|
||||||
optInCode = await checkOptInCode(optInCode, user)
|
throw new Error(`User with emailContact: ${email} is deleted.`)
|
||||||
|
}
|
||||||
|
const emailContact = user.emailContact
|
||||||
|
if (emailContact.deletedAt) {
|
||||||
|
logger.error(`The emailContact: ${email} of htis User is deleted.`)
|
||||||
|
throw new Error(`The emailContact: ${email} of htis User is deleted.`)
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const emailSent = await sendAccountActivationEmail({
|
const emailSent = await sendAccountActivationEmail({
|
||||||
link: activationLink(optInCode),
|
link: activationLink(emailContact.emailVerificationCode),
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
email,
|
email,
|
||||||
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
|
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||||
})
|
})
|
||||||
|
|
||||||
/* uncomment this, when you need the activation link on the console
|
|
||||||
// In case EMails are disabled log the activation link for the user
|
// In case EMails are disabled log the activation link for the user
|
||||||
if (!emailSent) {
|
if (!emailSent) {
|
||||||
// eslint-disable-next-line no-console
|
logger.info(`Account confirmation link: ${activationLink}`)
|
||||||
console.log(`Account confirmation link: ${activationLink}`)
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -720,9 +736,12 @@ export class AdminResolver {
|
|||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<ContributionMessage> {
|
): Promise<ContributionMessage> {
|
||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
|
if (!user.emailContact) {
|
||||||
|
user.emailContact = await UserContact.findOneOrFail({ where: { id: user.emailId } })
|
||||||
|
}
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
const contributionMessage = DbContributionMessage.create()
|
const contributionMessage = DbContributionMessage.create()
|
||||||
try {
|
try {
|
||||||
const contribution = await Contribution.findOne({
|
const contribution = await Contribution.findOne({
|
||||||
@ -735,6 +754,11 @@ export class AdminResolver {
|
|||||||
if (contribution.userId === user.id) {
|
if (contribution.userId === user.id) {
|
||||||
throw new Error('Admin can not answer on own contribution')
|
throw new Error('Admin can not answer on own contribution')
|
||||||
}
|
}
|
||||||
|
if (!contribution.user.emailContact) {
|
||||||
|
contribution.user.emailContact = await UserContact.findOneOrFail({
|
||||||
|
where: { id: contribution.user.emailId },
|
||||||
|
})
|
||||||
|
}
|
||||||
contributionMessage.contributionId = contributionId
|
contributionMessage.contributionId = contributionId
|
||||||
contributionMessage.createdAt = new Date()
|
contributionMessage.createdAt = new Date()
|
||||||
contributionMessage.message = message
|
contributionMessage.message = message
|
||||||
@ -751,19 +775,19 @@ export class AdminResolver {
|
|||||||
contribution.contributionStatus = ContributionStatus.IN_PROGRESS
|
contribution.contributionStatus = ContributionStatus.IN_PROGRESS
|
||||||
await queryRunner.manager.update(Contribution, { id: contributionId }, contribution)
|
await queryRunner.manager.update(Contribution, { id: contributionId }, contribution)
|
||||||
}
|
}
|
||||||
await queryRunner.commitTransaction()
|
|
||||||
|
|
||||||
await sendAddedContributionMessageEmail({
|
await sendAddedContributionMessageEmail({
|
||||||
senderFirstName: user.firstName,
|
senderFirstName: user.firstName,
|
||||||
senderLastName: user.lastName,
|
senderLastName: user.lastName,
|
||||||
recipientFirstName: contribution.user.firstName,
|
recipientFirstName: contribution.user.firstName,
|
||||||
recipientLastName: contribution.user.lastName,
|
recipientLastName: contribution.user.lastName,
|
||||||
recipientEmail: contribution.user.email,
|
recipientEmail: contribution.user.emailContact.email,
|
||||||
senderEmail: user.email,
|
senderEmail: user.emailContact.email,
|
||||||
contributionMemo: contribution.memo,
|
contributionMemo: contribution.memo,
|
||||||
message,
|
message,
|
||||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||||
})
|
})
|
||||||
|
await queryRunner.commitTransaction()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
logger.error(`ContributionMessage was not successful: ${e}`)
|
logger.error(`ContributionMessage was not successful: ${e}`)
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export class ContributionMessageResolver {
|
|||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
const contributionMessage = DbContributionMessage.create()
|
const contributionMessage = DbContributionMessage.create()
|
||||||
try {
|
try {
|
||||||
const contribution = await Contribution.findOne({ id: contributionId })
|
const contribution = await Contribution.findOne({ id: contributionId })
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export class GdtResolver {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const resultGDT = await apiGet(
|
const resultGDT = await apiGet(
|
||||||
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.email}/${currentPage}/${pageSize}/${order}`,
|
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.emailContact.email}/${currentPage}/${pageSize}/${order}`,
|
||||||
)
|
)
|
||||||
if (!resultGDT.success) {
|
if (!resultGDT.success) {
|
||||||
throw new Error(resultGDT.data)
|
throw new Error(resultGDT.data)
|
||||||
@ -37,7 +37,7 @@ export class GdtResolver {
|
|||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
try {
|
try {
|
||||||
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
||||||
email: user.email,
|
email: user.emailContact.email,
|
||||||
})
|
})
|
||||||
if (!resultGDTSum.success) {
|
if (!resultGDTSum.success) {
|
||||||
throw new Error('Call not successful')
|
throw new Error('Call not successful')
|
||||||
|
|||||||
@ -178,7 +178,7 @@ export class TransactionLinkResolver {
|
|||||||
logger.info('redeem contribution link...')
|
logger.info('redeem contribution link...')
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('SERIALIZABLE')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
try {
|
try {
|
||||||
const contributionLink = await queryRunner.manager
|
const contributionLink = await queryRunner.manager
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
@ -283,7 +283,10 @@ export class TransactionLinkResolver {
|
|||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
||||||
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
const linkedUser = await dbUser.findOneOrFail(
|
||||||
|
{ id: transactionLink.userId },
|
||||||
|
{ relations: ['emailContact'] },
|
||||||
|
)
|
||||||
|
|
||||||
if (user.id === linkedUser.id) {
|
if (user.id === linkedUser.id) {
|
||||||
throw new Error('Cannot redeem own transaction link.')
|
throw new Error('Cannot redeem own transaction link.')
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import Decimal from 'decimal.js-light'
|
|||||||
|
|
||||||
import { BalanceResolver } from './BalanceResolver'
|
import { BalanceResolver } from './BalanceResolver'
|
||||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||||
|
import { findUserByEmail } from './UserResolver'
|
||||||
import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed'
|
import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed'
|
||||||
|
|
||||||
export const executeTransaction = async (
|
export const executeTransaction = async (
|
||||||
@ -79,7 +80,7 @@ export const executeTransaction = async (
|
|||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
logger.debug(`open Transaction to write...`)
|
logger.debug(`open Transaction to write...`)
|
||||||
try {
|
try {
|
||||||
// transaction
|
// transaction
|
||||||
@ -149,8 +150,8 @@ export const executeTransaction = async (
|
|||||||
senderLastName: sender.lastName,
|
senderLastName: sender.lastName,
|
||||||
recipientFirstName: recipient.firstName,
|
recipientFirstName: recipient.firstName,
|
||||||
recipientLastName: recipient.lastName,
|
recipientLastName: recipient.lastName,
|
||||||
email: recipient.email,
|
email: recipient.emailContact.email,
|
||||||
senderEmail: sender.email,
|
senderEmail: sender.emailContact.email,
|
||||||
amount,
|
amount,
|
||||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||||
})
|
})
|
||||||
@ -160,8 +161,8 @@ export const executeTransaction = async (
|
|||||||
senderLastName: recipient.lastName,
|
senderLastName: recipient.lastName,
|
||||||
recipientFirstName: sender.firstName,
|
recipientFirstName: sender.firstName,
|
||||||
recipientLastName: sender.lastName,
|
recipientLastName: sender.lastName,
|
||||||
email: sender.email,
|
email: sender.emailContact.email,
|
||||||
senderEmail: recipient.email,
|
senderEmail: recipient.emailContact.email,
|
||||||
amount,
|
amount,
|
||||||
memo,
|
memo,
|
||||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||||
@ -184,7 +185,7 @@ export class TransactionResolver {
|
|||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
|
|
||||||
logger.addContext('user', user.id)
|
logger.addContext('user', user.id)
|
||||||
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.email})`)
|
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`)
|
||||||
|
|
||||||
// find current balance
|
// find current balance
|
||||||
const lastTransaction = await dbTransaction.findOne(
|
const lastTransaction = await dbTransaction.findOne(
|
||||||
@ -306,16 +307,25 @@ export class TransactionResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validate recipient user
|
// validate recipient user
|
||||||
const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true })
|
const recipientUser = await findUserByEmail(email)
|
||||||
|
/*
|
||||||
|
const emailContact = await UserContact.findOne({ email }, { withDeleted: true })
|
||||||
|
if (!emailContact) {
|
||||||
|
logger.error(`Could not find UserContact with email: ${email}`)
|
||||||
|
throw new Error(`Could not find UserContact with email: ${email}`)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// const recipientUser = await dbUser.findOne({ id: emailContact.userId })
|
||||||
if (!recipientUser) {
|
if (!recipientUser) {
|
||||||
logger.error(`recipient not known: email=${email}`)
|
logger.error(`unknown recipient to UserContact: email=${email}`)
|
||||||
throw new Error('recipient not known')
|
throw new Error('unknown recipient')
|
||||||
}
|
}
|
||||||
if (recipientUser.deletedAt) {
|
if (recipientUser.deletedAt) {
|
||||||
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
|
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
|
||||||
throw new Error('The recipient account was deleted')
|
throw new Error('The recipient account was deleted')
|
||||||
}
|
}
|
||||||
if (!recipientUser.emailChecked) {
|
const emailContact = recipientUser.emailContact
|
||||||
|
if (!emailContact.emailChecked) {
|
||||||
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`)
|
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`)
|
||||||
throw new Error('The recipient account is not activated')
|
throw new Error('The recipient account is not activated')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
import { testEnvironment, headerPushMock, resetToken, cleanDB, resetEntity } from '@test/helpers'
|
import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers'
|
||||||
import { userFactory } from '@/seeds/factory/user'
|
import { userFactory } from '@/seeds/factory/user'
|
||||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||||
import {
|
import {
|
||||||
@ -14,7 +14,6 @@ import {
|
|||||||
} from '@/seeds/graphql/mutations'
|
} from '@/seeds/graphql/mutations'
|
||||||
import { login, logout, verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries'
|
import { login, logout, verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries'
|
||||||
import { GraphQLError } from 'graphql'
|
import { GraphQLError } from 'graphql'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
|
||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
@ -31,6 +30,9 @@ import { EventProtocol } from '@entity/EventProtocol'
|
|||||||
import { logger } from '@test/testSetup'
|
import { logger } from '@test/testSetup'
|
||||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||||
|
import { UserContact } from '@entity/UserContact'
|
||||||
|
import { OptInType } from '../enum/OptInType'
|
||||||
|
import { UserContactType } from '../enum/UserContactType'
|
||||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||||
|
|
||||||
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||||
@ -92,7 +94,7 @@ describe('UserResolver', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let result: any
|
let result: any
|
||||||
let emailOptIn: string
|
let emailVerificationCode: string
|
||||||
let user: User[]
|
let user: User[]
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@ -111,11 +113,11 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('valid input data', () => {
|
describe('valid input data', () => {
|
||||||
let loginEmailOptIn: LoginEmailOptIn[]
|
// let loginEmailOptIn: LoginEmailOptIn[]
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
user = await User.find()
|
user = await User.find({ relations: ['emailContact'] })
|
||||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
// loginEmailOptIn = await LoginEmailOptIn.find()
|
||||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
emailVerificationCode = user[0].emailContact.emailVerificationCode.toString()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('filling all tables', () => {
|
describe('filling all tables', () => {
|
||||||
@ -125,15 +127,16 @@ describe('UserResolver', () => {
|
|||||||
id: expect.any(Number),
|
id: expect.any(Number),
|
||||||
gradidoID: expect.any(String),
|
gradidoID: expect.any(String),
|
||||||
alias: null,
|
alias: null,
|
||||||
email: 'peter@lustig.de',
|
emailContact: expect.any(UserContact), // 'peter@lustig.de',
|
||||||
|
emailId: expect.any(Number),
|
||||||
firstName: 'Peter',
|
firstName: 'Peter',
|
||||||
lastName: 'Lustig',
|
lastName: 'Lustig',
|
||||||
password: '0',
|
password: '0',
|
||||||
pubKey: null,
|
pubKey: null,
|
||||||
privKey: null,
|
privKey: null,
|
||||||
emailHash: expect.any(Buffer),
|
// emailHash: expect.any(Buffer),
|
||||||
createdAt: expect.any(Date),
|
createdAt: expect.any(Date),
|
||||||
emailChecked: false,
|
// emailChecked: false,
|
||||||
passphrase: expect.any(String),
|
passphrase: expect.any(String),
|
||||||
language: 'de',
|
language: 'de',
|
||||||
isAdmin: null,
|
isAdmin: null,
|
||||||
@ -149,18 +152,21 @@ describe('UserResolver', () => {
|
|||||||
expect(verUUID).toEqual(4)
|
expect(verUUID).toEqual(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates an email optin', () => {
|
it('creates an email contact', () => {
|
||||||
expect(loginEmailOptIn).toEqual([
|
expect(user[0].emailContact).toEqual({
|
||||||
{
|
id: expect.any(Number),
|
||||||
id: expect.any(Number),
|
type: UserContactType.USER_CONTACT_EMAIL,
|
||||||
userId: user[0].id,
|
userId: user[0].id,
|
||||||
verificationCode: expect.any(String),
|
email: 'peter@lustig.de',
|
||||||
emailOptInTypeId: 1,
|
emailChecked: false,
|
||||||
createdAt: expect.any(Date),
|
emailVerificationCode: expect.any(String),
|
||||||
resendCount: 0,
|
emailOptInTypeId: OptInType.EMAIL_OPT_IN_REGISTER,
|
||||||
updatedAt: expect.any(Date),
|
emailResendCount: 0,
|
||||||
},
|
phone: null,
|
||||||
])
|
createdAt: expect.any(Date),
|
||||||
|
deletedAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -169,7 +175,7 @@ describe('UserResolver', () => {
|
|||||||
it('sends an account activation email', () => {
|
it('sends an account activation email', () => {
|
||||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||||
/{optin}/g,
|
/{optin}/g,
|
||||||
emailOptIn,
|
emailVerificationCode,
|
||||||
).replace(/{code}/g, '')
|
).replace(/{code}/g, '')
|
||||||
expect(sendAccountActivationEmail).toBeCalledWith({
|
expect(sendAccountActivationEmail).toBeCalledWith({
|
||||||
link: activationLink,
|
link: activationLink,
|
||||||
@ -227,13 +233,13 @@ describe('UserResolver', () => {
|
|||||||
mutation: createUser,
|
mutation: createUser,
|
||||||
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'it' },
|
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'it' },
|
||||||
})
|
})
|
||||||
await expect(User.find()).resolves.toEqual(
|
await expect(
|
||||||
expect.arrayContaining([
|
UserContact.findOne({ email: 'bibi@bloxberg.de' }, { relations: ['user'] }),
|
||||||
expect.objectContaining({
|
).resolves.toEqual(
|
||||||
email: 'bibi@bloxberg.de',
|
expect.objectContaining({
|
||||||
language: 'de',
|
email: 'bibi@bloxberg.de',
|
||||||
}),
|
user: expect.objectContaining({ language: 'de' }),
|
||||||
]),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -244,10 +250,12 @@ describe('UserResolver', () => {
|
|||||||
mutation: createUser,
|
mutation: createUser,
|
||||||
variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined },
|
variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined },
|
||||||
})
|
})
|
||||||
await expect(User.find()).resolves.toEqual(
|
await expect(User.find({ relations: ['emailContact'] })).resolves.toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
email: 'raeuber@hotzenplotz.de',
|
emailContact: expect.objectContaining({
|
||||||
|
email: 'raeuber@hotzenplotz.de',
|
||||||
|
}),
|
||||||
publisherId: null,
|
publisherId: null,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
@ -264,7 +272,7 @@ describe('UserResolver', () => {
|
|||||||
// activate account of admin Peter Lustig
|
// activate account of admin Peter Lustig
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: setPassword,
|
mutation: setPassword,
|
||||||
variables: { code: emailOptIn, password: 'Aa12345_' },
|
variables: { code: emailVerificationCode, password: 'Aa12345_' },
|
||||||
})
|
})
|
||||||
|
|
||||||
// make Peter Lustig Admin
|
// make Peter Lustig Admin
|
||||||
@ -298,9 +306,13 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('sets the contribution link id', async () => {
|
it('sets the contribution link id', async () => {
|
||||||
await expect(User.findOne({ email: 'ein@besucher.de' })).resolves.toEqual(
|
await expect(
|
||||||
|
UserContact.findOne({ email: 'ein@besucher.de' }, { relations: ['user'] }),
|
||||||
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
contributionLinkId: link.id,
|
user: expect.objectContaining({
|
||||||
|
contributionLinkId: link.id,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -389,8 +401,12 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('sets the referrer id to bob baumeister id', async () => {
|
it('sets the referrer id to bob baumeister id', async () => {
|
||||||
await expect(User.findOne({ email: 'which@ever.de' })).resolves.toEqual(
|
await expect(
|
||||||
expect.objectContaining({ referrerId: bob.data.login.id }),
|
UserContact.findOne({ email: 'which@ever.de' }, { relations: ['user'] }),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
user: expect.objectContaining({ referrerId: bob.data.login.id }),
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -413,7 +429,7 @@ describe('UserResolver', () => {
|
|||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
amount: 19.99,
|
amount: 19.99,
|
||||||
memo: `Kein Trick, keine Zauberrei,
|
memo: `Kein Trick, keine Zauberrei,
|
||||||
bei Gradidio sei dabei!`,
|
bei Gradidio sei dabei!`,
|
||||||
})
|
})
|
||||||
const transactionLink = await TransactionLink.findOneOrFail()
|
const transactionLink = await TransactionLink.findOneOrFail()
|
||||||
resetToken()
|
resetToken()
|
||||||
@ -444,20 +460,23 @@ bei Gradidio sei dabei!`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
let result: any
|
let result: any
|
||||||
let emailOptIn: string
|
let emailVerificationCode: string
|
||||||
|
|
||||||
describe('valid optin code and valid password', () => {
|
describe('valid optin code and valid password', () => {
|
||||||
let newUser: any
|
let newUser: User
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await mutate({ mutation: createUser, variables: createUserVariables })
|
await mutate({ mutation: createUser, variables: createUserVariables })
|
||||||
const loginEmailOptIn = await LoginEmailOptIn.find()
|
const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email })
|
||||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
emailVerificationCode = emailContact.emailVerificationCode.toString()
|
||||||
result = await mutate({
|
result = await mutate({
|
||||||
mutation: setPassword,
|
mutation: setPassword,
|
||||||
variables: { code: emailOptIn, password: 'Aa12345_' },
|
variables: { code: emailVerificationCode, password: 'Aa12345_' },
|
||||||
})
|
})
|
||||||
newUser = await User.find()
|
newUser = await User.findOneOrFail(
|
||||||
|
{ id: emailContact.userId },
|
||||||
|
{ relations: ['emailContact'] },
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -465,11 +484,11 @@ bei Gradidio sei dabei!`,
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('sets email checked to true', () => {
|
it('sets email checked to true', () => {
|
||||||
expect(newUser[0].emailChecked).toBeTruthy()
|
expect(newUser.emailContact.emailChecked).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates the password', () => {
|
it('updates the password', () => {
|
||||||
expect(newUser[0].password).toEqual('3917921995996627700')
|
expect(newUser.password).toEqual('3917921995996627700')
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -491,11 +510,11 @@ bei Gradidio sei dabei!`,
|
|||||||
describe('no valid password', () => {
|
describe('no valid password', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await mutate({ mutation: createUser, variables: createUserVariables })
|
await mutate({ mutation: createUser, variables: createUserVariables })
|
||||||
const loginEmailOptIn = await LoginEmailOptIn.find()
|
const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email })
|
||||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
emailVerificationCode = emailContact.emailVerificationCode.toString()
|
||||||
result = await mutate({
|
result = await mutate({
|
||||||
mutation: setPassword,
|
mutation: setPassword,
|
||||||
variables: { code: emailOptIn, password: 'not-valid' },
|
variables: { code: emailVerificationCode, password: 'not-valid' },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -562,6 +581,7 @@ bei Gradidio sei dabei!`,
|
|||||||
|
|
||||||
describe('no users in database', () => {
|
describe('no users in database', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
result = await query({ query: login, variables })
|
result = await query({ query: login, variables })
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -574,7 +594,9 @@ bei Gradidio sei dabei!`,
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error found', () => {
|
it('logs the error found', () => {
|
||||||
expect(logger.error).toBeCalledWith('User with email=bibi@bloxberg.de does not exist')
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'UserContact with email=bibi@bloxberg.de does not exists',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -759,46 +781,68 @@ bei Gradidio sei dabei!`,
|
|||||||
|
|
||||||
describe('forgotPassword', () => {
|
describe('forgotPassword', () => {
|
||||||
const variables = { email: 'bibi@bloxberg.de' }
|
const variables = { email: 'bibi@bloxberg.de' }
|
||||||
|
const emailCodeRequestTime = CONFIG.EMAIL_CODE_REQUEST_TIME
|
||||||
|
|
||||||
describe('user is not in DB', () => {
|
describe('user is not in DB', () => {
|
||||||
it('returns true', async () => {
|
describe('duration not expired', () => {
|
||||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
it('returns true', async () => {
|
||||||
expect.objectContaining({
|
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||||
data: {
|
expect.objectContaining({
|
||||||
forgotPassword: true,
|
data: {
|
||||||
},
|
forgotPassword: true,
|
||||||
}),
|
},
|
||||||
)
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('user exists in DB', () => {
|
describe('user exists in DB', () => {
|
||||||
let result: any
|
let emailContact: UserContact
|
||||||
let loginEmailOptIn: LoginEmailOptIn[]
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await userFactory(testEnv, bibiBloxberg)
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
await resetEntity(LoginEmailOptIn)
|
// await resetEntity(LoginEmailOptIn)
|
||||||
result = await mutate({ mutation: forgotPassword, variables })
|
emailContact = await UserContact.findOneOrFail(variables)
|
||||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await cleanDB()
|
await cleanDB()
|
||||||
|
CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns true', async () => {
|
describe('duration not expired', () => {
|
||||||
await expect(result).toEqual(
|
it('returns true', async () => {
|
||||||
expect.objectContaining({
|
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||||
data: {
|
expect.objectContaining({
|
||||||
forgotPassword: true,
|
errors: [
|
||||||
},
|
new GraphQLError(
|
||||||
}),
|
`email already sent less than ${printTimeDuration(
|
||||||
)
|
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||||
|
)} minutes ago`,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('duration reset to 0', () => {
|
||||||
|
it('returns true', async () => {
|
||||||
|
CONFIG.EMAIL_CODE_REQUEST_TIME = 0
|
||||||
|
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
forgotPassword: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sends reset password email', () => {
|
it('sends reset password email', () => {
|
||||||
expect(sendResetPasswordEmail).toBeCalledWith({
|
expect(sendResetPasswordEmail).toBeCalledWith({
|
||||||
link: activationLink(loginEmailOptIn[0]),
|
link: activationLink(emailContact.emailVerificationCode),
|
||||||
firstName: 'Bibi',
|
firstName: 'Bibi',
|
||||||
lastName: 'Bloxberg',
|
lastName: 'Bloxberg',
|
||||||
email: 'bibi@bloxberg.de',
|
email: 'bibi@bloxberg.de',
|
||||||
@ -807,7 +851,8 @@ bei Gradidio sei dabei!`,
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('request reset password again', () => {
|
describe('request reset password again', () => {
|
||||||
it('throws an error', async () => {
|
it('thows an error', async () => {
|
||||||
|
CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime
|
||||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
|
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
|
||||||
@ -823,11 +868,11 @@ bei Gradidio sei dabei!`,
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('queryOptIn', () => {
|
describe('queryOptIn', () => {
|
||||||
let loginEmailOptIn: LoginEmailOptIn[]
|
let emailContact: UserContact
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await userFactory(testEnv, bibiBloxberg)
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
emailContact = await UserContact.findOneOrFail({ email: bibiBloxberg.email })
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -842,8 +887,8 @@ bei Gradidio sei dabei!`,
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
// keep Whitspace in error message!
|
// keep Whitspace in error message!
|
||||||
new GraphQLError(`Could not find any entity of type "LoginEmailOptIn" matching: {
|
new GraphQLError(`Could not find any entity of type "UserContact" matching: {
|
||||||
"verificationCode": "not-valid"
|
"emailVerificationCode": "not-valid"
|
||||||
}`),
|
}`),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@ -856,7 +901,7 @@ bei Gradidio sei dabei!`,
|
|||||||
await expect(
|
await expect(
|
||||||
query({
|
query({
|
||||||
query: queryOptIn,
|
query: queryOptIn,
|
||||||
variables: { optIn: loginEmailOptIn[0].verificationCode.toString() },
|
variables: { optIn: emailContact.emailVerificationCode.toString() },
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { backendLogger as logger } from '@/server/logger'
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
|
||||||
import { Context, getUser } from '@/server/context'
|
import { Context, getUser } from '@/server/context'
|
||||||
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
||||||
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
|
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { User } from '@model/User'
|
import { User } from '@model/User'
|
||||||
import { User as DbUser } from '@entity/User'
|
import { User as DbUser } from '@entity/User'
|
||||||
|
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||||
import { communityDbUser } from '@/util/communityUser'
|
import { communityDbUser } from '@/util/communityUser'
|
||||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
||||||
@ -16,7 +16,6 @@ import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
|
|||||||
import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs'
|
import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs'
|
||||||
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
|
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
|
||||||
import { OptInType } from '@enum/OptInType'
|
import { OptInType } from '@enum/OptInType'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
|
||||||
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
||||||
@ -29,10 +28,12 @@ import {
|
|||||||
EventLogin,
|
EventLogin,
|
||||||
EventRedeemRegister,
|
EventRedeemRegister,
|
||||||
EventRegister,
|
EventRegister,
|
||||||
|
EventSendAccountMultiRegistrationEmail,
|
||||||
EventSendConfirmationEmail,
|
EventSendConfirmationEmail,
|
||||||
EventActivateAccount,
|
EventActivateAccount,
|
||||||
} from '@/event/Event'
|
} from '@/event/Event'
|
||||||
import { getUserCreation } from './util/creations'
|
import { getUserCreation } from './util/creations'
|
||||||
|
import { UserContactType } from '../enum/UserContactType'
|
||||||
import { UserRepository } from '@/typeorm/repository/User'
|
import { UserRepository } from '@/typeorm/repository/User'
|
||||||
import { SearchAdminUsersResult } from '@model/AdminUser'
|
import { SearchAdminUsersResult } from '@model/AdminUser'
|
||||||
import Paginated from '@arg/Paginated'
|
import Paginated from '@arg/Paginated'
|
||||||
@ -147,6 +148,7 @@ const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[
|
|||||||
return [encryptionKeyHash, encryptionKey]
|
return [encryptionKeyHash, encryptionKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
const getEmailHash = (email: string): Buffer => {
|
const getEmailHash = (email: string): Buffer => {
|
||||||
logger.trace('getEmailHash...')
|
logger.trace('getEmailHash...')
|
||||||
const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
|
const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
|
||||||
@ -154,6 +156,7 @@ const getEmailHash = (email: string): Buffer => {
|
|||||||
logger.debug(`getEmailHash...successful: ${emailHash}`)
|
logger.debug(`getEmailHash...successful: ${emailHash}`)
|
||||||
return emailHash
|
return emailHash
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
|
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
|
||||||
logger.trace('SecretKeyCryptographyEncrypt...')
|
logger.trace('SecretKeyCryptographyEncrypt...')
|
||||||
@ -178,6 +181,19 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B
|
|||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newEmailContact = (email: string, userId: number): DbUserContact => {
|
||||||
|
logger.trace(`newEmailContact...`)
|
||||||
|
const emailContact = new DbUserContact()
|
||||||
|
emailContact.email = email
|
||||||
|
emailContact.userId = userId
|
||||||
|
emailContact.type = UserContactType.USER_CONTACT_EMAIL
|
||||||
|
emailContact.emailChecked = false
|
||||||
|
emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
|
||||||
|
emailContact.emailVerificationCode = random(64)
|
||||||
|
logger.debug(`newEmailContact...successful: ${emailContact}`)
|
||||||
|
return emailContact
|
||||||
|
}
|
||||||
|
/*
|
||||||
const newEmailOptIn = (userId: number): LoginEmailOptIn => {
|
const newEmailOptIn = (userId: number): LoginEmailOptIn => {
|
||||||
logger.trace('newEmailOptIn...')
|
logger.trace('newEmailOptIn...')
|
||||||
const emailOptIn = new LoginEmailOptIn()
|
const emailOptIn = new LoginEmailOptIn()
|
||||||
@ -187,7 +203,8 @@ const newEmailOptIn = (userId: number): LoginEmailOptIn => {
|
|||||||
logger.debug(`newEmailOptIn...successful: ${emailOptIn}`)
|
logger.debug(`newEmailOptIn...successful: ${emailOptIn}`)
|
||||||
return emailOptIn
|
return emailOptIn
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
/*
|
||||||
// needed by AdminResolver
|
// needed by AdminResolver
|
||||||
// checks if given code exists and can be resent
|
// checks if given code exists and can be resent
|
||||||
// if optIn does not exits, it is created
|
// if optIn does not exits, it is created
|
||||||
@ -227,10 +244,44 @@ export const checkOptInCode = async (
|
|||||||
logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${user.id}`)
|
logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${user.id}`)
|
||||||
return optInCode
|
return optInCode
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
export const checkEmailVerificationCode = async (
|
||||||
|
emailContact: DbUserContact,
|
||||||
|
optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER,
|
||||||
|
): Promise<DbUserContact> => {
|
||||||
|
logger.info(`checkEmailVerificationCode... ${emailContact}`)
|
||||||
|
if (emailContact.updatedAt) {
|
||||||
|
if (!canEmailResend(emailContact.updatedAt)) {
|
||||||
|
logger.error(
|
||||||
|
`email already sent less than ${printTimeDuration(
|
||||||
|
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||||
|
)} minutes ago`,
|
||||||
|
)
|
||||||
|
throw new Error(
|
||||||
|
`email already sent less than ${printTimeDuration(
|
||||||
|
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||||
|
)} minutes ago`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
emailContact.updatedAt = new Date()
|
||||||
|
emailContact.emailResendCount++
|
||||||
|
} else {
|
||||||
|
logger.trace('create new EmailVerificationCode for userId=' + emailContact.userId)
|
||||||
|
emailContact.emailChecked = false
|
||||||
|
emailContact.emailVerificationCode = random(64)
|
||||||
|
}
|
||||||
|
emailContact.emailOptInTypeId = optInType
|
||||||
|
await DbUserContact.save(emailContact).catch(() => {
|
||||||
|
logger.error('Unable to save email verification code= ' + emailContact)
|
||||||
|
throw new Error('Unable to save email verification code.')
|
||||||
|
})
|
||||||
|
logger.debug(`checkEmailVerificationCode...successful: ${emailContact}`)
|
||||||
|
return emailContact
|
||||||
|
}
|
||||||
|
|
||||||
export const activationLink = (optInCode: LoginEmailOptIn): string => {
|
export const activationLink = (verificationCode: BigInt): string => {
|
||||||
logger.debug(`activationLink(${LoginEmailOptIn})...`)
|
logger.debug(`activationLink(${verificationCode})...`)
|
||||||
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString())
|
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, verificationCode.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
const newGradidoID = async (): Promise<string> => {
|
const newGradidoID = async (): Promise<string> => {
|
||||||
@ -273,15 +324,12 @@ export class UserResolver {
|
|||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
logger.info(`login with ${email}, ***, ${publisherId} ...`)
|
logger.info(`login with ${email}, ***, ${publisherId} ...`)
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => {
|
const dbUser = await findUserByEmail(email)
|
||||||
logger.error(`User with email=${email} does not exist`)
|
|
||||||
throw new Error('No user with this credentials')
|
|
||||||
})
|
|
||||||
if (dbUser.deletedAt) {
|
if (dbUser.deletedAt) {
|
||||||
logger.error('The User was permanently deleted in database.')
|
logger.error('The User was permanently deleted in database.')
|
||||||
throw new Error('This user was permanently deleted. Contact support for questions.')
|
throw new Error('This user was permanently deleted. Contact support for questions.')
|
||||||
}
|
}
|
||||||
if (!dbUser.emailChecked) {
|
if (!dbUser.emailContact.emailChecked) {
|
||||||
logger.error('The Users email is not validate yet.')
|
logger.error('The Users email is not validate yet.')
|
||||||
throw new Error('User email not validated')
|
throw new Error('User email not validated')
|
||||||
}
|
}
|
||||||
@ -306,7 +354,7 @@ export class UserResolver {
|
|||||||
logger.debug('login credentials valid...')
|
logger.debug('login credentials valid...')
|
||||||
|
|
||||||
const user = new User(dbUser, await getUserCreation(dbUser.id))
|
const user = new User(dbUser, await getUserCreation(dbUser.id))
|
||||||
logger.debug('user=' + user)
|
logger.debug(`user= ${JSON.stringify(user, null, 2)}`)
|
||||||
|
|
||||||
// Elopage Status & Stored PublisherId
|
// Elopage Status & Stored PublisherId
|
||||||
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
|
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
|
||||||
@ -324,7 +372,7 @@ export class UserResolver {
|
|||||||
const ev = new EventLogin()
|
const ev = new EventLogin()
|
||||||
ev.userId = user.id
|
ev.userId = user.id
|
||||||
eventProtocol.writeEvent(new Event().setEventLogin(ev))
|
eventProtocol.writeEvent(new Event().setEventLogin(ev))
|
||||||
logger.info('successful Login:' + user)
|
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,66 +401,72 @@ export class UserResolver {
|
|||||||
)
|
)
|
||||||
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
|
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
|
||||||
// default int publisher_id = 0;
|
// default int publisher_id = 0;
|
||||||
|
const event = new Event()
|
||||||
|
|
||||||
// Validate Language (no throw)
|
// Validate Language (no throw)
|
||||||
if (!language || !isLanguage(language)) {
|
if (!language || !isLanguage(language)) {
|
||||||
language = DEFAULT_LANGUAGE
|
language = DEFAULT_LANGUAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate email unique
|
// check if user with email still exists?
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
// TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes
|
if (await checkEmailExists(email)) {
|
||||||
const userFound = await DbUser.findOne({ email }, { withDeleted: true })
|
const foundUser = await findUserByEmail(email)
|
||||||
logger.info(`DbUser.findOne(email=${email}) = ${userFound}`)
|
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`)
|
||||||
|
|
||||||
if (userFound) {
|
if (foundUser) {
|
||||||
// ATTENTION: this logger-message will be exactly expected during tests
|
// ATTENTION: this logger-message will be exactly expected during tests
|
||||||
logger.info(`User already exists with this email=${email}`)
|
logger.info(`User already exists with this email=${email}`)
|
||||||
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
||||||
|
|
||||||
const user = new User(communityDbUser)
|
const user = new User(communityDbUser)
|
||||||
user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in?
|
user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in?
|
||||||
user.gradidoID = uuidv4()
|
user.gradidoID = uuidv4()
|
||||||
user.email = email
|
user.email = email
|
||||||
user.firstName = firstName
|
user.firstName = firstName
|
||||||
user.lastName = lastName
|
user.lastName = lastName
|
||||||
user.language = language
|
user.language = language
|
||||||
user.publisherId = publisherId
|
user.publisherId = publisherId
|
||||||
logger.debug('partly faked user=' + user)
|
logger.debug('partly faked user=' + user)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const emailSent = await sendAccountMultiRegistrationEmail({
|
const emailSent = await sendAccountMultiRegistrationEmail({
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
email,
|
email,
|
||||||
})
|
})
|
||||||
logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`)
|
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
|
||||||
/* uncomment this, when you need the activation link on the console */
|
eventSendAccountMultiRegistrationEmail.userId = foundUser.id
|
||||||
// In case EMails are disabled log the activation link for the user
|
eventProtocol.writeEvent(
|
||||||
if (!emailSent) {
|
event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail),
|
||||||
logger.debug(`Email not sent!`)
|
)
|
||||||
|
logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`)
|
||||||
|
/* uncomment this, when you need the activation link on the console */
|
||||||
|
// In case EMails are disabled log the activation link for the user
|
||||||
|
if (!emailSent) {
|
||||||
|
logger.debug(`Email not send!`)
|
||||||
|
}
|
||||||
|
logger.info('createUser() faked and send multi registration mail...')
|
||||||
|
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
logger.info('createUser() faked and send multi registration mail...')
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const passphrase = PassphraseGenerate()
|
const passphrase = PassphraseGenerate()
|
||||||
// const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
// const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
||||||
// const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
|
// const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
|
||||||
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
||||||
const emailHash = getEmailHash(email)
|
// const emailHash = getEmailHash(email)
|
||||||
const gradidoID = await newGradidoID()
|
const gradidoID = await newGradidoID()
|
||||||
|
|
||||||
const eventRegister = new EventRegister()
|
const eventRegister = new EventRegister()
|
||||||
const eventRedeemRegister = new EventRedeemRegister()
|
const eventRedeemRegister = new EventRedeemRegister()
|
||||||
const eventSendConfirmEmail = new EventSendConfirmationEmail()
|
const eventSendConfirmEmail = new EventSendConfirmationEmail()
|
||||||
const dbUser = new DbUser()
|
|
||||||
|
let dbUser = new DbUser()
|
||||||
dbUser.gradidoID = gradidoID
|
dbUser.gradidoID = gradidoID
|
||||||
dbUser.email = email
|
|
||||||
dbUser.firstName = firstName
|
dbUser.firstName = firstName
|
||||||
dbUser.lastName = lastName
|
dbUser.lastName = lastName
|
||||||
dbUser.emailHash = emailHash
|
|
||||||
dbUser.language = language
|
dbUser.language = language
|
||||||
dbUser.publisherId = publisherId
|
dbUser.publisherId = publisherId
|
||||||
dbUser.passphrase = passphrase.join(' ')
|
dbUser.passphrase = passphrase.join(' ')
|
||||||
@ -443,25 +497,38 @@ export class UserResolver {
|
|||||||
// loginUser.pubKey = keyPair[0]
|
// loginUser.pubKey = keyPair[0]
|
||||||
// loginUser.privKey = encryptedPrivkey
|
// loginUser.privKey = encryptedPrivkey
|
||||||
|
|
||||||
const event = new Event()
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
try {
|
try {
|
||||||
await queryRunner.manager.save(dbUser).catch((error) => {
|
dbUser = await queryRunner.manager.save(dbUser).catch((error) => {
|
||||||
logger.error('Error while saving dbUser', error)
|
logger.error('Error while saving dbUser', error)
|
||||||
throw new Error('error saving user')
|
throw new Error('error saving user')
|
||||||
})
|
})
|
||||||
|
let emailContact = newEmailContact(email, dbUser.id)
|
||||||
|
emailContact = await queryRunner.manager.save(emailContact).catch((error) => {
|
||||||
|
logger.error('Error while saving emailContact', error)
|
||||||
|
throw new Error('error saving email user contact')
|
||||||
|
})
|
||||||
|
|
||||||
|
dbUser.emailContact = emailContact
|
||||||
|
dbUser.emailId = emailContact.id
|
||||||
|
await queryRunner.manager.save(dbUser).catch((error) => {
|
||||||
|
logger.error('Error while updating dbUser', error)
|
||||||
|
throw new Error('error updating user')
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
const emailOptIn = newEmailOptIn(dbUser.id)
|
const emailOptIn = newEmailOptIn(dbUser.id)
|
||||||
await queryRunner.manager.save(emailOptIn).catch((error) => {
|
await queryRunner.manager.save(emailOptIn).catch((error) => {
|
||||||
logger.error('Error while saving emailOptIn', error)
|
logger.error('Error while saving emailOptIn', error)
|
||||||
throw new Error('error saving email opt in')
|
throw new Error('error saving email opt in')
|
||||||
})
|
})
|
||||||
|
*/
|
||||||
|
|
||||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||||
/{optin}/g,
|
/{optin}/g,
|
||||||
emailOptIn.verificationCode.toString(),
|
emailContact.emailVerificationCode.toString(),
|
||||||
).replace(/{code}/g, redeemCode ? '/' + redeemCode : '')
|
).replace(/{code}/g, redeemCode ? '/' + redeemCode : '')
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
@ -476,8 +543,6 @@ export class UserResolver {
|
|||||||
eventSendConfirmEmail.userId = dbUser.id
|
eventSendConfirmEmail.userId = dbUser.id
|
||||||
eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail))
|
eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail))
|
||||||
|
|
||||||
/* uncomment this, when you need the activation link on the console */
|
|
||||||
// In case EMails are disabled log the activation link for the user
|
|
||||||
if (!emailSent) {
|
if (!emailSent) {
|
||||||
logger.debug(`Account confirmation link: ${activationLink}`)
|
logger.debug(`Account confirmation link: ${activationLink}`)
|
||||||
}
|
}
|
||||||
@ -508,22 +573,29 @@ export class UserResolver {
|
|||||||
async forgotPassword(@Arg('email') email: string): Promise<boolean> {
|
async forgotPassword(@Arg('email') email: string): Promise<boolean> {
|
||||||
logger.info(`forgotPassword(${email})...`)
|
logger.info(`forgotPassword(${email})...`)
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
const user = await DbUser.findOne({ email })
|
const user = await findUserByEmail(email).catch(() => {
|
||||||
|
logger.warn(`fail on find UserContact per ${email}`)
|
||||||
|
})
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.warn(`no user found with ${email}`)
|
logger.warn(`no user found with ${email}`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// can be both types: REGISTER and RESET_PASSWORD
|
// can be both types: REGISTER and RESET_PASSWORD
|
||||||
let optInCode = await LoginEmailOptIn.findOne({
|
// let optInCode = await LoginEmailOptIn.findOne({
|
||||||
userId: user.id,
|
// userId: user.id,
|
||||||
})
|
// })
|
||||||
|
// let optInCode = user.emailContact.emailVerificationCode
|
||||||
|
const dbUserContact = await checkEmailVerificationCode(
|
||||||
|
user.emailContact,
|
||||||
|
OptInType.EMAIL_OPT_IN_RESET_PASSWORD,
|
||||||
|
)
|
||||||
|
|
||||||
optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
|
// optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
|
||||||
logger.info(`optInCode for ${email}=${optInCode}`)
|
logger.info(`optInCode for ${email}=${dbUserContact}`)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const emailSent = await sendResetPasswordEmailMailer({
|
const emailSent = await sendResetPasswordEmailMailer({
|
||||||
link: activationLink(optInCode),
|
link: activationLink(dbUserContact.emailVerificationCode),
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
email,
|
email,
|
||||||
@ -533,7 +605,7 @@ export class UserResolver {
|
|||||||
/* uncomment this, when you need the activation link on the console */
|
/* uncomment this, when you need the activation link on the console */
|
||||||
// In case EMails are disabled log the activation link for the user
|
// In case EMails are disabled log the activation link for the user
|
||||||
if (!emailSent) {
|
if (!emailSent) {
|
||||||
logger.debug(`Reset password link: ${activationLink(optInCode)}`)
|
logger.debug(`Reset password link: ${activationLink(dbUserContact.emailVerificationCode)}`)
|
||||||
}
|
}
|
||||||
logger.info(`forgotPassword(${email}) successful...`)
|
logger.info(`forgotPassword(${email}) successful...`)
|
||||||
|
|
||||||
@ -556,13 +628,22 @@ export class UserResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load code
|
// Load code
|
||||||
|
/*
|
||||||
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
|
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
|
||||||
logger.error('Could not login with emailVerificationCode')
|
logger.error('Could not login with emailVerificationCode')
|
||||||
throw new Error('Could not login with emailVerificationCode')
|
throw new Error('Could not login with emailVerificationCode')
|
||||||
})
|
})
|
||||||
logger.debug('optInCode loaded...')
|
*/
|
||||||
|
const userContact = await DbUserContact.findOneOrFail(
|
||||||
|
{ emailVerificationCode: code },
|
||||||
|
{ relations: ['user'] },
|
||||||
|
).catch(() => {
|
||||||
|
logger.error('Could not login with emailVerificationCode')
|
||||||
|
throw new Error('Could not login with emailVerificationCode')
|
||||||
|
})
|
||||||
|
logger.debug('userContact loaded...')
|
||||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||||
if (!isOptInValid(optInCode)) {
|
if (!isEmailVerificationCodeValid(userContact.updatedAt)) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||||
)
|
)
|
||||||
@ -570,14 +651,11 @@ export class UserResolver {
|
|||||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
logger.debug('optInCode is valid...')
|
logger.debug('EmailVerificationCode is valid...')
|
||||||
|
|
||||||
// load user
|
// load user
|
||||||
const user = await DbUser.findOneOrFail({ id: optInCode.userId }).catch(() => {
|
const user = userContact.user
|
||||||
logger.error('Could not find corresponding Login User')
|
logger.debug('user with EmailVerificationCode found...')
|
||||||
throw new Error('Could not find corresponding Login User')
|
|
||||||
})
|
|
||||||
logger.debug('user with optInCode found...')
|
|
||||||
|
|
||||||
// Generate Passphrase if needed
|
// Generate Passphrase if needed
|
||||||
if (!user.passphrase) {
|
if (!user.passphrase) {
|
||||||
@ -597,10 +675,10 @@ export class UserResolver {
|
|||||||
logger.debug('Passphrase is valid...')
|
logger.debug('Passphrase is valid...')
|
||||||
|
|
||||||
// Activate EMail
|
// Activate EMail
|
||||||
user.emailChecked = true
|
userContact.emailChecked = true
|
||||||
|
|
||||||
// Update Password
|
// Update Password
|
||||||
const passwordHash = SecretKeyCryptographyCreateKey(user.email, password) // return short and long hash
|
const passwordHash = SecretKeyCryptographyCreateKey(userContact.email, password) // return short and long hash
|
||||||
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
||||||
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
||||||
user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
|
user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
|
||||||
@ -610,7 +688,7 @@ export class UserResolver {
|
|||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
|
|
||||||
const event = new Event()
|
const event = new Event()
|
||||||
|
|
||||||
@ -620,17 +698,21 @@ export class UserResolver {
|
|||||||
logger.error('error saving user: ' + error)
|
logger.error('error saving user: ' + error)
|
||||||
throw new Error('error saving user: ' + error)
|
throw new Error('error saving user: ' + error)
|
||||||
})
|
})
|
||||||
|
// Save userContact
|
||||||
|
await queryRunner.manager.save(userContact).catch((error) => {
|
||||||
|
logger.error('error saving userContact: ' + error)
|
||||||
|
throw new Error('error saving userContact: ' + error)
|
||||||
|
})
|
||||||
|
|
||||||
await queryRunner.commitTransaction()
|
await queryRunner.commitTransaction()
|
||||||
|
logger.info('User and UserContact data written successfully...')
|
||||||
|
|
||||||
const eventActivateAccount = new EventActivateAccount()
|
const eventActivateAccount = new EventActivateAccount()
|
||||||
eventActivateAccount.userId = user.id
|
eventActivateAccount.userId = user.id
|
||||||
eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount))
|
eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount))
|
||||||
|
|
||||||
logger.info('User data written successfully...')
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
logger.error('Error on writing User data:' + e)
|
logger.error('Error on writing User and UserContact data:' + e)
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
@ -638,11 +720,11 @@ export class UserResolver {
|
|||||||
|
|
||||||
// Sign into Klicktipp
|
// Sign into Klicktipp
|
||||||
// TODO do we always signUp the user? How to handle things with old users?
|
// TODO do we always signUp the user? How to handle things with old users?
|
||||||
if (optInCode.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
|
if (userContact.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
|
||||||
try {
|
try {
|
||||||
await klicktippSignIn(user.email, user.language, user.firstName, user.lastName)
|
await klicktippSignIn(userContact.email, user.language, user.firstName, user.lastName)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`klicktippSignIn(${user.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
|
`klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error subscribe to klicktipp:' + e)
|
logger.error('Error subscribe to klicktipp:' + e)
|
||||||
@ -661,10 +743,10 @@ export class UserResolver {
|
|||||||
@Query(() => Boolean)
|
@Query(() => Boolean)
|
||||||
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
|
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
|
||||||
logger.info(`queryOptIn(${optIn})...`)
|
logger.info(`queryOptIn(${optIn})...`)
|
||||||
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn })
|
const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn })
|
||||||
logger.debug(`found optInCode=${optInCode}`)
|
logger.debug(`found optInCode=${userContact}`)
|
||||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||||
if (!isOptInValid(optInCode)) {
|
if (!isEmailVerificationCodeValid(userContact.updatedAt)) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||||
)
|
)
|
||||||
@ -712,7 +794,10 @@ export class UserResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This had some error cases defined - like missing private key. This is no longer checked.
|
// TODO: This had some error cases defined - like missing private key. This is no longer checked.
|
||||||
const oldPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, password)
|
const oldPasswordHash = SecretKeyCryptographyCreateKey(
|
||||||
|
userEntity.emailContact.email,
|
||||||
|
password,
|
||||||
|
)
|
||||||
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
|
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
|
||||||
logger.error(`Old password is invalid`)
|
logger.error(`Old password is invalid`)
|
||||||
throw new Error(`Old password is invalid`)
|
throw new Error(`Old password is invalid`)
|
||||||
@ -720,7 +805,10 @@ export class UserResolver {
|
|||||||
|
|
||||||
const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
|
const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
|
||||||
logger.debug('oldPassword decrypted...')
|
logger.debug('oldPassword decrypted...')
|
||||||
const newPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, passwordNew) // return short and long hash
|
const newPasswordHash = SecretKeyCryptographyCreateKey(
|
||||||
|
userEntity.emailContact.email,
|
||||||
|
passwordNew,
|
||||||
|
) // return short and long hash
|
||||||
logger.debug('newPasswordHash created...')
|
logger.debug('newPasswordHash created...')
|
||||||
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
|
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
|
||||||
logger.debug('PrivateKey encrypted...')
|
logger.debug('PrivateKey encrypted...')
|
||||||
@ -732,7 +820,7 @@ export class UserResolver {
|
|||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await queryRunner.manager.save(userEntity).catch((error) => {
|
await queryRunner.manager.save(userEntity).catch((error) => {
|
||||||
@ -757,12 +845,8 @@ export class UserResolver {
|
|||||||
@Query(() => Boolean)
|
@Query(() => Boolean)
|
||||||
async hasElopage(@Ctx() context: Context): Promise<boolean> {
|
async hasElopage(@Ctx() context: Context): Promise<boolean> {
|
||||||
logger.info(`hasElopage()...`)
|
logger.info(`hasElopage()...`)
|
||||||
const userEntity = context.user
|
const userEntity = getUser(context)
|
||||||
if (!userEntity) {
|
const elopageBuys = hasElopageBuys(userEntity.emailContact.email)
|
||||||
logger.info('missing context.user for EloPage-check')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const elopageBuys = hasElopageBuys(userEntity.email)
|
|
||||||
logger.debug(`has ElopageBuys = ${elopageBuys}`)
|
logger.debug(`has ElopageBuys = ${elopageBuys}`)
|
||||||
return elopageBuys
|
return elopageBuys
|
||||||
}
|
}
|
||||||
@ -798,19 +882,58 @@ export class UserResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function findUserByEmail(email: string): Promise<DbUser> {
|
||||||
|
const dbUserContact = await DbUserContact.findOneOrFail(
|
||||||
|
{ email: email },
|
||||||
|
{ withDeleted: true, relations: ['user'] },
|
||||||
|
).catch(() => {
|
||||||
|
logger.error(`UserContact with email=${email} does not exists`)
|
||||||
|
throw new Error('No user with this credentials')
|
||||||
|
})
|
||||||
|
const dbUser = dbUserContact.user
|
||||||
|
dbUser.emailContact = dbUserContact
|
||||||
|
return dbUser
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkEmailExists(email: string): Promise<boolean> {
|
||||||
|
const userContact = await DbUserContact.findOne({ email: email }, { withDeleted: true })
|
||||||
|
if (userContact) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => {
|
const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => {
|
||||||
const timeElapsed = Date.now() - new Date(optIn.updatedAt).getTime()
|
const timeElapsed = Date.now() - new Date(optIn.updatedAt).getTime()
|
||||||
// time is given in minutes
|
// time is given in minutes
|
||||||
return timeElapsed <= duration * 60 * 1000
|
return timeElapsed <= duration * 60 * 1000
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
const isTimeExpired = (updatedAt: Date, duration: number): boolean => {
|
||||||
|
const timeElapsed = Date.now() - new Date(updatedAt).getTime()
|
||||||
|
// time is given in minutes
|
||||||
|
return timeElapsed <= duration * 60 * 1000
|
||||||
|
}
|
||||||
|
/*
|
||||||
const isOptInValid = (optIn: LoginEmailOptIn): boolean => {
|
const isOptInValid = (optIn: LoginEmailOptIn): boolean => {
|
||||||
return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME)
|
return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
const isEmailVerificationCodeValid = (updatedAt: Date | null): boolean => {
|
||||||
|
if (updatedAt == null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_VALID_TIME)
|
||||||
|
}
|
||||||
|
/*
|
||||||
const canResendOptIn = (optIn: LoginEmailOptIn): boolean => {
|
const canResendOptIn = (optIn: LoginEmailOptIn): boolean => {
|
||||||
return !isTimeExpired(optIn, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
return !isTimeExpired(optIn, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
const canEmailResend = (updatedAt: Date): boolean => {
|
||||||
|
return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
||||||
|
}
|
||||||
|
|
||||||
const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => {
|
const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => {
|
||||||
if (time > 60) {
|
if (time > 60) {
|
||||||
|
|||||||
@ -15,14 +15,21 @@ export const validateContribution = (
|
|||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
creationDate: Date,
|
creationDate: Date,
|
||||||
): void => {
|
): void => {
|
||||||
logger.trace('isContributionValid', creations, amount, creationDate)
|
logger.trace('isContributionValid: ', creations, amount, creationDate)
|
||||||
const index = getCreationIndex(creationDate.getMonth())
|
const index = getCreationIndex(creationDate.getMonth())
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
|
logger.error(
|
||||||
|
'No information for available creations with the given creationDate=',
|
||||||
|
creationDate,
|
||||||
|
)
|
||||||
throw new Error('No information for available creations for the given date')
|
throw new Error('No information for available creations for the given date')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (amount.greaterThan(creations[index].toString())) {
|
if (amount.greaterThan(creations[index].toString())) {
|
||||||
|
logger.error(
|
||||||
|
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
||||||
|
)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
||||||
)
|
)
|
||||||
@ -41,7 +48,7 @@ export const getUserCreations = async (
|
|||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
|
|
||||||
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
||||||
logger.trace('getUserCreations dateFilter', dateFilter)
|
logger.trace('getUserCreations dateFilter=', dateFilter)
|
||||||
|
|
||||||
const unionString = includePending
|
const unionString = includePending
|
||||||
? `
|
? `
|
||||||
@ -51,6 +58,7 @@ export const getUserCreations = async (
|
|||||||
AND contribution_date >= ${dateFilter}
|
AND contribution_date >= ${dateFilter}
|
||||||
AND confirmed_at IS NULL AND deleted_at IS NULL`
|
AND confirmed_at IS NULL AND deleted_at IS NULL`
|
||||||
: ''
|
: ''
|
||||||
|
logger.trace('getUserCreations unionString=', unionString)
|
||||||
|
|
||||||
const unionQuery = await queryRunner.manager.query(`
|
const unionQuery = await queryRunner.manager.query(`
|
||||||
SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM
|
SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM
|
||||||
@ -62,6 +70,7 @@ export const getUserCreations = async (
|
|||||||
GROUP BY month, userId
|
GROUP BY month, userId
|
||||||
ORDER BY date DESC
|
ORDER BY date DESC
|
||||||
`)
|
`)
|
||||||
|
logger.trace('getUserCreations unionQuery=', unionQuery)
|
||||||
|
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
|
|
||||||
@ -82,6 +91,7 @@ export const getUserCreations = async (
|
|||||||
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
||||||
logger.trace('getUserCreation', id, includePending)
|
logger.trace('getUserCreation', id, includePending)
|
||||||
const creations = await getUserCreations([id], includePending)
|
const creations = await getUserCreations([id], includePending)
|
||||||
|
logger.trace('getUserCreation creations=', creations)
|
||||||
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;')
|
||||||
|
}
|
||||||
@ -59,3 +59,4 @@ networks:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db_test_vol:
|
db_test_vol:
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
To introduce the Gradido-ID base on the requirement to identify an user account per technical key instead of using an email-address. Such a technical key ensures an exact identification of an user account without giving detailed information for possible missusage.
|
The introduction of the Gradido-ID base on the requirement to identify an user account per technical key instead of using an email-address. Such a technical key ensures an exact identification of an user account without giving detailed information for possible missusage.
|
||||||
|
|
||||||
Additionally the Gradido-ID allows to administrade any user account data like changing the email address or define several email addresses without any side effects on the identification of the user account.
|
Additionally the Gradido-ID allows to administrade any user account data like changing the email address or define several email addresses without any side effects on the identification of the user account.
|
||||||
|
|
||||||
@ -22,12 +22,12 @@ The second step is to decribe all concerning business logic processes, which hav
|
|||||||
|
|
||||||
The entity users has to be changed by adding the following columns.
|
The entity users has to be changed by adding the following columns.
|
||||||
|
|
||||||
| Column | Type | Description |
|
| Column | Type | Description |
|
||||||
| ------------------------ | ------ | -------------------------------------------------------------------------------------- |
|
| ------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
| gradidoID | String | technical unique key of the user as UUID (version 4) |
|
| gradidoID | String | technical unique key of the user as UUID (version 4) |
|
||||||
| alias | String | a business unique key of the user |
|
| alias | String | a business unique key of the user |
|
||||||
| passphraseEncryptionType | int | defines the type of encrypting the passphrase: 1 = email (default), 2 = gradidoID, ... |
|
| passphraseEncryptionType | int | defines the type of encrypting the passphrase: 1 = email (default), 2 = gradidoID, ... |
|
||||||
| emailID | int | technical foreign key to the new entity Contact |
|
| emailID | int | technical foreign key to the entry with type Email and contactChannel=maincontact of the new entity UserContacts |
|
||||||
|
|
||||||
##### Email vs emailID
|
##### Email vs emailID
|
||||||
|
|
||||||
@ -39,14 +39,21 @@ The preferred and proper solution will be to add a new column `Users.emailId `as
|
|||||||
|
|
||||||
A new entity `UserContacts `is introduced to store several contacts of different types like email, telephone or other kinds of contact addresses.
|
A new entity `UserContacts `is introduced to store several contacts of different types like email, telephone or other kinds of contact addresses.
|
||||||
|
|
||||||
| Column | Type | Description |
|
| Column | Type | Description |
|
||||||
| --------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| id | int | the technical key of a contact entity |
|
| id | int | the technical key of a contact entity |
|
||||||
| type | int | Defines the type of contact entry as enum: Email, Phone, etc |
|
| type | int | Defines the type of contact entry as enum: Email, Phone, etc |
|
||||||
| usersID | int | Defines the foreign key to the `Users` table |
|
| userID | int | Defines the foreign key to the `Users` table |
|
||||||
| email | String | defines the address of a contact entry of type Email |
|
| email | String | defines the address of a contact entry of type Email |
|
||||||
| phone | String | defines the address of a contact entry of type Phone |
|
| emailVerificationCode | unsinged bigint(20) | unique code to verify email or password reset |
|
||||||
| contactChannels | String | define the contact channel as comma separated list for which this entry is confirmed by the user e.g. main contact (default), infomail, contracting, advertisings, ... |
|
| emailOptInType | int | REGISTER=1, RESET_PASSWORD=2 |
|
||||||
|
| emailResendCount | int | counter how often the email was resend |
|
||||||
|
| emailChecked | boolean | flag if email is verified and confirmed |
|
||||||
|
| createdAt | DateTime | point of time the Contact was created |
|
||||||
|
| updatedAt | DateTime | point of time the Contact was updated |
|
||||||
|
| deletedAt | DateTime | point of time the Contact was soft deleted |
|
||||||
|
| phone | String | defines the address of a contact entry of type Phone |
|
||||||
|
| contactChannels | String | define the contact channel as comma separated list for which this entry is confirmed by the user e.g. main contact (default), infomail, contracting, advertisings, ... |
|
||||||
|
|
||||||
### Database-Migration
|
### Database-Migration
|
||||||
|
|
||||||
@ -58,18 +65,24 @@ In a one-time migration create for each entry of the `Users `tabel an unique UUI
|
|||||||
|
|
||||||
#### Primary Email Contact
|
#### Primary Email Contact
|
||||||
|
|
||||||
In a one-time migration read for each entry of the `Users `table the `Users.id` and `Users.email` and create for it a new entry in the `UsersContact `table, by initializing the contact-values with:
|
In a one-time migration read for each entry of the `Users `table the `Users.id` and `Users.email`, select from the table `login_email_opt_in` the entry with the `login_email_opt_in.user_id` = `Users.id` and create a new entry in the `UsersContact `table, by initializing the contact-values with:
|
||||||
|
|
||||||
* id = new technical key
|
* id = new technical key
|
||||||
* type = Enum-Email
|
* type = Enum-Email
|
||||||
* userID = `Users.id`
|
* userID = `Users.id`
|
||||||
* email = `Users.email`
|
* email = `Users.email`
|
||||||
|
* emailVerifyCode = `login_email_opt_in.verification_code`
|
||||||
|
* emailOptInType = `login_email_opt_in.email_opt_in_type_id`
|
||||||
|
* emailResendCount = `login_email_opt_in.resent_count`
|
||||||
|
* emailChecked = `Users.emailChecked`
|
||||||
|
* createdAt = `login_email_opt_in.created_at`
|
||||||
|
* updatedAt = `login_email_opt_in.updated_at`
|
||||||
* phone = null
|
* phone = null
|
||||||
* usedChannel = Enum-"main contact"
|
* usedChannel = Enum-"main contact"
|
||||||
|
|
||||||
and update the `Users `entry with `Users.emailId = UsersContact.Id` and `Users.passphraseEncryptionType = 1`
|
and update the `Users `entry with `Users.emailId = UsersContact.Id` and `Users.passphraseEncryptionType = 1`
|
||||||
|
|
||||||
After this one-time migration the column `Users.email` can be deleted.
|
After this one-time migration and a verification, which ensures that all data are migrated, then the columns `Users.email`, `Users.emailChecked`, `Users.emailHash` and the table `login_email_opt_in` can be deleted.
|
||||||
|
|
||||||
### Adaption of BusinessLogic
|
### Adaption of BusinessLogic
|
||||||
|
|
||||||
@ -109,7 +122,7 @@ The logic of change password has to be adapted by
|
|||||||
|
|
||||||
* read the users email address from the `UsersContact `table
|
* read the users email address from the `UsersContact `table
|
||||||
* give the email address as input for the password decryption of the existing password
|
* give the email address as input for the password decryption of the existing password
|
||||||
* use the `Users.userID` as input for the password encryption fo the new password
|
* use the `Users.userID` as input for the password encryption for the new password
|
||||||
* change the `Users.passphraseEnrycptionType` to the new value =2
|
* change the `Users.passphraseEnrycptionType` to the new value =2
|
||||||
* if the `Users.passphraseEncryptionType` = 2, then
|
* if the `Users.passphraseEncryptionType` = 2, then
|
||||||
|
|
||||||
@ -129,11 +142,17 @@ A new logic has to be introduced to search the user identity per different input
|
|||||||
A new mapping logic will be necessary to allow using unmigrated APIs like GDT-servers api. So it must be possible to give this identity-mapping logic the following input to get the respective output:
|
A new mapping logic will be necessary to allow using unmigrated APIs like GDT-servers api. So it must be possible to give this identity-mapping logic the following input to get the respective output:
|
||||||
|
|
||||||
* email -> userID
|
* email -> userID
|
||||||
|
* email -> gradidoID
|
||||||
* email -> alias
|
* email -> alias
|
||||||
|
* userID -> gradidoID
|
||||||
* userID -> email
|
* userID -> email
|
||||||
* userID -> alias
|
* userID -> alias
|
||||||
|
* alias -> gradidoID
|
||||||
* alias -> email
|
* alias -> email
|
||||||
* alias -> userID
|
* alias -> userID
|
||||||
|
* gradidoID -> email
|
||||||
|
* gradidoID -> userID
|
||||||
|
* gradidoID -> alias
|
||||||
|
|
||||||
#### GDT-Access
|
#### GDT-Access
|
||||||
|
|
||||||
|
|||||||
@ -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