start adaptions of users changes in backend

This commit is contained in:
Claus-Peter Hübner 2022-08-09 03:56:22 +02:00
parent f73f132d50
commit fadbc7068e
8 changed files with 176 additions and 22 deletions

View File

@ -19,6 +19,7 @@
"dependencies": {
"@types/jest": "^27.0.2",
"@types/lodash.clonedeep": "^4.5.6",
"@types/uuid": "^8.3.4",
"apollo-server-express": "^2.25.2",
"apollo-server-testing": "^2.25.2",
"axios": "^0.21.1",

View 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
})

View File

@ -8,12 +8,12 @@ import { FULL_CREATION_AVAILABLE } from '../resolver/const/const'
export class User {
constructor(user: dbUser, creation: Decimal[] = FULL_CREATION_AVAILABLE) {
this.id = user.id
this.email = user.email
this.email = user.emailContact.email
this.firstName = user.firstName
this.lastName = user.lastName
this.deletedAt = user.deletedAt
this.createdAt = user.createdAt
this.emailChecked = user.emailChecked
this.emailChecked = user.emailContact.emailChecked
this.language = user.language
this.publisherId = user.publisherId
this.isAdmin = user.isAdmin

View File

@ -1,12 +1,12 @@
import fs from 'fs'
import { backendLogger as logger } from '@/server/logger'
import { Context, getUser } from '@/server/context'
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
import { getConnection } from '@dbTools/typeorm'
import CONFIG from '@/config'
import { User } from '@model/User'
import { User as DbUser } from '@entity/User'
import { UserContact as DbUserContact } from '@entity/UserContact'
import { communityDbUser } from '@/util/communityUser'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
@ -32,6 +32,7 @@ import {
EventSendConfirmationEmail,
} from '@/event/Event'
import { getUserCreation } from './util/creations'
import { UserContactType } from '../enum/UserContactType'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const sodium = require('sodium-native')
@ -172,6 +173,19 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B
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 => {
logger.trace('newEmailOptIn...')
const emailOptIn = new LoginEmailOptIn()
@ -182,6 +196,7 @@ const newEmailOptIn = (userId: number): LoginEmailOptIn => {
return emailOptIn
}
/*
// needed by AdminResolver
// checks if given code exists and can be resent
// if optIn does not exits, it is created
@ -218,6 +233,36 @@ export const checkOptInCode = async (
logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${userId}`)
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 => {
logger.debug(`activationLink(${LoginEmailOptIn})...`)
@ -251,15 +296,31 @@ export class UserResolver {
): Promise<User> {
logger.info(`login with ${email}, ***, ${publisherId} ...`)
email = email.trim().toLowerCase()
const dbUser = await findUserByEmail(email)
/*
const dbUserContact = await DbUserContact.findOneOrFail({ email }, { withDeleted: true }).catch(
() => {
logger.error(`UserContact with email=${email} does not exists`)
throw new Error('No user with this credentials')
},
)
const userId = dbUserContact.userId
const dbUser = await DbUser.findOneOrFail(userId).catch(() => {
logger.error(`User with emeilContact=${email} connected per userId=${userId} does not exist`)
throw new Error('No user with this credentials')
})
*/
/*
const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => {
logger.error(`User with email=${email} does not exists`)
throw new Error('No user with this credentials')
})
*/
if (dbUser.deletedAt) {
logger.error('The User was permanently deleted in database.')
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.')
throw new Error('User email not validated')
}
@ -339,11 +400,13 @@ export class UserResolver {
// Validate email unique
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
const userFound = await DbUser.findOne({ email }, { withDeleted: true })
logger.info(`DbUser.findOne(email=${email}) = ${userFound}`)
const foundUser = await findUserByEmail(email)
if (userFound) {
// TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes
// const userFound = await DbUser.findOne({ email }, { withDeleted: true })
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`)
if (foundUser) {
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.
@ -382,8 +445,11 @@ export class UserResolver {
const eventRegister = new EventRegister()
const eventRedeemRegister = new EventRedeemRegister()
const eventSendConfirmEmail = new EventSendConfirmationEmail()
// const dbEmailContact = new DbUserContact()
// dbEmailContact.email = email
const dbUser = new DbUser()
dbUser.email = email
// dbUser.emailContact = dbEmailContact
dbUser.firstName = firstName
dbUser.lastName = lastName
// dbUser.emailHash = emailHash
@ -426,16 +492,29 @@ export class UserResolver {
logger.error('Error while saving dbUser', error)
throw new Error('error saving user')
})
const emailContact = newEmailContact(email, dbUser.id)
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
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)
await queryRunner.manager.save(emailOptIn).catch((error) => {
logger.error('Error while saving emailOptIn', error)
throw new Error('error saving email opt in')
})
*/
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/{optin}/g,
emailOptIn.verificationCode.toString(),
emailContact.emailVerificationCode.toString(),
).replace(/{code}/g, redeemCode ? '/' + redeemCode : '')
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -482,16 +561,19 @@ export class UserResolver {
async forgotPassword(@Arg('email') email: string): Promise<boolean> {
logger.info(`forgotPassword(${email})...`)
email = email.trim().toLowerCase()
const user = await DbUser.findOne({ email })
const user = await findUserByEmail(email)
// const user = await DbUser.findOne({ email })
if (!user) {
logger.warn(`no user found with ${email}`)
return true
}
// can be both types: REGISTER and RESET_PASSWORD
let optInCode = await LoginEmailOptIn.findOne({
userId: user.id,
})
// let optInCode = await LoginEmailOptIn.findOne({
// userId: user.id,
// })
let optInCode = user.emailContact.emailVerificationCode
optInCode = await checkEmailVerificationCode(user.emailContact, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
optInCode = await checkOptInCode(optInCode, user.id, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
logger.info(`optInCode for ${email}=${optInCode}`)
@ -727,25 +809,55 @@ export class UserResolver {
logger.info('missing context.user for EloPage-check')
return false
}
const elopageBuys = hasElopageBuys(userEntity.email)
const elopageBuys = hasElopageBuys(userEntity.emailContact.email)
logger.debug(`has ElopageBuys = ${elopageBuys}`)
return elopageBuys
}
}
async function findUserByEmail(email: string): Promise<DbUser> {
const dbUserContact = await DbUserContact.findOneOrFail(email, { withDeleted: true }).catch(
() => {
logger.error(`UserContact with email=${email} does not exists`)
throw new Error('No user with this credentials')
},
)
const userId = dbUserContact.userId
const dbUser = await DbUser.findOneOrFail(userId).catch(() => {
logger.error(`User with emeilContact=${email} connected per userId=${userId} does not exist`)
throw new Error('No user with this credentials')
})
return dbUser
}
/*
const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => {
const timeElapsed = Date.now() - new Date(optIn.updatedAt).getTime()
// time is given in minutes
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 => {
return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME)
}
*/
const isEmailVerificationCodeValid = (updatedAt: Date): boolean => {
return isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_VALID_TIME)
}
/*
const canResendOptIn = (optIn: LoginEmailOptIn): boolean => {
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 } => {
if (time > 60) {
@ -763,3 +875,4 @@ export const printTimeDuration = (duration: number): string => {
if (time.hours) return `${time.hours} hours` + (result !== '' ? ` and ${result}` : '')
return result
}

View File

@ -2,13 +2,14 @@
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
import { User as dbUser } from '@entity/User'
// import { UserContact as EmailContact } from '@entity/UserContact'
import { User } from '@model/User'
const communityDbUser: dbUser = {
id: -1,
gradidoID: '11111111-2222-3333-4444-55555555',
alias: '',
email: 'support@gradido.net',
// email: 'support@gradido.net',
firstName: 'Gradido',
lastName: 'Akademie',
pubKey: Buffer.from(''),
@ -17,7 +18,7 @@ const communityDbUser: dbUser = {
password: BigInt(0),
// emailHash: Buffer.from(''),
createdAt: new Date(),
emailChecked: false,
// emailChecked: false,
language: '',
isAdmin: null,
publisherId: 0,

View File

@ -1000,6 +1000,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
"@types/uuid@^8.3.4":
version "8.3.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
"@types/validator@^13.1.3":
version "13.6.3"
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.6.3.tgz#31ca2e997bf13a0fffca30a25747d5b9f7dbb7de"

View File

@ -6,8 +6,10 @@ import {
DeleteDateColumn,
OneToMany,
JoinColumn,
OneToOne,
} from 'typeorm'
import { Contribution } from '../Contribution'
import { UserContact } from '../UserContact'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@ -37,11 +39,18 @@ export class User extends BaseEntity {
@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, { primary: true, cascade: true })
@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',
@ -69,9 +78,10 @@ export class User extends BaseEntity {
@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
@ -106,4 +116,8 @@ export class User extends BaseEntity {
@OneToMany(() => Contribution, (contribution) => contribution.user)
@JoinColumn({ name: 'user_id' })
contributions?: Contribution[]
@OneToMany(() => UserContact, (usercontact) => usercontact.userId)
@JoinColumn({ name: 'user_id' })
usercontacts?: UserContact[]
}

View File

@ -20,8 +20,17 @@ export class UserContact extends BaseEntity {
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
email: string
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
emailHash: Buffer
@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