gradido/backend/src/graphql/resolver/UserResolver.ts

865 lines
29 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
import { User as DbUser } from '@entity/User'
import { UserContact as DbUserContact } from '@entity/UserContact'
import i18n from 'i18n'
import {
Resolver,
Query,
Args,
Arg,
Authorized,
Ctx,
UseMiddleware,
Mutation,
Int,
} from 'type-graphql'
import { v4 as uuidv4 } from 'uuid'
import CreateUserArgs from '@arg/CreateUserArgs'
import Paginated from '@arg/Paginated'
import SearchUsersArgs from '@arg/SearchUsersArgs'
import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs'
import { OptInType } from '@enum/OptInType'
import { Order } from '@enum/Order'
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
import { UserContactType } from '@enum/UserContactType'
import { SearchAdminUsersResult } from '@model/AdminUser'
import { User } from '@model/User'
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
import { UserRepository } from '@repository/User'
import { klicktippSignIn } from '@/apis/KlicktippController'
import { encode } from '@/auth/JWT'
import { RIGHTS } from '@/auth/RIGHTS'
import CONFIG from '@/config'
import {
sendAccountActivationEmail,
sendAccountMultiRegistrationEmail,
sendResetPasswordEmail,
} from '@/emails/sendEmailVariants'
import {
Event,
EventType,
EVENT_USER_LOGIN,
EVENT_EMAIL_ACCOUNT_MULTIREGISTRATION,
EVENT_EMAIL_CONFIRMATION,
EVENT_USER_REGISTER,
EVENT_USER_ACTIVATE_ACCOUNT,
EVENT_EMAIL_ADMIN_CONFIRMATION,
EVENT_USER_LOGOUT,
EVENT_EMAIL_FORGOT_PASSWORD,
EVENT_USER_INFO_UPDATE,
EVENT_ADMIN_USER_ROLE_SET,
EVENT_ADMIN_USER_DELETE,
EVENT_ADMIN_USER_UNDELETE,
} from '@/event/Event'
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
import { isValidPassword } from '@/password/EncryptorUtils'
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
import LogError from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { communityDbUser } from '@/util/communityUser'
import { hasElopageBuys } from '@/util/hasElopageBuys'
import { getTimeDurationObject, printTimeDuration } from '@/util/time'
import { FULL_CREATION_AVAILABLE } from './const/const'
import { getUserCreations } from './util/creations'
import { findUserByIdentifier } from './util/findUserByIdentifier'
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs
const random = require('random-bigint')
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs
const sodium = require('sodium-native')
const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl']
const DEFAULT_LANGUAGE = 'de'
const isLanguage = (language: string): boolean => {
return LANGUAGES.includes(language)
}
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
}
// eslint-disable-next-line @typescript-eslint/ban-types
export const activationLink = (verificationCode: BigInt): string => {
logger.debug(`activationLink(${verificationCode})...`)
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, verificationCode.toString())
}
const newGradidoID = async (): Promise<string> => {
let gradidoId: string
let countIds: number
do {
gradidoId = uuidv4()
countIds = await DbUser.count({ where: { gradidoID: gradidoId } })
if (countIds > 0) {
logger.info('Gradido-ID creation conflict...')
}
} while (countIds > 0)
return gradidoId
}
@Resolver()
export class UserResolver {
@Authorized([RIGHTS.VERIFY_LOGIN])
@Query(() => User)
@UseMiddleware(klicktippNewsletterStateMiddleware)
async verifyLogin(@Ctx() context: Context): Promise<User> {
logger.info('verifyLogin...')
// TODO refactor and do not have duplicate code with login(see below)
const userEntity = getUser(context)
const user = new User(userEntity)
// Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage(context)
logger.debug(`verifyLogin... successful: ${user.firstName}.${user.lastName}, ${user.email}`)
return user
}
@Authorized([RIGHTS.LOGIN])
@Mutation(() => User)
@UseMiddleware(klicktippNewsletterStateMiddleware)
async login(
@Args() { email, password, publisherId }: UnsecureLoginArgs,
@Ctx() context: Context,
): Promise<User> {
logger.info(`login with ${email}, ***, ${publisherId} ...`)
email = email.trim().toLowerCase()
const dbUser = await findUserByEmail(email)
if (dbUser.deletedAt) {
throw new LogError('This user was permanently deleted. Contact support for questions', dbUser)
}
if (!dbUser.emailContact.emailChecked) {
throw new LogError('The Users email is not validate yet', dbUser)
}
// TODO: at least in test this does not work since `dbUser.password = 0` and `BigInto(0) = 0n`
if (dbUser.password === BigInt(0)) {
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code
throw new LogError('The User has not set a password yet', dbUser)
}
if (!verifyPassword(dbUser, password)) {
throw new LogError('No user with this credentials', dbUser)
}
if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) {
dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
dbUser.password = encryptPassword(dbUser, password)
await dbUser.save()
}
// add pubKey in logger-context for layout-pattern X{user} to print it in each logging message
logger.addContext('user', dbUser.id)
logger.debug('validation of login credentials successful...')
const user = new User(dbUser)
logger.debug(`user= ${JSON.stringify(user, null, 2)}`)
i18n.setLocale(user.language)
// Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
logger.info('user.hasElopage', user.hasElopage)
if (!user.hasElopage && publisherId) {
user.publisherId = publisherId
dbUser.publisherId = publisherId
await DbUser.save(dbUser)
}
context.setHeaders.push({
key: 'token',
value: encode(dbUser.gradidoID),
})
await EVENT_USER_LOGIN(dbUser)
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
return user
}
@Authorized([RIGHTS.LOGOUT])
@Mutation(() => Boolean)
async logout(@Ctx() context: Context): Promise<boolean> {
await EVENT_USER_LOGOUT(getUser(context))
// remove user from logger context
logger.addContext('user', 'unknown')
return true
}
@Authorized([RIGHTS.CREATE_USER])
@Mutation(() => User)
async createUser(
@Args()
{ email, firstName, lastName, language, publisherId = null, redeemCode = null }: CreateUserArgs,
): Promise<User> {
logger.addContext('user', 'unknown')
logger.info(
`createUser(email=${email}, firstName=${firstName}, lastName=${lastName}, language=${language}, publisherId=${publisherId}, redeemCode =${redeemCode})`,
)
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
// default int publisher_id = 0;
// Validate Language (no throw)
if (!language || !isLanguage(language)) {
language = DEFAULT_LANGUAGE
}
i18n.setLocale(language)
// check if user with email still exists?
email = email.trim().toLowerCase()
if (await checkEmailExists(email)) {
const foundUser = await findUserByEmail(email)
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`)
if (foundUser) {
// ATTENTION: this logger-message will be exactly expected during tests, next line
logger.info(`User already exists with this email=${email}`)
logger.info(
`Specified username when trying to register multiple times with this email: firstName=${firstName}, lastName=${lastName}`,
)
// 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)
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.email = email
user.firstName = firstName
user.lastName = lastName
user.language = language
user.publisherId = publisherId
logger.debug('partly faked user', user)
const emailSent = await sendAccountMultiRegistrationEmail({
firstName: foundUser.firstName, // this is the real name of the email owner, but just "firstName" would be the name of the new registrant which shall not be passed to the outside
lastName: foundUser.lastName, // this is the real name of the email owner, but just "lastName" would be the name of the new registrant which shall not be passed to the outside
email,
language: foundUser.language, // use language of the emails owner for sending
})
await EVENT_EMAIL_ACCOUNT_MULTIREGISTRATION(foundUser)
logger.info(
`sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${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
}
}
const gradidoID = await newGradidoID()
const eventRegisterRedeem = Event(
EventType.USER_REGISTER_REDEEM,
{ id: 0 } as DbUser,
{ id: 0 } as DbUser,
)
let dbUser = new DbUser()
dbUser.gradidoID = gradidoID
dbUser.firstName = firstName
dbUser.lastName = lastName
dbUser.language = language
dbUser.publisherId = publisherId || 0
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
logger.debug('new dbUser', dbUser)
if (redeemCode) {
if (redeemCode.match(/^CL-/)) {
const contributionLink = await DbContributionLink.findOne({
code: redeemCode.replace('CL-', ''),
})
logger.info('redeemCode found contributionLink', contributionLink)
if (contributionLink) {
dbUser.contributionLinkId = contributionLink.id
eventRegisterRedeem.involvedContributionLink = contributionLink
}
} else {
const transactionLink = await DbTransactionLink.findOne({ code: redeemCode })
logger.info('redeemCode found transactionLink', transactionLink)
if (transactionLink) {
dbUser.referrerId = transactionLink.userId
eventRegisterRedeem.involvedTransactionLink = transactionLink
}
}
}
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ')
try {
dbUser = await queryRunner.manager.save(dbUser).catch((error) => {
throw new LogError('Error while saving dbUser', error)
})
let emailContact = newEmailContact(email, dbUser.id)
emailContact = await queryRunner.manager.save(emailContact).catch((error) => {
throw new LogError('Error while saving user email contact', error)
})
dbUser.emailContact = emailContact
dbUser.emailId = emailContact.id
await queryRunner.manager.save(dbUser).catch((error) => {
throw new LogError('Error while updating dbUser', error)
})
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/{optin}/g,
emailContact.emailVerificationCode.toString(),
).replace(/{code}/g, redeemCode ? '/' + redeemCode : '')
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendAccountActivationEmail({
firstName,
lastName,
email,
language,
activationLink,
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
})
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
await EVENT_EMAIL_CONFIRMATION(dbUser)
if (!emailSent) {
logger.debug(`Account confirmation link: ${activationLink}`)
}
await queryRunner.commitTransaction()
logger.addContext('user', dbUser.id)
} catch (e) {
await queryRunner.rollbackTransaction()
throw new LogError('Error creating user', e)
} finally {
await queryRunner.release()
}
logger.info('createUser() successful...')
if (redeemCode) {
eventRegisterRedeem.affectedUser = dbUser
eventRegisterRedeem.actingUser = dbUser
await eventRegisterRedeem.save()
} else {
await EVENT_USER_REGISTER(dbUser)
}
return new User(dbUser)
}
@Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL])
@Mutation(() => Boolean)
async forgotPassword(@Arg('email') email: string): Promise<boolean> {
logger.addContext('user', 'unknown')
logger.info(`forgotPassword(${email})...`)
email = email.trim().toLowerCase()
const user = await findUserByEmail(email).catch(() => {
logger.warn(`fail on find UserContact per ${email}`)
})
if (!user) {
logger.warn(`no user found with ${email}`)
return true
}
if (!canEmailResend(user.emailContact.updatedAt || user.emailContact.createdAt)) {
throw new LogError(
`Email already sent less than ${printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} ago`,
)
}
user.emailContact.updatedAt = new Date()
user.emailContact.emailResendCount++
user.emailContact.emailVerificationCode = random(64)
user.emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_RESET_PASSWORD
await user.emailContact.save().catch(() => {
throw new LogError('Unable to save email verification code', user.emailContact)
})
logger.info(`optInCode for ${email}=${user.emailContact}`)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendResetPasswordEmail({
firstName: user.firstName,
lastName: user.lastName,
email,
language: user.language,
resetLink: activationLink(user.emailContact.emailVerificationCode),
timeDurationObject: getTimeDurationObject(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
if (!emailSent) {
logger.debug(
`Reset password link: ${activationLink(user.emailContact.emailVerificationCode)}`,
)
}
logger.info(`forgotPassword(${email}) successful...`)
await EVENT_EMAIL_FORGOT_PASSWORD(user)
return true
}
@Authorized([RIGHTS.SET_PASSWORD])
@Mutation(() => Boolean)
async setPassword(
@Arg('code') code: string,
@Arg('password') password: string,
): Promise<boolean> {
logger.info(`setPassword(${code}, ***)...`)
// Validate Password
if (!isValidPassword(password)) {
throw new LogError(
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
)
}
// load code
const userContact = await DbUserContact.findOneOrFail(
{ emailVerificationCode: code },
{ relations: ['user'] },
).catch(() => {
throw new LogError('Could not login with emailVerificationCode')
})
logger.debug('userContact loaded...')
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
throw new LogError(
`Email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
)
}
logger.debug('EmailVerificationCode is valid...')
// load user
const user = userContact.user
logger.debug('user with EmailVerificationCode found...')
// Activate EMail
userContact.emailChecked = true
// Update Password
user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
user.password = encryptPassword(user, password)
logger.debug('User credentials updated ...')
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ')
try {
// Save user
await queryRunner.manager.save(user).catch((error) => {
throw new LogError('Error saving user', error)
})
// Save userContact
await queryRunner.manager.save(userContact).catch((error) => {
throw new LogError('Error saving userContact', error)
})
await queryRunner.commitTransaction()
logger.info('User and UserContact data written successfully...')
} catch (e) {
await queryRunner.rollbackTransaction()
throw new LogError('Error on writing User and User Contact data', e)
} finally {
await queryRunner.release()
}
// Sign into Klicktipp
// TODO do we always signUp the user? How to handle things with old users?
if (userContact.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
try {
await klicktippSignIn(userContact.email, user.language, user.firstName, user.lastName)
logger.debug(
`klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
)
} catch (e) {
logger.error('Error subscribing to klicktipp', e)
}
}
await EVENT_USER_ACTIVATE_ACCOUNT(user)
return true
}
@Authorized([RIGHTS.QUERY_OPT_IN])
@Query(() => Boolean)
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
logger.info(`queryOptIn(${optIn})...`)
const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn })
logger.debug(`found optInCode=${userContact}`)
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
throw new LogError(
`Email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
)
}
logger.info(`queryOptIn(${optIn}) successful...`)
return true
}
@Authorized([RIGHTS.UPDATE_USER_INFOS])
@Mutation(() => Boolean)
async updateUserInfos(
@Args()
{
firstName,
lastName,
language,
password,
passwordNew,
hideAmountGDD,
hideAmountGDT,
}: UpdateUserInfosArgs,
@Ctx() context: Context,
): Promise<boolean> {
logger.info(`updateUserInfos(${firstName}, ${lastName}, ${language}, ***, ***)...`)
const user = getUser(context)
if (firstName) {
user.firstName = firstName
}
if (lastName) {
user.lastName = lastName
}
if (language) {
if (!isLanguage(language)) {
throw new LogError('Given language is not a valid language', language)
}
user.language = language
i18n.setLocale(language)
}
if (password && passwordNew) {
// Validate Password
if (!isValidPassword(passwordNew)) {
throw new LogError(
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
)
}
if (!verifyPassword(user, password)) {
throw new LogError(`Old password is invalid`)
}
// Save new password hash and newly encrypted private key
user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
user.password = encryptPassword(user, passwordNew)
}
// Save hideAmountGDD value
if (hideAmountGDD !== undefined) {
user.hideAmountGDD = hideAmountGDD
}
// Save hideAmountGDT value
if (hideAmountGDT !== undefined) {
user.hideAmountGDT = hideAmountGDT
}
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ')
try {
await queryRunner.manager.save(user).catch((error) => {
throw new LogError('Error saving user', error)
})
await queryRunner.commitTransaction()
logger.debug('writing User data successful...')
} catch (e) {
await queryRunner.rollbackTransaction()
throw new LogError('Error on writing updated user data', e)
} finally {
await queryRunner.release()
}
logger.info('updateUserInfos() successfully finished...')
await EVENT_USER_INFO_UPDATE(user)
return true
}
@Authorized([RIGHTS.HAS_ELOPAGE])
@Query(() => Boolean)
async hasElopage(@Ctx() context: Context): Promise<boolean> {
logger.info(`hasElopage()...`)
const userEntity = getUser(context)
const elopageBuys = hasElopageBuys(userEntity.emailContact.email)
logger.debug(`has ElopageBuys = ${elopageBuys}`)
return elopageBuys
}
@Authorized([RIGHTS.SEARCH_ADMIN_USERS])
@Query(() => SearchAdminUsersResult)
async searchAdminUsers(
@Args()
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
): Promise<SearchAdminUsersResult> {
const userRepository = getCustomRepository(UserRepository)
const [users, count] = await userRepository.findAndCount({
where: {
isAdmin: Not(IsNull()),
},
order: {
createdAt: order,
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
return {
userCount: count,
userList: users.map((user) => {
return {
firstName: user.firstName,
lastName: user.lastName,
}
}),
}
}
@Authorized([RIGHTS.SEARCH_USERS])
@Query(() => SearchUsersResult)
async searchUsers(
@Args()
{ searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs,
@Ctx() context: Context,
): Promise<SearchUsersResult> {
const clientTimezoneOffset = getClientTimezoneOffset(context)
const userRepository = getCustomRepository(UserRepository)
const userFields = [
'id',
'firstName',
'lastName',
'emailId',
'emailContact',
'deletedAt',
'isAdmin',
]
const [users, count] = await userRepository.findBySearchCriteriaPagedFiltered(
userFields.map((fieldName) => {
return 'user.' + fieldName
}),
searchText,
filters || null,
currentPage,
pageSize,
)
if (users.length === 0) {
return {
userCount: 0,
userList: [],
}
}
const creations = await getUserCreations(
users.map((u) => u.id),
clientTimezoneOffset,
)
const adminUsers = await Promise.all(
users.map(async (user) => {
let emailConfirmationSend = ''
if (!user.emailContact.emailChecked) {
if (user.emailContact.updatedAt) {
emailConfirmationSend = user.emailContact.updatedAt.toISOString()
} else {
emailConfirmationSend = user.emailContact.createdAt.toISOString()
}
}
const userCreations = creations.find((c) => c.id === user.id)
const adminUser = new UserAdmin(
user,
userCreations ? userCreations.creations : FULL_CREATION_AVAILABLE,
await hasElopageBuys(user.emailContact.email),
emailConfirmationSend,
)
return adminUser
}),
)
return {
userCount: count,
userList: adminUsers,
}
}
@Authorized([RIGHTS.SET_USER_ROLE])
@Mutation(() => Date, { nullable: true })
async setUserRole(
@Arg('userId', () => Int)
userId: number,
@Arg('isAdmin', () => Boolean)
isAdmin: boolean,
@Ctx()
context: Context,
): Promise<Date | null> {
const user = await DbUser.findOne({ id: userId })
// user exists ?
if (!user) {
throw new LogError('Could not find user with given ID', userId)
}
// administrator user changes own role?
const moderator = getUser(context)
if (moderator.id === userId) {
throw new LogError('Administrator can not change his own role')
}
// change isAdmin
switch (user.isAdmin) {
case null:
if (isAdmin === true) {
user.isAdmin = new Date()
} else {
throw new LogError('User is already an usual user')
}
break
default:
if (isAdmin === false) {
user.isAdmin = null
} else {
throw new LogError('User is already admin')
}
break
}
await user.save()
await EVENT_ADMIN_USER_ROLE_SET(user, moderator)
const newUser = await DbUser.findOne({ id: userId })
return newUser ? newUser.isAdmin : null
}
@Authorized([RIGHTS.DELETE_USER])
@Mutation(() => Date, { nullable: true })
async deleteUser(
@Arg('userId', () => Int) userId: number,
@Ctx() context: Context,
): Promise<Date | null> {
const user = await DbUser.findOne({ id: userId })
// user exists ?
if (!user) {
throw new LogError('Could not find user with given ID', userId)
}
// moderator user disabled own account?
const moderator = getUser(context)
if (moderator.id === userId) {
throw new LogError('Moderator can not delete his own account')
}
// soft-delete user
await user.softRemove()
await EVENT_ADMIN_USER_DELETE(user, moderator)
const newUser = await DbUser.findOne({ id: userId }, { withDeleted: true })
return newUser ? newUser.deletedAt : null
}
@Authorized([RIGHTS.UNDELETE_USER])
@Mutation(() => Date, { nullable: true })
async unDeleteUser(
@Arg('userId', () => Int) userId: number,
@Ctx() context: Context,
): Promise<Date | null> {
const user = await DbUser.findOne({ id: userId }, { withDeleted: true })
if (!user) {
throw new LogError('Could not find user with given ID', userId)
}
if (!user.deletedAt) {
throw new LogError('User is not deleted')
}
await user.recover()
await EVENT_ADMIN_USER_UNDELETE(user, getUser(context))
return null
}
// TODO this is an admin function - needs refactor
@Authorized([RIGHTS.SEND_ACTIVATION_EMAIL])
@Mutation(() => Boolean)
async sendActivationEmail(
@Arg('email') email: string,
@Ctx() context: Context,
): Promise<boolean> {
email = email.trim().toLowerCase()
// const user = await dbUser.findOne({ id: emailContact.userId })
const user = await findUserByEmail(email)
if (user.deletedAt || user.emailContact.deletedAt) {
throw new LogError('User with given email contact is deleted', email)
}
user.emailContact.emailResendCount++
await user.emailContact.save()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendAccountActivationEmail({
firstName: user.firstName,
lastName: user.lastName,
email,
language: user.language,
activationLink: activationLink(user.emailContact.emailVerificationCode),
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
})
// In case EMails are disabled log the activation link for the user
if (!emailSent) {
logger.info(`Account confirmation link: ${activationLink}`)
}
await EVENT_EMAIL_ADMIN_CONFIRMATION(user, getUser(context))
return true
}
@Authorized([RIGHTS.USER])
@Query(() => User)
async user(@Arg('identifier') identifier: string): Promise<User> {
return new User(await findUserByIdentifier(identifier))
}
}
export async function findUserByEmail(email: string): Promise<DbUser> {
const dbUserContact = await DbUserContact.findOneOrFail(
{ email },
{ withDeleted: true, relations: ['user'] },
).catch(() => {
throw new LogError('No user with this credentials', email)
})
const dbUser = dbUserContact.user
dbUser.emailContact = dbUserContact
return dbUser
}
async function checkEmailExists(email: string): Promise<boolean> {
const userContact = await DbUserContact.findOne({ email }, { withDeleted: true })
if (userContact) {
return true
}
return false
}
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 isEmailVerificationCodeValid = (updatedAt: Date): boolean => {
return isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_VALID_TIME)
}
const canEmailResend = (updatedAt: Date): boolean => {
return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME)
}