mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
start adaptions of users changes in backend
This commit is contained in:
parent
f73f132d50
commit
fadbc7068e
@ -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",
|
||||
|
||||
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
|
||||
})
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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[]
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user