more auth stuff
This commit is contained in:
parent
1904fe94ed
commit
d81fc53819
@ -2,6 +2,7 @@ import { Controller } from '@nestjs/common';
|
|||||||
import { AuthService } from './services/auth.service';
|
import { AuthService } from './services/auth.service';
|
||||||
import { MessagePattern } from '@nestjs/microservices';
|
import { MessagePattern } from '@nestjs/microservices';
|
||||||
import { LoginRequest } from './interfaces/auth.interface';
|
import { LoginRequest } from './interfaces/auth.interface';
|
||||||
|
import { UserInfo } from '@freeblox/shared';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
@ -14,6 +15,16 @@ export class AuthController {
|
|||||||
|
|
||||||
@MessagePattern('auth.verify')
|
@MessagePattern('auth.verify')
|
||||||
verify({ token }: { token: string }) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,17 @@ import { UserTokenEntity } from './database/entities/user-token.entity';
|
|||||||
import { OTPService } from './services/otp.service';
|
import { OTPService } from './services/otp.service';
|
||||||
import { BanEntity } from './database/entities/ban.entity';
|
import { BanEntity } from './database/entities/ban.entity';
|
||||||
import { BanService } from './services/ban.service';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -26,9 +37,10 @@ import { BanService } from './services/ban.service';
|
|||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: (config: ConfigService) => ({
|
useFactory: (config: ConfigService) => ({
|
||||||
...config.get('typeorm'),
|
...config.get('typeorm'),
|
||||||
|
entities,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
TypeOrmModule.forFeature([UserEntity, UserTokenEntity, BanEntity]),
|
TypeOrmModule.forFeature(entities),
|
||||||
ClientsModule.register([natsClient('auth')]),
|
ClientsModule.register([natsClient('auth')]),
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
@ -36,8 +48,9 @@ import { BanService } from './services/ban.service';
|
|||||||
...keysProviders,
|
...keysProviders,
|
||||||
JWTService,
|
JWTService,
|
||||||
OTPService,
|
OTPService,
|
||||||
AuthService,
|
|
||||||
BanService,
|
BanService,
|
||||||
|
RoleService,
|
||||||
|
AuthService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AuthModule implements OnModuleInit {
|
export class AuthModule implements OnModuleInit {
|
||||||
|
@ -3,6 +3,7 @@ import { resolve } from 'path';
|
|||||||
|
|
||||||
export const security = registerAs('security', () => ({
|
export const security = registerAs('security', () => ({
|
||||||
algorithm: String(process.env.JWT_ALGORITHM || 'RS512'),
|
algorithm: String(process.env.JWT_ALGORITHM || 'RS512'),
|
||||||
|
tokenExpiry: Number(process.env.JWT_EXPIRY) || 60 * 60,
|
||||||
privateKeyPath: resolve(String(process.env.PRIVATE_KEY_FILE)),
|
privateKeyPath: resolve(String(process.env.PRIVATE_KEY_FILE)),
|
||||||
publicKeyPath: resolve(String(process.env.PUBLIC_KEY_FILE)),
|
publicKeyPath: resolve(String(process.env.PUBLIC_KEY_FILE)),
|
||||||
}));
|
}));
|
||||||
|
18
apps/auth/src/database/entities/privilege.entity.ts
Normal file
18
apps/auth/src/database/entities/privilege.entity.ts
Normal file
@ -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;
|
||||||
|
}
|
54
apps/auth/src/database/entities/role.entity.ts
Normal file
54
apps/auth/src/database/entities/role.entity.ts
Normal file
@ -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[];
|
||||||
|
}
|
@ -5,6 +5,7 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { UserEntity } from './user.entity';
|
import { UserEntity } from './user.entity';
|
||||||
|
|
||||||
@ -29,5 +30,6 @@ export class UserTokenEntity {
|
|||||||
created_at: Date;
|
created_at: Date;
|
||||||
|
|
||||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE' })
|
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
user: UserEntity;
|
user: UserEntity;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import { MetaEntity } from '@freeblox/shared';
|
import { MetaEntity } from '@freeblox/shared';
|
||||||
import { Exclude, Expose } from 'class-transformer';
|
import { Exclude, Expose } from 'class-transformer';
|
||||||
import { IsOptional, IsString } from 'class-validator';
|
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()
|
@Expose()
|
||||||
|
@Entity('users')
|
||||||
export class UserEntity extends MetaEntity {
|
export class UserEntity extends MetaEntity {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
@IsString()
|
@IsString()
|
||||||
@ -55,4 +63,30 @@ export class UserEntity extends MetaEntity {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
loginAt: Date;
|
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[];
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
table.text('reason').notNullable();
|
table.text('reason').notNullable();
|
||||||
|
|
||||||
table.string('ip', 255).nullable();
|
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('user_id').nullable();
|
||||||
table.uuid('admin_id').nullable();
|
table.uuid('admin_id').nullable();
|
||||||
|
@ -2,6 +2,7 @@ import { Knex } from 'knex';
|
|||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
return knex.schema.createTable('user_privilege', (table) => {
|
return knex.schema.createTable('user_privilege', (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
table.integer('privilege_id').nullable().unsigned();
|
table.integer('privilege_id').nullable().unsigned();
|
||||||
table.uuid('user_id').nullable();
|
table.uuid('user_id').nullable();
|
||||||
table
|
table
|
||||||
|
@ -4,6 +4,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
return knex.schema.createTable('roles', (table) => {
|
return knex.schema.createTable('roles', (table) => {
|
||||||
table.increments('id').primary();
|
table.increments('id').primary();
|
||||||
table.text('role').notNullable();
|
table.text('role').notNullable();
|
||||||
|
table.string('mpath').nullable().defaultTo('');
|
||||||
table.integer('parent_id').nullable().unsigned();
|
table.integer('parent_id').nullable().unsigned();
|
||||||
table.boolean('automatic').defaultTo(false);
|
table.boolean('automatic').defaultTo(false);
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { Knex } from 'knex';
|
|||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
return knex.schema.createTable('user_role', (table) => {
|
return knex.schema.createTable('user_role', (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
table.integer('role_id').nullable().unsigned();
|
table.integer('role_id').nullable().unsigned();
|
||||||
table.uuid('user_id').nullable();
|
table.uuid('user_id').nullable();
|
||||||
table.foreign('role_id').references('roles.id').onDelete('CASCADE');
|
table.foreign('role_id').references('roles.id').onDelete('CASCADE');
|
||||||
|
@ -2,6 +2,7 @@ import { Knex } from 'knex';
|
|||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
return knex.schema.createTable('role_privilege', (table) => {
|
return knex.schema.createTable('role_privilege', (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
table.integer('role_id').nullable().unsigned();
|
table.integer('role_id').nullable().unsigned();
|
||||||
table.integer('privilege_id').nullable().unsigned();
|
table.integer('privilege_id').nullable().unsigned();
|
||||||
table.foreign('role_id').references('roles.id').onDelete('CASCADE');
|
table.foreign('role_id').references('roles.id').onDelete('CASCADE');
|
||||||
|
@ -6,25 +6,30 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
name: 'player',
|
name: 'player',
|
||||||
automatic: true,
|
automatic: true,
|
||||||
id: 0,
|
id: 0,
|
||||||
|
path: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'member',
|
name: 'member',
|
||||||
id: 0,
|
id: 0,
|
||||||
parent: 'player',
|
parent: 'player',
|
||||||
|
path: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'moderator',
|
name: 'moderator',
|
||||||
id: 0,
|
id: 0,
|
||||||
parent: 'member',
|
parent: 'member',
|
||||||
|
path: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'admin',
|
name: 'admin',
|
||||||
id: 0,
|
id: 0,
|
||||||
parent: 'moderator',
|
parent: 'moderator',
|
||||||
|
path: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'reduced',
|
name: 'reduced',
|
||||||
id: 0,
|
id: 0,
|
||||||
|
path: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -105,7 +110,10 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
const findRole = initialRoles.find(
|
const findRole = initialRoles.find(
|
||||||
(parent) => parent.name === role.parent,
|
(parent) => parent.name === role.parent,
|
||||||
);
|
);
|
||||||
parentId = findRole?.id;
|
if (findRole) {
|
||||||
|
parentId = findRole.id;
|
||||||
|
role.path += findRole.path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [created] = await knex('roles')
|
const [created] = await knex('roles')
|
||||||
@ -113,12 +121,17 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
{
|
{
|
||||||
role: role.name,
|
role: role.name,
|
||||||
automatic: role?.automatic,
|
automatic: role?.automatic,
|
||||||
|
mpath: role.path,
|
||||||
parent_id: parentId,
|
parent_id: parentId,
|
||||||
created_at: new Date(),
|
created_at: new Date(),
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
.returning(['id']);
|
.returning(['id']);
|
||||||
|
|
||||||
role.id = created.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) {
|
for (const privilege of initialPrivileges) {
|
||||||
|
4
apps/auth/src/interfaces/token-response.interface.ts
Normal file
4
apps/auth/src/interfaces/token-response.interface.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface TokenResponse {
|
||||||
|
token: string;
|
||||||
|
expires_in: number;
|
||||||
|
}
|
@ -14,6 +14,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { OTPService } from './otp.service';
|
import { OTPService } from './otp.service';
|
||||||
import { BanService } from './ban.service';
|
import { BanService } from './ban.service';
|
||||||
import { UserInfo } from '@freeblox/shared';
|
import { UserInfo } from '@freeblox/shared';
|
||||||
|
import { RoleService } from './role.service';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@ -21,8 +23,10 @@ export class AuthService {
|
|||||||
private readonly jwtService: JWTService,
|
private readonly jwtService: JWTService,
|
||||||
private readonly otpService: OTPService,
|
private readonly otpService: OTPService,
|
||||||
private readonly banService: BanService,
|
private readonly banService: BanService,
|
||||||
|
private readonly roleService: RoleService,
|
||||||
@InjectRepository(UserEntity)
|
@InjectRepository(UserEntity)
|
||||||
private readonly userRepository: Repository<UserEntity>,
|
private readonly userRepository: Repository<UserEntity>,
|
||||||
|
private readonly config: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,17 +82,24 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for active ban
|
||||||
const bans = await this.banService.getActiveBansForUser(userEntity);
|
const bans = await this.banService.getActiveBansForUser(userEntity);
|
||||||
const banned = !!bans.length;
|
const banned = !!bans.length;
|
||||||
|
|
||||||
|
// Get all privileges applicable to user
|
||||||
|
const privileges = await this.roleService.getUserPrivileges(userEntity);
|
||||||
|
|
||||||
// Issue token
|
// Issue token
|
||||||
|
const exp = this.config.get('security.tokenExpiry');
|
||||||
const issuedToken = await this.jwtService.sign({
|
const issuedToken = await this.jwtService.sign({
|
||||||
sub: userEntity.id,
|
sub: userEntity.id,
|
||||||
username: userEntity.username,
|
username: userEntity.username,
|
||||||
display_name: userEntity.displayName,
|
display_name: userEntity.displayName,
|
||||||
language: userEntity.language,
|
language: userEntity.language,
|
||||||
banned: banned,
|
banned: banned,
|
||||||
privileges: banned ? [] : ['freeblox'],
|
privileges: banned
|
||||||
|
? []
|
||||||
|
: privileges.map((privilege) => privilege.privilege),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set login time to now
|
// Set login time to now
|
||||||
@ -97,16 +108,19 @@ export class AuthService {
|
|||||||
{ loginAt: new Date() },
|
{ loginAt: new Date() },
|
||||||
);
|
);
|
||||||
|
|
||||||
return issuedToken;
|
return {
|
||||||
|
token: issuedToken,
|
||||||
|
expires_in: exp,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate user token
|
* Get user from token
|
||||||
* @param token JWT Token
|
* @param token JWT Token
|
||||||
* @returns User entity
|
* @returns User entity
|
||||||
*/
|
*/
|
||||||
async getUserFromToken(token: string) {
|
async getUserFromToken(token: string) {
|
||||||
const tokenInfo = await this.jwtService.verify(token);
|
const tokenInfo = await this.verifyToken(token);
|
||||||
const user = await this.userRepository.findOneByOrFail({
|
const user = await this.userRepository.findOneByOrFail({
|
||||||
id: tokenInfo.sub,
|
id: tokenInfo.sub,
|
||||||
activated: true,
|
activated: true,
|
||||||
@ -114,6 +128,30 @@ export class AuthService {
|
|||||||
return instanceToPlain(user);
|
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
|
* Get user bans
|
||||||
* @param tokeninfo
|
* @param tokeninfo
|
||||||
|
@ -13,12 +13,14 @@ export class JWTService {
|
|||||||
|
|
||||||
async sign(data: Record<string, unknown>, audience = 'urn:freeblox:service') {
|
async sign(data: Record<string, unknown>, audience = 'urn:freeblox:service') {
|
||||||
const alg = this.config.get('security.algorithm');
|
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)
|
const jwt = await new SignJWT(data)
|
||||||
.setProtectedHeader({ alg })
|
.setProtectedHeader({ alg })
|
||||||
.setIssuedAt()
|
.setIssuedAt()
|
||||||
.setIssuer('urn:freeblox:auth')
|
.setIssuer('urn:freeblox:auth')
|
||||||
.setAudience(audience)
|
.setAudience(audience)
|
||||||
.setExpirationTime('8d')
|
.setExpirationTime(exp)
|
||||||
.sign(this.privateKey);
|
.sign(this.privateKey);
|
||||||
return jwt;
|
return jwt;
|
||||||
}
|
}
|
||||||
|
78
apps/auth/src/services/role.service.ts
Normal file
78
apps/auth/src/services/role.service.ts
Normal file
@ -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<UserEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 `[<Found role entity>, <Privilege list>]`
|
||||||
|
*/
|
||||||
|
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, []];
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
},
|
||||||
|
);
|
30
apps/freeblox-web-service/src/guards/auth.guard.ts
Normal file
30
apps/freeblox-web-service/src/guards/auth.guard.ts
Normal file
@ -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<boolean> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ async function bootstrap() {
|
|||||||
.setTitle('Freeblox Web Service')
|
.setTitle('Freeblox Web Service')
|
||||||
.setDescription('Freeblox Web Service API gateway')
|
.setDescription('Freeblox Web Service API gateway')
|
||||||
.setVersion('1.0')
|
.setVersion('1.0')
|
||||||
|
.addBearerAuth()
|
||||||
.build();
|
.build();
|
||||||
const document = SwaggerModule.createDocument(app, config);
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
SwaggerModule.setup('api', app, document);
|
SwaggerModule.setup('api', app, document);
|
||||||
|
@ -2,18 +2,26 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
ClassSerializerInterceptor,
|
ClassSerializerInterceptor,
|
||||||
Controller,
|
Controller,
|
||||||
|
Get,
|
||||||
Inject,
|
Inject,
|
||||||
Post,
|
Post,
|
||||||
|
UseGuards,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ClientProxy } from '@nestjs/microservices';
|
import { ClientProxy } from '@nestjs/microservices';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { LoginDto } from './dtos/login.dto';
|
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({
|
@Controller({
|
||||||
version: '1',
|
version: '1',
|
||||||
path: 'auth',
|
path: 'auth',
|
||||||
})
|
})
|
||||||
|
@ApiBearerAuth()
|
||||||
@ApiTags('Auth')
|
@ApiTags('Auth')
|
||||||
@UseInterceptors(ClassSerializerInterceptor)
|
@UseInterceptors(ClassSerializerInterceptor)
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
@ -23,4 +31,17 @@ export class AuthController {
|
|||||||
async login(@Body() body: LoginDto) {
|
async login(@Body() body: LoginDto) {
|
||||||
return this.auth.send('auth.login', { body });
|
return this.auth.send('auth.login', { body });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('me')
|
||||||
|
@ApiOkResponse({ type: UserDto })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async myInfo(@User() user: UserInfo): Promise<UserDto> {
|
||||||
|
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 }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
33
apps/freeblox-web-service/src/services/auth/dtos/user.dto.ts
Normal file
33
apps/freeblox-web-service/src/services/auth/dtos/user.dto.ts
Normal file
@ -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;
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { Exclude } from 'class-transformer';
|
import { Exclude } from 'class-transformer';
|
||||||
import { CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
import { Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
export class MetaEntity {
|
export class MetaEntity {
|
||||||
@CreateDateColumn({ name: 'created_at' })
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
@ -10,3 +10,13 @@ export class MetaEntity {
|
|||||||
@Exclude()
|
@Exclude()
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UserMetaEntity extends MetaEntity {
|
||||||
|
@Column({ name: 'created_by' })
|
||||||
|
@Exclude()
|
||||||
|
createdBy: string;
|
||||||
|
|
||||||
|
@Column({ name: 'updated_by' })
|
||||||
|
@Exclude()
|
||||||
|
updatedBy: string;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user