diff --git a/apps/server/src/modules/user/domain/index.ts b/apps/server/src/modules/user/domain/index.ts new file mode 100644 index 00000000000..d3c8f348a36 --- /dev/null +++ b/apps/server/src/modules/user/domain/index.ts @@ -0,0 +1 @@ +export * from './user'; diff --git a/apps/server/src/modules/user/domain/user.ts b/apps/server/src/modules/user/domain/user.ts new file mode 100644 index 00000000000..118b4aaa933 --- /dev/null +++ b/apps/server/src/modules/user/domain/user.ts @@ -0,0 +1,78 @@ +import { EntityId, LanguageType } from '@shared/domain'; +import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; + +export interface UserProps extends AuthorizableObject { + createdAt?: Date; + updatedAt?: Date; + email: string; + firstName: string; + lastName: string; + roles: EntityId[]; + schoolId: EntityId; + ldapDn?: string; + externalId?: string; + importHash?: string; + firstNameSearchValues?: string[]; + lastNameSearchValues?: string[]; + emailSearchValues?: string[]; + language?: LanguageType; + forcePasswordChange?: boolean; + preferences?: Record; + lastLoginSystemChange?: Date; + outdatedSince?: Date; + previousExternalId?: string; +} + +export class User extends DomainObject { + get email(): string { + return this.props.email; + } + + get firstName(): string { + return this.props.firstName; + } + + get lastName(): string { + return this.props.lastName; + } + + get schoolId(): string { + return this.props.schoolId; + } + + get roles(): EntityId[] { + return this.props.roles; + } + + get ldapDn(): string | undefined { + return this.props.ldapDn; + } + + get externalId(): string | undefined { + return this.props.externalId; + } + + get language(): LanguageType | undefined { + return this.props.language; + } + + get forcePasswordChange(): boolean | undefined { + return this.props.forcePasswordChange; + } + + get preferences(): Record | undefined { + return this.props.preferences; + } + + get lastLoginSystemChange(): Date | undefined { + return this.props.lastLoginSystemChange; + } + + get outdatedSince(): Date | undefined { + return this.props.outdatedSince; + } + + get previousExternalId(): string | undefined { + return this.props.previousExternalId; + } +} diff --git a/apps/server/src/modules/user/repo/index.ts b/apps/server/src/modules/user/repo/index.ts new file mode 100644 index 00000000000..9c279843376 --- /dev/null +++ b/apps/server/src/modules/user/repo/index.ts @@ -0,0 +1,2 @@ +export * from './mapper'; +export * from './user.repo'; diff --git a/apps/server/src/modules/user/repo/mapper/index.ts b/apps/server/src/modules/user/repo/mapper/index.ts new file mode 100644 index 00000000000..74dd8b1ca18 --- /dev/null +++ b/apps/server/src/modules/user/repo/mapper/index.ts @@ -0,0 +1 @@ +export * from './user.mapper'; diff --git a/apps/server/src/modules/user/repo/mapper/school.mapper.ts b/apps/server/src/modules/user/repo/mapper/school.mapper.ts new file mode 100644 index 00000000000..7f75e8dc373 --- /dev/null +++ b/apps/server/src/modules/user/repo/mapper/school.mapper.ts @@ -0,0 +1,24 @@ +import { School as SchoolEntity } from '@shared/domain'; + +export class SchoolMapper { + static mapToDO(entity: SchoolEntity): School { + return new School({ + id: entity.id, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + email: entity.email, + }); + } + + static mapToEntity(domainObject: School): SchoolEntity { + return new SchoolEntity({}); + } + + static mapToDOs(entities: SchoolEntity[]): School[] { + return entities.map((entity) => this.mapToDO(entity)); + } + + static mapToEntities(domainObjects: School[]): SchoolEntity[] { + return domainObjects.map((domainObject) => this.mapToEntity(domainObject)); + } +} diff --git a/apps/server/src/modules/user/repo/mapper/user.mapper.ts b/apps/server/src/modules/user/repo/mapper/user.mapper.ts new file mode 100644 index 00000000000..f9b11f2df00 --- /dev/null +++ b/apps/server/src/modules/user/repo/mapper/user.mapper.ts @@ -0,0 +1,56 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { User as UserEntity } from '@shared/domain'; +import { User } from '../../domain'; + +export class UserMapper { + static mapToDO(entity: UserEntity): User { + return new User({ + id: entity.id, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + email: entity.email, + firstName: entity.firstName, + lastName: entity.lastName, + roles: entity.roles.map((role) => role.id), + school: SchoolMapper.mapToDO(entity.school), + ldapDn: entity.ldapDn, + externalId: entity.externalId, + importHash: entity.importHash, + firstNameSearchValues: entity.firstNameSearchValues, + lastNameSearchValues: entity.lastNameSearchValues, + emailSearchValues: entity.emailSearchValues, + language: entity.language, + forcePasswordChange: entity.forcePasswordChange, + preferences: entity.preferences, + lastLoginSystemChange: entity.lastLoginSystemChange, + outdatedSince: entity.outdatedSince, + previousExternalId: entity.previousExternalId, + }); + } + + static mapToEntity(domainObject: User): UserEntity { + return new UserEntity({ + email: domainObject.email, + firstName: domainObject.firstName, + lastName: domainObject.lastName, + school: new ObjectId(domainObject.school), + roles: domainObject.roles.map((roleId) => new ObjectId(roleId)), + ldapDn: domainObject.ldapDn, + externalId: domainObject.externalId, + language: domainObject.language, + forcePasswordChange: domainObject.forcePasswordChange, + preferences: domainObject.preferences, + lastLoginSystemChange: domainObject.lastLoginSystemChange, + outdatedSince: domainObject.outdatedSince, + previousExternalId: domainObject.previousExternalId, + }); + } + + static mapToDOs(entities: UserEntity[]): User[] { + return entities.map((entity) => this.mapToDO(entity)); + } + + static mapToEntities(domainObjects: User[]): UserEntity[] { + return domainObjects.map((domainObject) => this.mapToEntity(domainObject)); + } +} diff --git a/apps/server/src/modules/user/repo/user.entity.ts b/apps/server/src/modules/user/repo/user.entity.ts new file mode 100644 index 00000000000..09ae4d2a65a --- /dev/null +++ b/apps/server/src/modules/user/repo/user.entity.ts @@ -0,0 +1,134 @@ +import { Collection, Entity, Index, ManyToMany, ManyToOne, Property } from '@mikro-orm/core'; +import { IEntityWithSchool } from '../interface'; +import { BaseEntityWithTimestamps } from './base.entity'; +import { Role } from './role.entity'; +import type { School } from './school.entity'; + +export enum LanguageType { + DE = 'de', + EN = 'en', + ES = 'es', + UK = 'uk', +} + +export interface IUserProperties { + email: string; + firstName: string; + lastName: string; + school: School; + roles: Role[]; + ldapDn?: string; + externalId?: string; + language?: LanguageType; + forcePasswordChange?: boolean; + preferences?: Record; + deletedAt?: Date; + lastLoginSystemChange?: Date; + outdatedSince?: Date; + previousExternalId?: string; +} + +@Entity({ tableName: 'users' }) +@Index({ properties: ['id', 'email'] }) +@Index({ properties: ['firstName', 'lastName'] }) +@Index({ properties: ['externalId', 'school'] }) +@Index({ properties: ['school', 'ldapDn'] }) +@Index({ properties: ['school', 'roles'] }) +export class User extends BaseEntityWithTimestamps implements IEntityWithSchool { + @Property() + @Index() + // @Unique() + email: string; + + @Property() + firstName: string; + + @Property() + lastName: string; + + @Index() + @ManyToMany({ fieldName: 'roles', entity: () => Role }) + roles = new Collection(this); + + @Index() + @ManyToOne('School', { fieldName: 'schoolId' }) + school: School; + + @Property({ nullable: true }) + @Index() + ldapDn?: string; + + @Property({ nullable: true, fieldName: 'ldapId' }) + externalId?: string; + + @Property({ nullable: true }) + previousExternalId?: string; + + @Property({ nullable: true }) + @Index() + importHash?: string; + + @Property({ nullable: true }) + firstNameSearchValues?: string[]; + + @Property({ nullable: true }) + lastNameSearchValues?: string[]; + + @Property({ nullable: true }) + emailSearchValues?: string[]; + + @Property({ nullable: true }) + language?: LanguageType; + + @Property({ nullable: true }) + forcePasswordChange?: boolean; + + @Property({ nullable: true }) + preferences?: Record; + + @Property({ nullable: true }) + @Index() + deletedAt?: Date; + + @Property({ nullable: true }) + lastLoginSystemChange?: Date; + + @Property({ nullable: true }) + outdatedSince?: Date; + + constructor(props: IUserProperties) { + super(); + this.firstName = props.firstName; + this.lastName = props.lastName; + this.email = props.email; + this.school = props.school; + this.roles.set(props.roles); + this.ldapDn = props.ldapDn; + this.externalId = props.externalId; + this.forcePasswordChange = props.forcePasswordChange; + this.language = props.language; + this.preferences = props.preferences ?? {}; + this.deletedAt = props.deletedAt; + this.lastLoginSystemChange = props.lastLoginSystemChange; + this.outdatedSince = props.outdatedSince; + this.previousExternalId = props.previousExternalId; + } + + public resolvePermissions(): string[] { + if (!this.roles.isInitialized(true)) { + throw new Error('Roles items are not loaded.'); + } + + let permissions: string[] = []; + + const roles = this.roles.getItems(); + roles.forEach((role) => { + const rolePermissions = role.resolvePermissions(); + permissions = [...permissions, ...rolePermissions]; + }); + + const uniquePermissions = [...new Set(permissions)]; + + return uniquePermissions; + } +} diff --git a/apps/server/src/modules/user/repo/user.repo.ts b/apps/server/src/modules/user/repo/user.repo.ts new file mode 100644 index 00000000000..72fb5f3b2e8 --- /dev/null +++ b/apps/server/src/modules/user/repo/user.repo.ts @@ -0,0 +1,19 @@ +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { Injectable } from '@nestjs/common'; +import { EntityId, User as UserEntity } from '@shared/domain'; +import { User } from '../domain'; +import { UserMapper } from './mapper'; + +@Injectable() +export class UserRepo { + constructor(private readonly em: EntityManager) {} + + async findById(userId: EntityId): Promise { + const user = await this.em.findOneOrFail( + UserEntity, + { _id: new ObjectId(userId) }, + { populate: ['school', 'roles'] } + ); + return UserMapper.mapToDO(user); + } +}