From d81fc53819b42f9c5bd31de20d26e9bf4f4b61b3 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Fri, 30 Jun 2023 21:29:34 +0300 Subject: [PATCH] more auth stuff --- apps/auth/src/auth.controller.ts | 13 +++- apps/auth/src/auth.module.ts | 17 +++- apps/auth/src/config/security.config.ts | 1 + .../src/database/entities/privilege.entity.ts | 18 +++++ .../auth/src/database/entities/role.entity.ts | 54 +++++++++++++ .../database/entities/user-token.entity.ts | 2 + .../auth/src/database/entities/user.entity.ts | 38 ++++++++- .../database/migrations/20230630153343_ban.ts | 2 +- .../20230630160302_user-privilege.ts | 1 + .../migrations/20230630160541_role.ts | 1 + .../migrations/20230630160546_user-role.ts | 1 + .../20230630160552_role-privilege.ts | 1 + .../database/seeds/0002-initial-privileges.ts | 15 +++- .../interfaces/token-response.interface.ts | 4 + apps/auth/src/services/auth.service.ts | 46 ++++++++++- apps/auth/src/services/jwt.service.ts | 4 +- apps/auth/src/services/role.service.ts | 78 +++++++++++++++++++ .../src/decorators/user.decorator.ts | 8 ++ .../src/guards/auth.guard.ts | 30 +++++++ apps/freeblox-web-service/src/main.ts | 1 + .../src/services/auth/auth.controller.ts | 23 +++++- .../src/services/auth/dtos/user.dto.ts | 33 ++++++++ libs/shared/src/database/metaentity.ts | 12 ++- 23 files changed, 389 insertions(+), 14 deletions(-) create mode 100644 apps/auth/src/database/entities/privilege.entity.ts create mode 100644 apps/auth/src/database/entities/role.entity.ts create mode 100644 apps/auth/src/interfaces/token-response.interface.ts create mode 100644 apps/auth/src/services/role.service.ts create mode 100644 apps/freeblox-web-service/src/decorators/user.decorator.ts create mode 100644 apps/freeblox-web-service/src/guards/auth.guard.ts create mode 100644 apps/freeblox-web-service/src/services/auth/dtos/user.dto.ts diff --git a/apps/auth/src/auth.controller.ts b/apps/auth/src/auth.controller.ts index b87f312..c571b68 100644 --- a/apps/auth/src/auth.controller.ts +++ b/apps/auth/src/auth.controller.ts @@ -2,6 +2,7 @@ import { Controller } from '@nestjs/common'; import { AuthService } from './services/auth.service'; import { MessagePattern } from '@nestjs/microservices'; import { LoginRequest } from './interfaces/auth.interface'; +import { UserInfo } from '@freeblox/shared'; @Controller() export class AuthController { @@ -14,6 +15,16 @@ export class AuthController { @MessagePattern('auth.verify') verify({ token }: { token: string }) { - return this.authService.getUserFromToken(token); + return this.authService.verifyToken(token); + } + + @MessagePattern('auth.getUserById') + getUserById({ id }: { id: string }) { + return this.authService.getUserById(id); + } + + @MessagePattern('auth.getUserBans') + getUserBans({ user }: { user: UserInfo }) { + return this.authService.getUserBans(user); } } diff --git a/apps/auth/src/auth.module.ts b/apps/auth/src/auth.module.ts index 3a45c75..fb1d88f 100644 --- a/apps/auth/src/auth.module.ts +++ b/apps/auth/src/auth.module.ts @@ -14,6 +14,17 @@ import { UserTokenEntity } from './database/entities/user-token.entity'; import { OTPService } from './services/otp.service'; import { BanEntity } from './database/entities/ban.entity'; import { BanService } from './services/ban.service'; +import { PrivilegeEntity } from './database/entities/privilege.entity'; +import { RoleService } from './services/role.service'; +import { RoleEntity } from './database/entities/role.entity'; + +const entities = [ + UserEntity, + UserTokenEntity, + BanEntity, + PrivilegeEntity, + RoleEntity, +]; @Module({ imports: [ @@ -26,9 +37,10 @@ import { BanService } from './services/ban.service'; inject: [ConfigService], useFactory: (config: ConfigService) => ({ ...config.get('typeorm'), + entities, }), }), - TypeOrmModule.forFeature([UserEntity, UserTokenEntity, BanEntity]), + TypeOrmModule.forFeature(entities), ClientsModule.register([natsClient('auth')]), ], controllers: [AuthController], @@ -36,8 +48,9 @@ import { BanService } from './services/ban.service'; ...keysProviders, JWTService, OTPService, - AuthService, BanService, + RoleService, + AuthService, ], }) export class AuthModule implements OnModuleInit { diff --git a/apps/auth/src/config/security.config.ts b/apps/auth/src/config/security.config.ts index 02100f5..cb6cf48 100644 --- a/apps/auth/src/config/security.config.ts +++ b/apps/auth/src/config/security.config.ts @@ -3,6 +3,7 @@ import { resolve } from 'path'; export const security = registerAs('security', () => ({ algorithm: String(process.env.JWT_ALGORITHM || 'RS512'), + tokenExpiry: Number(process.env.JWT_EXPIRY) || 60 * 60, privateKeyPath: resolve(String(process.env.PRIVATE_KEY_FILE)), publicKeyPath: resolve(String(process.env.PUBLIC_KEY_FILE)), })); diff --git a/apps/auth/src/database/entities/privilege.entity.ts b/apps/auth/src/database/entities/privilege.entity.ts new file mode 100644 index 0000000..d666ecb --- /dev/null +++ b/apps/auth/src/database/entities/privilege.entity.ts @@ -0,0 +1,18 @@ +import { UserMetaEntity } from '@freeblox/shared'; +import { Exclude, Expose } from 'class-transformer'; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('privileges') +@Exclude() +export class PrivilegeEntity extends UserMetaEntity { + @PrimaryGeneratedColumn() + @Expose() + id: number; + + @Column() + @Expose() + privilege: string; + + @Column({ default: false }) + automatic: boolean; +} diff --git a/apps/auth/src/database/entities/role.entity.ts b/apps/auth/src/database/entities/role.entity.ts new file mode 100644 index 0000000..5ce4ccf --- /dev/null +++ b/apps/auth/src/database/entities/role.entity.ts @@ -0,0 +1,54 @@ +import { UserMetaEntity } from '@freeblox/shared'; +import { Exclude, Expose } from 'class-transformer'; +import { + Column, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + PrimaryGeneratedColumn, + Tree, + TreeChildren, + TreeParent, +} from 'typeorm'; +import { PrivilegeEntity } from './privilege.entity'; + +@Entity('roles') +@Exclude() +@Tree('materialized-path') +export class RoleEntity extends UserMetaEntity { + @PrimaryGeneratedColumn() + @Expose() + id: number; + + @Column() + @Expose() + role: string; + + @Column({ name: 'parent_id' }) + @Expose() + parentId: number; + + @TreeParent() + @JoinColumn({ name: 'parent_id' }) + parent: RoleEntity; + + @TreeChildren() + children: RoleEntity[]; + + @Column({ default: false }) + automatic: boolean; + + @ManyToMany(() => PrivilegeEntity, { eager: true }) + @Expose() + @JoinTable({ + name: 'role_privilege', + joinColumn: { + name: 'role_id', + }, + inverseJoinColumn: { + name: 'privilege_id', + }, + }) + privileges: PrivilegeEntity[]; +} diff --git a/apps/auth/src/database/entities/user-token.entity.ts b/apps/auth/src/database/entities/user-token.entity.ts index f7d60d5..98a3e55 100644 --- a/apps/auth/src/database/entities/user-token.entity.ts +++ b/apps/auth/src/database/entities/user-token.entity.ts @@ -5,6 +5,7 @@ import { Column, CreateDateColumn, ManyToOne, + JoinColumn, } from 'typeorm'; import { UserEntity } from './user.entity'; @@ -29,5 +30,6 @@ export class UserTokenEntity { created_at: Date; @ManyToOne(() => UserEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) user: UserEntity; } diff --git a/apps/auth/src/database/entities/user.entity.ts b/apps/auth/src/database/entities/user.entity.ts index 91b427b..86c8a11 100644 --- a/apps/auth/src/database/entities/user.entity.ts +++ b/apps/auth/src/database/entities/user.entity.ts @@ -1,10 +1,18 @@ import { MetaEntity } from '@freeblox/shared'; import { Exclude, Expose } from 'class-transformer'; import { IsOptional, IsString } from 'class-validator'; -import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { + Column, + Entity, + JoinTable, + ManyToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { PrivilegeEntity } from './privilege.entity'; +import { RoleEntity } from './role.entity'; -@Entity('user_entity') @Expose() +@Entity('users') export class UserEntity extends MetaEntity { @PrimaryGeneratedColumn('uuid') @IsString() @@ -55,4 +63,30 @@ export class UserEntity extends MetaEntity { @IsString() @IsOptional() loginAt: Date; + + @ManyToMany(() => PrivilegeEntity) + @JoinTable({ + name: 'user_privilege', + joinColumn: { + name: 'user_id', + }, + inverseJoinColumn: { + name: 'privilege_id', + }, + }) + @Exclude() + privileges: PrivilegeEntity[]; + + @ManyToMany(() => RoleEntity) + @JoinTable({ + name: 'user_role', + joinColumn: { + name: 'user_id', + }, + inverseJoinColumn: { + name: 'role_id', + }, + }) + @Exclude() + roles: RoleEntity[]; } diff --git a/apps/auth/src/database/migrations/20230630153343_ban.ts b/apps/auth/src/database/migrations/20230630153343_ban.ts index b61e383..423b17b 100644 --- a/apps/auth/src/database/migrations/20230630153343_ban.ts +++ b/apps/auth/src/database/migrations/20230630153343_ban.ts @@ -7,7 +7,7 @@ export async function up(knex: Knex): Promise { table.text('reason').notNullable(); table.string('ip', 255).nullable(); - table.integer('cidr', 2).nullable().unsigned(); + table.integer('cidr', 2).nullable().defaultTo(32).unsigned(); table.uuid('user_id').nullable(); table.uuid('admin_id').nullable(); diff --git a/apps/auth/src/database/migrations/20230630160302_user-privilege.ts b/apps/auth/src/database/migrations/20230630160302_user-privilege.ts index 746ab8e..8ae4b0c 100644 --- a/apps/auth/src/database/migrations/20230630160302_user-privilege.ts +++ b/apps/auth/src/database/migrations/20230630160302_user-privilege.ts @@ -2,6 +2,7 @@ import { Knex } from 'knex'; export async function up(knex: Knex): Promise { return knex.schema.createTable('user_privilege', (table) => { + table.increments('id').primary(); table.integer('privilege_id').nullable().unsigned(); table.uuid('user_id').nullable(); table diff --git a/apps/auth/src/database/migrations/20230630160541_role.ts b/apps/auth/src/database/migrations/20230630160541_role.ts index fcf0741..d459a47 100644 --- a/apps/auth/src/database/migrations/20230630160541_role.ts +++ b/apps/auth/src/database/migrations/20230630160541_role.ts @@ -4,6 +4,7 @@ export async function up(knex: Knex): Promise { return knex.schema.createTable('roles', (table) => { table.increments('id').primary(); table.text('role').notNullable(); + table.string('mpath').nullable().defaultTo(''); table.integer('parent_id').nullable().unsigned(); table.boolean('automatic').defaultTo(false); diff --git a/apps/auth/src/database/migrations/20230630160546_user-role.ts b/apps/auth/src/database/migrations/20230630160546_user-role.ts index d6ef87a..3b52d91 100644 --- a/apps/auth/src/database/migrations/20230630160546_user-role.ts +++ b/apps/auth/src/database/migrations/20230630160546_user-role.ts @@ -2,6 +2,7 @@ import { Knex } from 'knex'; export async function up(knex: Knex): Promise { return knex.schema.createTable('user_role', (table) => { + table.increments('id').primary(); table.integer('role_id').nullable().unsigned(); table.uuid('user_id').nullable(); table.foreign('role_id').references('roles.id').onDelete('CASCADE'); diff --git a/apps/auth/src/database/migrations/20230630160552_role-privilege.ts b/apps/auth/src/database/migrations/20230630160552_role-privilege.ts index 21a30e2..9e5461e 100644 --- a/apps/auth/src/database/migrations/20230630160552_role-privilege.ts +++ b/apps/auth/src/database/migrations/20230630160552_role-privilege.ts @@ -2,6 +2,7 @@ import { Knex } from 'knex'; export async function up(knex: Knex): Promise { return knex.schema.createTable('role_privilege', (table) => { + table.increments('id').primary(); table.integer('role_id').nullable().unsigned(); table.integer('privilege_id').nullable().unsigned(); table.foreign('role_id').references('roles.id').onDelete('CASCADE'); diff --git a/apps/auth/src/database/seeds/0002-initial-privileges.ts b/apps/auth/src/database/seeds/0002-initial-privileges.ts index 3125326..0b29c46 100644 --- a/apps/auth/src/database/seeds/0002-initial-privileges.ts +++ b/apps/auth/src/database/seeds/0002-initial-privileges.ts @@ -6,25 +6,30 @@ export async function seed(knex: Knex): Promise { name: 'player', automatic: true, id: 0, + path: '', }, { name: 'member', id: 0, parent: 'player', + path: '', }, { name: 'moderator', id: 0, parent: 'member', + path: '', }, { name: 'admin', id: 0, parent: 'moderator', + path: '', }, { name: 'reduced', id: 0, + path: '', }, ]; @@ -105,7 +110,10 @@ export async function seed(knex: Knex): Promise { const findRole = initialRoles.find( (parent) => parent.name === role.parent, ); - parentId = findRole?.id; + if (findRole) { + parentId = findRole.id; + role.path += findRole.path; + } } const [created] = await knex('roles') @@ -113,12 +121,17 @@ export async function seed(knex: Knex): Promise { { role: role.name, automatic: role?.automatic, + mpath: role.path, parent_id: parentId, created_at: new Date(), }, ]) .returning(['id']); + role.id = created.id; + role.path += `${role.path ? '.' : ''}${role.id}`; + + await knex('roles').where({ id: role.id }).update({ mpath: role.path }); } for (const privilege of initialPrivileges) { diff --git a/apps/auth/src/interfaces/token-response.interface.ts b/apps/auth/src/interfaces/token-response.interface.ts new file mode 100644 index 0000000..bf6d6d6 --- /dev/null +++ b/apps/auth/src/interfaces/token-response.interface.ts @@ -0,0 +1,4 @@ +export interface TokenResponse { + token: string; + expires_in: number; +} diff --git a/apps/auth/src/services/auth.service.ts b/apps/auth/src/services/auth.service.ts index 2818da5..67156a2 100644 --- a/apps/auth/src/services/auth.service.ts +++ b/apps/auth/src/services/auth.service.ts @@ -14,6 +14,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { OTPService } from './otp.service'; import { BanService } from './ban.service'; import { UserInfo } from '@freeblox/shared'; +import { RoleService } from './role.service'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class AuthService { @@ -21,8 +23,10 @@ export class AuthService { private readonly jwtService: JWTService, private readonly otpService: OTPService, private readonly banService: BanService, + private readonly roleService: RoleService, @InjectRepository(UserEntity) private readonly userRepository: Repository, + private readonly config: ConfigService, ) {} /** @@ -78,17 +82,24 @@ export class AuthService { } } + // Check for active ban const bans = await this.banService.getActiveBansForUser(userEntity); const banned = !!bans.length; + // Get all privileges applicable to user + const privileges = await this.roleService.getUserPrivileges(userEntity); + // Issue token + const exp = this.config.get('security.tokenExpiry'); const issuedToken = await this.jwtService.sign({ sub: userEntity.id, username: userEntity.username, display_name: userEntity.displayName, language: userEntity.language, banned: banned, - privileges: banned ? [] : ['freeblox'], + privileges: banned + ? [] + : privileges.map((privilege) => privilege.privilege), }); // Set login time to now @@ -97,16 +108,19 @@ export class AuthService { { loginAt: new Date() }, ); - return issuedToken; + return { + token: issuedToken, + expires_in: exp, + }; } /** - * Validate user token + * Get user from token * @param token JWT Token * @returns User entity */ async getUserFromToken(token: string) { - const tokenInfo = await this.jwtService.verify(token); + const tokenInfo = await this.verifyToken(token); const user = await this.userRepository.findOneByOrFail({ id: tokenInfo.sub, activated: true, @@ -114,6 +128,30 @@ export class AuthService { return instanceToPlain(user); } + /** + * Verify user token + * @param token JWT token + * @returns User token info + */ + async verifyToken(token: string) { + try { + return await this.jwtService.verify(token); + } catch (e) { + console.error(token, e); + throw new ForbiddenException('Invalid token'); + } + } + + /** + * Get user entity by ID + * @param id User ID + * @returns User entity + */ + async getUserById(id: string) { + if (!id) throw new BadRequestException('ID is required'); + return instanceToPlain(await this.userRepository.findOneByOrFail({ id })); + } + /** * Get user bans * @param tokeninfo diff --git a/apps/auth/src/services/jwt.service.ts b/apps/auth/src/services/jwt.service.ts index c487fd5..b880f5c 100644 --- a/apps/auth/src/services/jwt.service.ts +++ b/apps/auth/src/services/jwt.service.ts @@ -13,12 +13,14 @@ export class JWTService { async sign(data: Record, audience = 'urn:freeblox:service') { const alg = this.config.get('security.algorithm'); + const exp = + this.config.get('security.tokenExpiry') + Math.floor(Date.now() / 1000); const jwt = await new SignJWT(data) .setProtectedHeader({ alg }) .setIssuedAt() .setIssuer('urn:freeblox:auth') .setAudience(audience) - .setExpirationTime('8d') + .setExpirationTime(exp) .sign(this.privateKey); return jwt; } diff --git a/apps/auth/src/services/role.service.ts b/apps/auth/src/services/role.service.ts new file mode 100644 index 0000000..1d03269 --- /dev/null +++ b/apps/auth/src/services/role.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { EntityManager, Repository } from 'typeorm'; +import { UserEntity } from '../database/entities/user.entity'; +import { RoleEntity } from '../database/entities/role.entity'; +import { PrivilegeEntity } from '../database/entities/privilege.entity'; + +@Injectable() +export class RoleService { + constructor( + @InjectEntityManager() private manager: EntityManager, + @InjectRepository(UserEntity) + private readonly userRepository: Repository, + ) {} + + /** + * Get all applicable privileges for roles + * @param roles Roles to look for privileges for + * @param startingPrivileges Privileges already included + * @returns Unique list of privileges + */ + async getApplicablePrivileges( + roles: RoleEntity[], + startingPrivileges: PrivilegeEntity[] = [], + ) { + const roleTree = await this.manager + .getTreeRepository(RoleEntity) + .findTrees({ relations: ['privileges'] }); + + const privileges: PrivilegeEntity[] = [...startingPrivileges]; + for (const { id } of roles) { + for (const role of roleTree) { + const [result, list] = this.accumulatePrivilegesFromTree(role, id); + if (!result) continue; + privileges.push(...list); + break; + } + } + + return privileges.filter( + (value, index, array) => + array.findIndex((entry) => entry.id === value.id) === index, + ); + } + + /** + * Get all applicable privileges for user. + * @param user User + * @returns Unique privilege list + */ + async getUserPrivileges(user: UserEntity) { + if (!user.privileges || !user.roles) { + user = await this.userRepository.findOne({ + where: { id: user.id }, + relations: ['privileges', 'roles'], + }); + } + + return this.getApplicablePrivileges(user.roles, user.privileges); + } + + /** + * Accumulate privileges from a tree structure of roles. + * @param root Node to start from + * @param target Target role ID + * @returns `[, ]` + */ + private accumulatePrivilegesFromTree(root: RoleEntity, target: number) { + const privileges: PrivilegeEntity[] = [...root.privileges]; + if (root.id === target) return [root, privileges]; + for (const child of root.children || []) { + const [found, list] = this.accumulatePrivilegesFromTree(child, target); + if (found) return [found, [...privileges, ...list]]; + continue; + } + return [null, []]; + } +} diff --git a/apps/freeblox-web-service/src/decorators/user.decorator.ts b/apps/freeblox-web-service/src/decorators/user.decorator.ts new file mode 100644 index 0000000..b956b57 --- /dev/null +++ b/apps/freeblox-web-service/src/decorators/user.decorator.ts @@ -0,0 +1,8 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const User = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const response = ctx.switchToHttp().getResponse(); + return response.locals.user; + }, +); diff --git a/apps/freeblox-web-service/src/guards/auth.guard.ts b/apps/freeblox-web-service/src/guards/auth.guard.ts new file mode 100644 index 0000000..278916c --- /dev/null +++ b/apps/freeblox-web-service/src/guards/auth.guard.ts @@ -0,0 +1,30 @@ +import { + CanActivate, + ExecutionContext, + Inject, + Injectable, +} from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { Request, Response } from 'express'; +import { lastValueFrom } from 'rxjs'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor(@Inject('auth') private authClient: ClientProxy) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest() as Request; + const response = context.switchToHttp().getResponse() as Response; + if (!request.headers.authorization) return false; + + // Verify token by auth microservice + const [, token] = request.headers.authorization.split(' '); + const user = await lastValueFrom( + this.authClient.send('auth.verify', { token }), + ); + + // Add token contents to locals + response.locals.user = user; + return true; + } +} diff --git a/apps/freeblox-web-service/src/main.ts b/apps/freeblox-web-service/src/main.ts index 4d47ed2..b3eb9ce 100644 --- a/apps/freeblox-web-service/src/main.ts +++ b/apps/freeblox-web-service/src/main.ts @@ -9,6 +9,7 @@ async function bootstrap() { .setTitle('Freeblox Web Service') .setDescription('Freeblox Web Service API gateway') .setVersion('1.0') + .addBearerAuth() .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); diff --git a/apps/freeblox-web-service/src/services/auth/auth.controller.ts b/apps/freeblox-web-service/src/services/auth/auth.controller.ts index f05d312..624c36c 100644 --- a/apps/freeblox-web-service/src/services/auth/auth.controller.ts +++ b/apps/freeblox-web-service/src/services/auth/auth.controller.ts @@ -2,18 +2,26 @@ import { Body, ClassSerializerInterceptor, Controller, + Get, Inject, Post, + UseGuards, UseInterceptors, } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { LoginDto } from './dtos/login.dto'; +import { User } from '../../decorators/user.decorator'; +import { UserInfo } from '@freeblox/shared'; +import { lastValueFrom } from 'rxjs'; +import { AuthGuard } from '../../guards/auth.guard'; +import { UserDto } from './dtos/user.dto'; @Controller({ version: '1', path: 'auth', }) +@ApiBearerAuth() @ApiTags('Auth') @UseInterceptors(ClassSerializerInterceptor) export class AuthController { @@ -23,4 +31,17 @@ export class AuthController { async login(@Body() body: LoginDto) { return this.auth.send('auth.login', { body }); } + + @Get('me') + @ApiOkResponse({ type: UserDto }) + @UseGuards(AuthGuard) + async myInfo(@User() user: UserInfo): Promise { + return lastValueFrom(this.auth.send('auth.getUserById', { id: user.sub })); + } + + @Get('bans') + @UseGuards(AuthGuard) + async banInfo(@User() user: UserInfo) { + return lastValueFrom(this.auth.send('auth.getUserBans', { user })); + } } diff --git a/apps/freeblox-web-service/src/services/auth/dtos/user.dto.ts b/apps/freeblox-web-service/src/services/auth/dtos/user.dto.ts new file mode 100644 index 0000000..74930e1 --- /dev/null +++ b/apps/freeblox-web-service/src/services/auth/dtos/user.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UserDto { + @ApiProperty() + id: string; + + @ApiProperty() + username: string; + + @ApiProperty() + email: string; + + @ApiProperty({ nullable: true }) + phone: string; + + @ApiProperty({ nullable: true }) + country: string; + + @ApiProperty() + language: string; + + @ApiProperty({ nullable: true }) + displayName: string; + + @ApiProperty() + verified: boolean; + + @ApiProperty() + activated: boolean; + + @ApiProperty({ type: Date }) + loginAt: string; +} diff --git a/libs/shared/src/database/metaentity.ts b/libs/shared/src/database/metaentity.ts index fa04b24..767000c 100644 --- a/libs/shared/src/database/metaentity.ts +++ b/libs/shared/src/database/metaentity.ts @@ -1,5 +1,5 @@ import { Exclude } from 'class-transformer'; -import { CreateDateColumn, UpdateDateColumn } from 'typeorm'; +import { Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; export class MetaEntity { @CreateDateColumn({ name: 'created_at' }) @@ -10,3 +10,13 @@ export class MetaEntity { @Exclude() updatedAt: Date; } + +export class UserMetaEntity extends MetaEntity { + @Column({ name: 'created_by' }) + @Exclude() + createdBy: string; + + @Column({ name: 'updated_by' }) + @Exclude() + updatedBy: string; +}