gradido/backend/src/graphql/resolver/UserResolver.ts
2025-03-16 16:38:15 +01:00

1088 lines
38 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, In, Point } from '@dbTools/typeorm'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import { ProjectBranding } from '@entity/ProjectBranding'
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, Mutation, Int } from 'type-graphql'
import { IRestResponse } from 'typed-rest-client'
import { v4 as uuidv4 } from 'uuid'
import { UserArgs } from '@arg//UserArgs'
import { CreateUserArgs } from '@arg/CreateUserArgs'
import { Paginated } from '@arg/Paginated'
import { SearchUsersFilters } from '@arg/SearchUsersFilters'
import { SetUserRoleArgs } from '@arg/SetUserRoleArgs'
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 { Location } from '@model/Location'
import { GmsUserAuthenticationResult } from '@model/GmsUserAuthenticationResult'
import { User } from '@model/User'
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
import { UserLocationResult } from '@model/UserLocationResult'
import { HumHubClient } from '@/apis/humhub/HumHubClient'
import { GetUser } from '@/apis/humhub/model/GetUser'
import { PostUser } from '@/apis/humhub/model/PostUser'
import { subscribe } from '@/apis/KlicktippController'
import { encode } from '@/auth/JWT'
import { RIGHTS } from '@/auth/RIGHTS'
import { CONFIG } from '@/config'
import { PublishNameLogic } from '@/data/PublishName.logic'
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/Events'
import { PublishNameType } from '@/graphql/enum/PublishNameType'
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 { delay } from '@/util/utilities'
import random from 'random-bigint'
import { randombytes_random } from 'sodium-native'
import { FULL_CREATION_AVAILABLE } from './const/const'
import { authenticateGmsUserPlayground } from './util/authenticateGmsUserPlayground'
import { getHomeCommunity } from './util/communities'
import { compareGmsRelevantUserSettings } from './util/compareGmsRelevantUserSettings'
import { getUserCreations } from './util/creations'
import { findUserByIdentifier } from './util/findUserByIdentifier'
import { findUsers } from './util/findUsers'
import { getKlicktippState } from './util/getKlicktippState'
import { Location2Point, Point2Location } from './util/Location2Point'
import { setUserRole, deleteUserRole } from './util/modifyUserRole'
import { sendUserToGms } from './util/sendUserToGms'
import { syncHumhub } from './util/syncHumhub'
import { validateAlias } from './util/validateAlias'
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).toString()
logger.debug('newEmailContact...successful', emailContact)
return emailContact
}
// eslint-disable-next-line @typescript-eslint/ban-types
export const activationLink = (verificationCode: string): string => {
logger.debug(`activationLink(${verificationCode})...`)
return CONFIG.EMAIL_LINK_SETPASSWORD + 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)
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.klickTipp = await getKlicktippState(userEntity.emailContact.email)
return user
}
@Authorized([RIGHTS.LOGIN])
@Mutation(() => User)
async login(
@Args() { email, password, publisherId, project }: UnsecureLoginArgs,
@Ctx() context: Context,
): Promise<User> {
logger.info(`login with ${email}, ***, ${publisherId}, project=${project} ...`)
email = email.trim().toLowerCase()
let dbUser: DbUser
try {
dbUser = await findUserByEmail(email)
} catch (e) {
// simulate delay which occur on password encryption 650 ms +- 50 rnd
await delay(650 + Math.floor(Math.random() * 101) - 50)
throw e
}
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 (!(await verifyPassword(dbUser, password))) {
throw new LogError('No user with this credentials', dbUser)
}
// request to humhub and klicktipp run in parallel
let humhubUserPromise: Promise<IRestResponse<GetUser>> | undefined
let projectBrandingPromise: Promise<ProjectBranding | null> | undefined
const klicktippStatePromise = getKlicktippState(dbUser.emailContact.email)
if (CONFIG.HUMHUB_ACTIVE && dbUser.humhubAllowed) {
const getHumhubUser = new PostUser(dbUser)
humhubUserPromise = HumHubClient.getInstance()?.userByUsernameAsync(
getHumhubUser.account.username,
)
}
if (project) {
projectBrandingPromise = ProjectBranding.findOne({
where: { alias: project },
select: { spaceId: true },
})
}
if (
(dbUser.passwordEncryptionType as PasswordEncryptionType) !==
PasswordEncryptionType.GRADIDO_ID
) {
dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
dbUser.password = await 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: await encode(dbUser.gradidoID),
})
await EVENT_USER_LOGIN(dbUser)
const projectBranding = await projectBrandingPromise
logger.debug('project branding: ', projectBranding)
// load humhub state
if (humhubUserPromise) {
try {
const result = await humhubUserPromise
user.humhubAllowed = result?.result?.account.status === 1
if (user.humhubAllowed) {
let spaceId = null
if (projectBranding) {
spaceId = projectBranding.spaceId
}
void syncHumhub(null, dbUser, spaceId)
}
} catch (e) {
logger.error("couldn't reach out to humhub, disable for now", e)
user.humhubAllowed = false
}
}
user.klickTipp = await klicktippStatePromise
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()
{
alias = null,
email,
firstName,
lastName,
language,
publisherId = null,
redeemCode = null,
project = null,
}: CreateUserArgs,
): Promise<User> {
logger.addContext('user', 'unknown')
logger.info(
`createUser(email=${email}, firstName=${firstName}, lastName=${lastName}, language=${language}, publisherId=${publisherId}, redeemCode=${redeemCode}, project=${project})`,
)
// 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, 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 = 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.firstName = firstName
user.lastName = lastName
user.language = language
user.publisherId = publisherId
if (alias && (await validateAlias(alias))) {
user.alias = alias
}
logger.debug('partly faked user', user)
void 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
logger.info('createUser() faked and send multi registration mail...')
return user
}
}
let projectBrandingPromise: Promise<ProjectBranding | null> | undefined
if (project) {
projectBrandingPromise = ProjectBranding.findOne({
where: { alias: project },
select: { logoUrl: true, spaceId: true },
})
}
const gradidoID = await newGradidoID()
const eventRegisterRedeem = Event(
EventType.USER_REGISTER_REDEEM,
{ id: 0 } as DbUser,
{ id: 0 } as DbUser,
)
let dbUser = new DbUser()
const homeCom = await getHomeCommunity()
if (homeCom.communityUuid) {
dbUser.communityUuid = homeCom.communityUuid
}
dbUser.gradidoID = gradidoID
dbUser.firstName = firstName
dbUser.lastName = lastName
dbUser.language = language
// enable humhub from now on for new user
dbUser.humhubAllowed = true
if (alias && (await validateAlias(alias))) {
dbUser.alias = alias
}
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({
where: { 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({ where: { 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')
let projectBranding: ProjectBranding | null | undefined
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
}${emailContact.emailVerificationCode.toString()}${redeemCode ? `/${redeemCode}` : ''}${
project ? `?project=` + project : ''
}`
projectBranding = projectBrandingPromise ? await projectBrandingPromise : undefined
void sendAccountActivationEmail({
firstName,
lastName,
email,
language,
activationLink,
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
logoUrl: projectBranding?.logoUrl,
})
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
await EVENT_EMAIL_CONFIRMATION(dbUser)
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 (CONFIG.HUMHUB_ACTIVE) {
let spaceId: number | null = null
if (projectBranding) {
spaceId = projectBranding.spaceId
}
void syncHumhub(null, dbUser, spaceId)
}
if (redeemCode) {
eventRegisterRedeem.affectedUser = dbUser
eventRegisterRedeem.actingUser = dbUser
await eventRegisterRedeem.save()
} else {
await EVENT_USER_REGISTER(dbUser)
}
if (!CONFIG.GMS_ACTIVE) {
logger.info('GMS deactivated per configuration! New user is not published to GMS.')
} else {
try {
if (dbUser.gmsAllowed && !dbUser.gmsRegistered) {
await sendUserToGms(dbUser, homeCom)
}
} catch (err) {
if (CONFIG.GMS_CREATE_USER_THROW_ERRORS) {
throw new LogError('Error publishing new created user to GMS:', err)
} else {
logger.error('Error publishing new created user to GMS:', err)
}
}
}
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((error) => {
logger.warn(`fail on find UserContact per ${email} because: ${error}`)
})
if (!user || user.deletedAt) {
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).toString()
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)
void sendResetPasswordEmail({
firstName: user.firstName,
lastName: user.lastName,
email,
language: user.language,
resetLink: activationLink(user.emailContact.emailVerificationCode),
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
})
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({
where: { 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 = await 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 as OptInType) === OptInType.EMAIL_OPT_IN_REGISTER) {
try {
await subscribe(userContact.email, user.language, user.firstName, user.lastName)
logger.debug(
`subscribe(${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({
where: { 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.CHECK_USERNAME])
@Query(() => Boolean)
async checkUsername(@Arg('username') username: string): Promise<boolean> {
try {
await validateAlias(username)
return true
} catch {
return false
}
}
@Authorized([RIGHTS.UPDATE_USER_INFOS])
@Mutation(() => Boolean)
async updateUserInfos(
@Args() updateUserInfosArgs: UpdateUserInfosArgs,
@Ctx() context: Context,
): Promise<boolean> {
const {
firstName,
lastName,
alias,
language,
password,
passwordNew,
hideAmountGDD,
hideAmountGDT,
humhubAllowed,
gmsAllowed,
gmsPublishName,
humhubPublishName,
gmsLocation,
gmsPublishLocation,
} = updateUserInfosArgs
logger.info(
`updateUserInfos(${firstName}, ${lastName}, ${alias}, ${language}, ***, ***, ${hideAmountGDD}, ${hideAmountGDT}, ${gmsAllowed}, ${gmsPublishName}, ${gmsLocation}, ${gmsPublishLocation})...`,
)
const user = getUser(context)
const updateUserInGMS = compareGmsRelevantUserSettings(user, updateUserInfosArgs)
// try {
if (firstName) {
user.firstName = firstName
}
if (lastName) {
user.lastName = lastName
}
// currently alias can only be set, not updated
if (alias && !user.alias && (await validateAlias(alias))) {
user.alias = alias
}
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 (!(await 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 = await encryptPassword(user, passwordNew)
}
// Save hideAmountGDD value
if (hideAmountGDD !== undefined) {
user.hideAmountGDD = hideAmountGDD
}
// Save hideAmountGDT value
if (hideAmountGDT !== undefined) {
user.hideAmountGDT = hideAmountGDT
}
if (humhubAllowed !== undefined) {
user.humhubAllowed = humhubAllowed
}
if (gmsAllowed !== undefined) {
user.gmsAllowed = gmsAllowed
}
if (gmsPublishName !== null && gmsPublishName !== undefined) {
user.gmsPublishName = gmsPublishName
}
if (humhubPublishName !== null && humhubPublishName !== undefined) {
user.humhubPublishName = humhubPublishName
}
if (gmsLocation) {
user.location = Location2Point(gmsLocation)
}
if (gmsPublishLocation !== null && gmsPublishLocation !== undefined) {
user.gmsPublishLocation = gmsPublishLocation
}
// } catch (err) {
// console.log('error:', err)
// }
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...', user)
} 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)
// validate if user settings are changed with relevance to update gms-user
try {
if (CONFIG.GMS_ACTIVE && updateUserInGMS) {
logger.debug(`changed user-settings relevant for gms-user update...`)
const homeCom = await getHomeCommunity()
if (homeCom.gmsApiKey !== null) {
logger.debug(`send User to Gms...`, user)
await sendUserToGms(user, homeCom)
logger.debug(`sendUserToGms successfully.`)
}
}
} catch (e) {
logger.error('error sync user with gms', e)
}
try {
if (CONFIG.HUMHUB_ACTIVE) {
await syncHumhub(updateUserInfosArgs, user)
}
} catch (e) {
logger.error('error sync user with humhub', e)
}
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.GMS_USER_PLAYGROUND])
@Query(() => GmsUserAuthenticationResult)
async authenticateGmsUserSearch(@Ctx() context: Context): Promise<GmsUserAuthenticationResult> {
logger.info(`authenticateGmsUserSearch()...`)
const dbUser = getUser(context)
let result = new GmsUserAuthenticationResult()
if (context.token) {
const homeCom = await getHomeCommunity()
if (!homeCom.gmsApiKey) {
throw new LogError('authenticateGmsUserSearch missing HomeCommunity GmsApiKey')
}
result = await authenticateGmsUserPlayground(homeCom.gmsApiKey, context.token, dbUser)
logger.info('authenticateGmsUserSearch=', result)
} else {
throw new LogError('authenticateGmsUserSearch missing valid user login-token')
}
return result
}
@Authorized([RIGHTS.GMS_USER_PLAYGROUND])
@Query(() => UserLocationResult)
async userLocation(@Ctx() context: Context): Promise<UserLocationResult> {
logger.info(`userLocation()...`)
const dbUser = getUser(context)
const result = new UserLocationResult()
if (context.token) {
const homeCom = await getHomeCommunity()
result.communityLocation = Point2Location(homeCom.location as Point)
result.userLocation = Point2Location(dbUser.location as Point)
logger.info('userLocation=', result)
} else {
throw new LogError('userLocation missing valid user login-token')
}
return result
}
@Authorized([RIGHTS.HUMHUB_AUTO_LOGIN])
@Query(() => String)
async authenticateHumhubAutoLogin(
@Ctx() context: Context,
@Arg('project', () => String, { nullable: true }) project?: string | null,
): Promise<string> {
logger.info(`authenticateHumhubAutoLogin()...`)
const dbUser = getUser(context)
const humhubClient = HumHubClient.getInstance()
if (!humhubClient) {
throw new LogError('cannot create humhub client')
}
const userNameLogic = new PublishNameLogic(dbUser)
const username = userNameLogic.getUsername(dbUser.humhubPublishName as PublishNameType)
let humhubUser = await humhubClient.userByUsername(username)
if (!humhubUser) {
humhubUser = await humhubClient.userByEmail(dbUser.emailContact.email)
}
if (!humhubUser) {
throw new LogError("user don't exist (any longer) on humhub")
}
if (humhubUser.account.status !== 1) {
throw new LogError('user status is not 1', humhubUser.account.status)
}
return await humhubClient.createAutoLoginUrl(humhubUser.account.username, project)
}
@Authorized([RIGHTS.SEARCH_ADMIN_USERS])
@Query(() => SearchAdminUsersResult)
async searchAdminUsers(
@Args()
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
): Promise<SearchAdminUsersResult> {
const [users, count] = await DbUser.findAndCount({
relations: ['userRoles'],
where: {
userRoles: { role: In(['admin', 'moderator']) },
},
order: {
createdAt: order,
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
return {
userCount: count,
userList: users.map((user) => {
return {
firstName: user.firstName,
lastName: user.lastName,
role: user.userRoles ? user.userRoles[0].role : '',
}
}),
}
}
@Authorized([RIGHTS.SEARCH_USERS])
@Query(() => SearchUsersResult)
async searchUsers(
@Arg('query', () => String) query: string,
@Arg('filters', () => SearchUsersFilters, { nullable: true })
filters: SearchUsersFilters | null | undefined,
@Args()
{ currentPage = 1, pageSize = 25, order = Order.ASC }: Paginated,
@Ctx() context: Context,
): Promise<SearchUsersResult> {
const clientTimezoneOffset = getClientTimezoneOffset(context)
const userFields = ['id', 'firstName', 'lastName', 'emailId', 'emailContact', 'deletedAt']
const [users, count] = await findUsers(
userFields,
query,
filters ?? null,
currentPage,
pageSize,
order,
)
if (users.length === 0) {
return {
userCount: count,
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(() => String, { nullable: true })
async setUserRole(
@Args() { userId, role }: SetUserRoleArgs,
@Ctx()
context: Context,
): Promise<string | null> {
const user = await DbUser.findOne({
where: { id: userId },
relations: ['userRoles'],
})
// 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')
}
// if user role(s) should be deleted by role=null as parameter
if (role === null) {
await deleteUserRole(user)
} else if (isUserInRole(user, role)) {
throw new LogError('User already has role=', role)
} else {
await setUserRole(user, role)
}
await EVENT_ADMIN_USER_ROLE_SET(user, moderator)
const newUser = await DbUser.findOne({ where: { id: userId }, relations: ['userRoles'] })
return newUser?.userRoles ? newUser.userRoles[0].role : 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({ where: { 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({ where: { 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({ where: { 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
void sendAccountActivationEmail({
firstName: user.firstName,
lastName: user.lastName,
email,
language: user.language,
activationLink: activationLink(user.emailContact.emailVerificationCode),
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
})
await EVENT_EMAIL_ADMIN_CONFIRMATION(user, getUser(context))
return true
}
@Authorized([RIGHTS.USER])
@Query(() => User)
async user(
@Args()
{ identifier, communityIdentifier }: UserArgs,
): Promise<User> {
const foundDbUser = await findUserByIdentifier(identifier, communityIdentifier)
const modelUser = new User(foundDbUser)
return modelUser
}
}
export async function findUserByEmail(email: string): Promise<DbUser> {
const dbUser = await DbUser.findOneOrFail({
where: {
emailContact: { email },
},
withDeleted: true,
relations: { userRoles: true, emailContact: true },
}).catch(() => {
throw new LogError('No user with this credentials', email)
})
return dbUser
}
async function checkEmailExists(email: string): Promise<boolean> {
const userContact = await DbUserContact.findOne({
where: { 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)
}
export function isUserInRole(user: DbUser, role: string | null | undefined): boolean {
if (user && role) {
for (const userRole of user.userRoles) {
if (userRole.role === role) {
return true
}
}
}
return false
}