mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge pull request #2797 from gradido/events-user
feat(backend): events for users
This commit is contained in:
commit
5542989063
6
backend/src/event/EVENT_ADMIN_USER_DELETE.ts
Normal file
6
backend/src/event/EVENT_ADMIN_USER_DELETE.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { Event as DbEvent } from '@entity/Event'
|
||||
import { Event, EventType } from './Event'
|
||||
|
||||
export const EVENT_ADMIN_USER_DELETE = async (user: DbUser, moderator: DbUser): Promise<DbEvent> =>
|
||||
Event(EventType.ADMIN_USER_DELETE, user, moderator).save()
|
||||
8
backend/src/event/EVENT_ADMIN_USER_ROLE_SET.ts
Normal file
8
backend/src/event/EVENT_ADMIN_USER_ROLE_SET.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { Event as DbEvent } from '@entity/Event'
|
||||
import { Event, EventType } from './Event'
|
||||
|
||||
export const EVENT_ADMIN_USER_ROLE_SET = async (
|
||||
user: DbUser,
|
||||
moderator: DbUser,
|
||||
): Promise<DbEvent> => Event(EventType.ADMIN_USER_ROLE_SET, user, moderator).save()
|
||||
8
backend/src/event/EVENT_ADMIN_USER_UNDELETE.ts
Normal file
8
backend/src/event/EVENT_ADMIN_USER_UNDELETE.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { Event as DbEvent } from '@entity/Event'
|
||||
import { Event, EventType } from './Event'
|
||||
|
||||
export const EVENT_ADMIN_USER_UNDELETE = async (
|
||||
user: DbUser,
|
||||
moderator: DbUser,
|
||||
): Promise<DbEvent> => Event(EventType.ADMIN_USER_UNDELETE, user, moderator).save()
|
||||
6
backend/src/event/EVENT_EMAIL_FORGOT_PASSWORD.ts
Normal file
6
backend/src/event/EVENT_EMAIL_FORGOT_PASSWORD.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { Event as DbEvent } from '@entity/Event'
|
||||
import { Event, EventType } from './Event'
|
||||
|
||||
export const EVENT_EMAIL_FORGOT_PASSWORD = async (user: DbUser): Promise<DbEvent> =>
|
||||
Event(EventType.EMAIL_FORGOT_PASSWORD, user, { id: 0 } as DbUser).save()
|
||||
6
backend/src/event/EVENT_LOGOUT.ts
Normal file
6
backend/src/event/EVENT_LOGOUT.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { Event as DbEvent } from '@entity/Event'
|
||||
import { Event, EventType } from './Event'
|
||||
|
||||
export const EVENT_LOGOUT = async (user: DbUser): Promise<DbEvent> =>
|
||||
Event(EventType.LOGOUT, user, user).save()
|
||||
6
backend/src/event/EVENT_USER_INFO_UPDATE.ts
Normal file
6
backend/src/event/EVENT_USER_INFO_UPDATE.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { Event as DbEvent } from '@entity/Event'
|
||||
import { Event, EventType } from './Event'
|
||||
|
||||
export const EVENT_USER_INFO_UPDATE = async (user: DbUser): Promise<DbEvent> =>
|
||||
Event(EventType.USER_INFO_UPDATE, user, user).save()
|
||||
@ -47,12 +47,17 @@ export { EVENT_ADMIN_CONTRIBUTION_LINK_DELETE } from './EVENT_ADMIN_CONTRIBUTION
|
||||
export { EVENT_ADMIN_CONTRIBUTION_LINK_UPDATE } from './EVENT_ADMIN_CONTRIBUTION_LINK_UPDATE'
|
||||
export { EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE } from './EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE'
|
||||
export { EVENT_ADMIN_SEND_CONFIRMATION_EMAIL } from './EVENT_ADMIN_SEND_CONFIRMATION_EMAIL'
|
||||
export { EVENT_ADMIN_USER_DELETE } from './EVENT_ADMIN_USER_DELETE'
|
||||
export { EVENT_ADMIN_USER_UNDELETE } from './EVENT_ADMIN_USER_UNDELETE'
|
||||
export { EVENT_ADMIN_USER_ROLE_SET } from './EVENT_ADMIN_USER_ROLE_SET'
|
||||
export { EVENT_CONTRIBUTION_CREATE } from './EVENT_CONTRIBUTION_CREATE'
|
||||
export { EVENT_CONTRIBUTION_DELETE } from './EVENT_CONTRIBUTION_DELETE'
|
||||
export { EVENT_CONTRIBUTION_UPDATE } from './EVENT_CONTRIBUTION_UPDATE'
|
||||
export { EVENT_CONTRIBUTION_MESSAGE_CREATE } from './EVENT_CONTRIBUTION_MESSAGE_CREATE'
|
||||
export { EVENT_CONTRIBUTION_LINK_REDEEM } from './EVENT_CONTRIBUTION_LINK_REDEEM'
|
||||
export { EVENT_EMAIL_FORGOT_PASSWORD } from './EVENT_EMAIL_FORGOT_PASSWORD'
|
||||
export { EVENT_LOGIN } from './EVENT_LOGIN'
|
||||
export { EVENT_LOGOUT } from './EVENT_LOGOUT'
|
||||
export { EVENT_REGISTER } from './EVENT_REGISTER'
|
||||
export { EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL } from './EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL'
|
||||
export { EVENT_SEND_CONFIRMATION_EMAIL } from './EVENT_SEND_CONFIRMATION_EMAIL'
|
||||
@ -61,3 +66,4 @@ export { EVENT_TRANSACTION_RECEIVE } from './EVENT_TRANSACTION_RECEIVE'
|
||||
export { EVENT_TRANSACTION_LINK_CREATE } from './EVENT_TRANSACTION_LINK_CREATE'
|
||||
export { EVENT_TRANSACTION_LINK_DELETE } from './EVENT_TRANSACTION_LINK_DELETE'
|
||||
export { EVENT_TRANSACTION_LINK_REDEEM } from './EVENT_TRANSACTION_LINK_REDEEM'
|
||||
export { EVENT_USER_INFO_UPDATE } from './EVENT_USER_INFO_UPDATE'
|
||||
|
||||
@ -11,12 +11,17 @@ export enum EventType {
|
||||
ADMIN_CONTRIBUTION_LINK_UPDATE = 'ADMIN_CONTRIBUTION_LINK_UPDATE',
|
||||
ADMIN_CONTRIBUTION_MESSAGE_CREATE = 'ADMIN_CONTRIBUTION_MESSAGE_CREATE',
|
||||
ADMIN_SEND_CONFIRMATION_EMAIL = 'ADMIN_SEND_CONFIRMATION_EMAIL',
|
||||
ADMIN_USER_DELETE = 'ADMIN_USER_DELETE',
|
||||
ADMIN_USER_UNDELETE = 'ADMIN_USER_UNDELETE',
|
||||
ADMIN_USER_ROLE_SET = 'ADMIN_USER_ROLE_SET',
|
||||
CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE',
|
||||
CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE',
|
||||
CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE',
|
||||
CONTRIBUTION_MESSAGE_CREATE = 'CONTRIBUTION_MESSAGE_CREATE',
|
||||
CONTRIBUTION_LINK_REDEEM = 'CONTRIBUTION_LINK_REDEEM',
|
||||
EMAIL_FORGOT_PASSWORD = 'EMAIL_FORGOT_PASSWORD',
|
||||
LOGIN = 'LOGIN',
|
||||
LOGOUT = 'LOGOUT',
|
||||
REGISTER = 'REGISTER',
|
||||
REDEEM_REGISTER = 'REDEEM_REGISTER',
|
||||
SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTIREGISTRATION_EMAIL',
|
||||
@ -26,6 +31,7 @@ export enum EventType {
|
||||
TRANSACTION_LINK_CREATE = 'TRANSACTION_LINK_CREATE',
|
||||
TRANSACTION_LINK_DELETE = 'TRANSACTION_LINK_DELETE',
|
||||
TRANSACTION_LINK_REDEEM = 'TRANSACTION_LINK_REDEEM',
|
||||
USER_INFO_UPDATE = 'USER_INFO_UPDATE',
|
||||
// VISIT_GRADIDO = 'VISIT_GRADIDO',
|
||||
// VERIFY_REDEEM = 'VERIFY_REDEEM',
|
||||
// INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
|
||||
|
||||
@ -868,6 +868,20 @@ describe('UserResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the LOGOUT event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.LOGOUT,
|
||||
affectedUserId: userConatct.user.id,
|
||||
actingUserId: userConatct.user.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1023,6 +1037,20 @@ describe('UserResolver', () => {
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
it('stores the EMAIL_FORGOT_PASSWORD event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.EMAIL_FORGOT_PASSWORD,
|
||||
affectedUserId: userConatct.user.id,
|
||||
actingUserId: 0,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('request reset password again', () => {
|
||||
@ -1147,6 +1175,20 @@ describe('UserResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the USER_INFO_UPDATE event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.USER_INFO_UPDATE,
|
||||
affectedUserId: userConatct.user.id,
|
||||
actingUserId: userConatct.user.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('language is not valid', () => {
|
||||
@ -1517,6 +1559,24 @@ describe('UserResolver', () => {
|
||||
)
|
||||
expect(new Date(result.data.setUserRole)).toEqual(expect.any(Date))
|
||||
})
|
||||
|
||||
it('stores the ADMIN_USER_ROLE_SET event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const adminConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'peter@lustig.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.ADMIN_USER_ROLE_SET,
|
||||
affectedUserId: userConatct.user.id,
|
||||
actingUserId: adminConatct.user.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('to usual user', () => {
|
||||
@ -1702,6 +1762,24 @@ describe('UserResolver', () => {
|
||||
expect(new Date(result.data.deleteUser)).toEqual(expect.any(Date))
|
||||
})
|
||||
|
||||
it('stores the ADMIN_USER_DELETE event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'], withDeleted: true },
|
||||
)
|
||||
const adminConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'peter@lustig.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.ADMIN_USER_DELETE,
|
||||
affectedUserId: userConatct.user.id,
|
||||
actingUserId: adminConatct.user.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('delete deleted user', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
@ -1977,6 +2055,24 @@ describe('UserResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the ADMIN_USER_UNDELETE event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const adminConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'peter@lustig.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.ADMIN_USER_UNDELETE,
|
||||
affectedUserId: userConatct.user.id,
|
||||
actingUserId: adminConatct.user.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -61,6 +61,12 @@ import {
|
||||
EVENT_REGISTER,
|
||||
EVENT_ACTIVATE_ACCOUNT,
|
||||
EVENT_ADMIN_SEND_CONFIRMATION_EMAIL,
|
||||
EVENT_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 { getUserCreations } from './util/creations'
|
||||
import { isValidPassword } from '@/password/EncryptorUtils'
|
||||
@ -189,15 +195,9 @@ export class UserResolver {
|
||||
|
||||
@Authorized([RIGHTS.LOGOUT])
|
||||
@Mutation(() => Boolean)
|
||||
logout(): boolean {
|
||||
// TODO: Event still missing here!!
|
||||
// TODO: We dont need this anymore, but might need this in the future in oder to invalidate a valid JWT-Token.
|
||||
// Furthermore this hook can be useful for tracking user behaviour (did he logout or not? Warn him if he didn't on next login)
|
||||
// The functionality is fully client side - the client just needs to delete his token with the current implementation.
|
||||
// we could try to force this by sending `token: null` or `token: ''` with this call. But since it bares no real security
|
||||
// we should just return true for now.
|
||||
logger.info('Logout...')
|
||||
// remove user.pubKey from logger-context to ensure a correct filter on log-messages belonging to the same user
|
||||
async logout(@Ctx() context: Context): Promise<boolean> {
|
||||
await EVENT_LOGOUT(getUser(context))
|
||||
// remove user from logger context
|
||||
logger.addContext('user', 'unknown')
|
||||
return true
|
||||
}
|
||||
@ -411,6 +411,7 @@ export class UserResolver {
|
||||
)
|
||||
}
|
||||
logger.info(`forgotPassword(${email}) successful...`)
|
||||
await EVENT_EMAIL_FORGOT_PASSWORD(user)
|
||||
|
||||
return true
|
||||
}
|
||||
@ -473,8 +474,6 @@ export class UserResolver {
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info('User and UserContact data written successfully...')
|
||||
|
||||
await EVENT_ACTIVATE_ACCOUNT(user)
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
throw new LogError('Error on writing User and User Contact data', e)
|
||||
@ -492,13 +491,9 @@ export class UserResolver {
|
||||
)
|
||||
} catch (e) {
|
||||
logger.error('Error subscribing to klicktipp', e)
|
||||
// TODO is this a problem?
|
||||
// eslint-disable-next-line no-console
|
||||
/* uncomment this, when you need the activation link on the console
|
||||
console.log('Could not subscribe to klicktipp')
|
||||
*/
|
||||
}
|
||||
}
|
||||
await EVENT_ACTIVATE_ACCOUNT(user)
|
||||
|
||||
return true
|
||||
}
|
||||
@ -535,21 +530,21 @@ export class UserResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
logger.info(`updateUserInfos(${firstName}, ${lastName}, ${language}, ***, ***)...`)
|
||||
const userEntity = getUser(context)
|
||||
const user = getUser(context)
|
||||
|
||||
if (firstName) {
|
||||
userEntity.firstName = firstName
|
||||
user.firstName = firstName
|
||||
}
|
||||
|
||||
if (lastName) {
|
||||
userEntity.lastName = lastName
|
||||
user.lastName = lastName
|
||||
}
|
||||
|
||||
if (language) {
|
||||
if (!isLanguage(language)) {
|
||||
throw new LogError('Given language is not a valid language', language)
|
||||
}
|
||||
userEntity.language = language
|
||||
user.language = language
|
||||
i18n.setLocale(language)
|
||||
}
|
||||
|
||||
@ -561,22 +556,22 @@ export class UserResolver {
|
||||
)
|
||||
}
|
||||
|
||||
if (!verifyPassword(userEntity, password)) {
|
||||
if (!verifyPassword(user, password)) {
|
||||
throw new LogError(`Old password is invalid`)
|
||||
}
|
||||
|
||||
// Save new password hash and newly encrypted private key
|
||||
userEntity.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
|
||||
userEntity.password = encryptPassword(userEntity, passwordNew)
|
||||
user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
|
||||
user.password = encryptPassword(user, passwordNew)
|
||||
}
|
||||
|
||||
// Save hideAmountGDD value
|
||||
if (hideAmountGDD !== undefined) {
|
||||
userEntity.hideAmountGDD = hideAmountGDD
|
||||
user.hideAmountGDD = hideAmountGDD
|
||||
}
|
||||
// Save hideAmountGDT value
|
||||
if (hideAmountGDT !== undefined) {
|
||||
userEntity.hideAmountGDT = hideAmountGDT
|
||||
user.hideAmountGDT = hideAmountGDT
|
||||
}
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
@ -584,7 +579,7 @@ export class UserResolver {
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
|
||||
try {
|
||||
await queryRunner.manager.save(userEntity).catch((error) => {
|
||||
await queryRunner.manager.save(user).catch((error) => {
|
||||
throw new LogError('Error saving user', error)
|
||||
})
|
||||
|
||||
@ -597,6 +592,8 @@ export class UserResolver {
|
||||
await queryRunner.release()
|
||||
}
|
||||
logger.info('updateUserInfos() successfully finished...')
|
||||
await EVENT_USER_INFO_UPDATE(user)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@ -722,8 +719,8 @@ export class UserResolver {
|
||||
throw new LogError('Could not find user with given ID', userId)
|
||||
}
|
||||
// administrator user changes own role?
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === userId) {
|
||||
const moderator = getUser(context)
|
||||
if (moderator.id === userId) {
|
||||
throw new LogError('Administrator can not change his own role')
|
||||
}
|
||||
// change isAdmin
|
||||
@ -744,6 +741,7 @@ export class UserResolver {
|
||||
break
|
||||
}
|
||||
await user.save()
|
||||
await EVENT_ADMIN_USER_ROLE_SET(user, moderator)
|
||||
const newUser = await DbUser.findOne({ id: userId })
|
||||
return newUser ? newUser.isAdmin : null
|
||||
}
|
||||
@ -760,19 +758,23 @@ export class UserResolver {
|
||||
throw new LogError('Could not find user with given ID', userId)
|
||||
}
|
||||
// moderator user disabled own account?
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === userId) {
|
||||
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): Promise<Date | null> {
|
||||
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)
|
||||
@ -781,6 +783,7 @@ export class UserResolver {
|
||||
throw new LogError('User is not deleted')
|
||||
}
|
||||
await user.recover()
|
||||
await EVENT_ADMIN_USER_UNDELETE(user, getUser(context))
|
||||
return null
|
||||
}
|
||||
|
||||
@ -814,9 +817,8 @@ export class UserResolver {
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
logger.info(`Account confirmation link: ${activationLink}`)
|
||||
} else {
|
||||
await EVENT_ADMIN_SEND_CONFIRMATION_EMAIL(user, getUser(context))
|
||||
}
|
||||
await EVENT_ADMIN_SEND_CONFIRMATION_EMAIL(user, getUser(context))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user