auth changes, exception filters, two new services
This commit is contained in:
parent
529e4a96d7
commit
b6460f1cda
22
apps/assets/src/assets.controller.spec.ts
Normal file
22
apps/assets/src/assets.controller.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AssetsController } from './assets.controller';
|
||||||
|
import { AssetsService } from './assets.service';
|
||||||
|
|
||||||
|
describe('AssetsController', () => {
|
||||||
|
let assetsController: AssetsController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [AssetsController],
|
||||||
|
providers: [AssetsService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
assetsController = app.get<AssetsController>(AssetsController);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('root', () => {
|
||||||
|
it('should return "Hello World!"', () => {
|
||||||
|
expect(assetsController.getHello()).toBe('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
12
apps/assets/src/assets.controller.ts
Normal file
12
apps/assets/src/assets.controller.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { AssetsService } from './assets.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AssetsController {
|
||||||
|
constructor(private readonly assetsService: AssetsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
getHello(): string {
|
||||||
|
return this.assetsService.getHello();
|
||||||
|
}
|
||||||
|
}
|
29
apps/assets/src/assets.module.ts
Normal file
29
apps/assets/src/assets.module.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AssetsController } from './assets.controller';
|
||||||
|
import { AssetsService } from './assets.service';
|
||||||
|
import { makeKnex, makeTypeOrm, natsClient } from '@freeblox/shared';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { ClientsModule } from '@nestjs/microservices';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
ignoreEnvFile: process.env.NODE_ENV === 'development',
|
||||||
|
load: [makeKnex('assets', __dirname), makeTypeOrm('assets')],
|
||||||
|
}),
|
||||||
|
TypeOrmModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (config: ConfigService) => config.get('typeorm'),
|
||||||
|
}),
|
||||||
|
ClientsModule.register([
|
||||||
|
natsClient('assets'),
|
||||||
|
natsClient('auth'),
|
||||||
|
natsClient('player'),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
controllers: [AssetsController],
|
||||||
|
providers: [AssetsService],
|
||||||
|
})
|
||||||
|
export class AssetsModule {}
|
8
apps/assets/src/assets.service.ts
Normal file
8
apps/assets/src/assets.service.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AssetsService {
|
||||||
|
getHello(): string {
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
18
apps/assets/src/main.ts
Normal file
18
apps/assets/src/main.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AssetsModule } from './assets.module';
|
||||||
|
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
|
||||||
|
AssetsModule,
|
||||||
|
{
|
||||||
|
transport: Transport.NATS,
|
||||||
|
options: {
|
||||||
|
servers: [String(process.env.NATS_ENTRYPOINT)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.listen();
|
||||||
|
}
|
||||||
|
bootstrap();
|
24
apps/assets/test/app.e2e-spec.ts
Normal file
24
apps/assets/test/app.e2e-spec.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import * as request from 'supertest';
|
||||||
|
import { AssetsModule } from './../src/assets.module';
|
||||||
|
|
||||||
|
describe('AssetsController (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AssetsModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
9
apps/assets/test/jest-e2e.json
Normal file
9
apps/assets/test/jest-e2e.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
9
apps/assets/tsconfig.app.json
Normal file
9
apps/assets/tsconfig.app.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": false,
|
||||||
|
"outDir": "../../dist/apps/assets"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||||
|
}
|
@ -20,7 +20,7 @@ export class AuthController {
|
|||||||
|
|
||||||
@MessagePattern('auth.verify')
|
@MessagePattern('auth.verify')
|
||||||
verify({ token }: { token: string }) {
|
verify({ token }: { token: string }) {
|
||||||
return this.authService.verifyToken(token);
|
return this.authService.verifyAccessToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@MessagePattern('auth.getUserById')
|
@MessagePattern('auth.getUserById')
|
||||||
|
@ -3,11 +3,14 @@ import {
|
|||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
|
JoinTable,
|
||||||
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { UserEntity } from './user.entity';
|
import { UserEntity } from './user.entity';
|
||||||
import { Exclude, Expose } from 'class-transformer';
|
import { Exclude, Expose } from 'class-transformer';
|
||||||
|
import { PrivilegeEntity } from './privilege.entity';
|
||||||
|
|
||||||
@Entity('bans')
|
@Entity('bans')
|
||||||
@Expose()
|
@Expose()
|
||||||
@ -18,6 +21,9 @@ export class BanEntity {
|
|||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
reason: string;
|
reason: string;
|
||||||
|
|
||||||
|
@Column({ default: false, name: 'privilege_ban' })
|
||||||
|
privilegeBan: boolean;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
ip: string;
|
ip: string;
|
||||||
|
|
||||||
@ -39,4 +45,12 @@ export class BanEntity {
|
|||||||
@JoinColumn({ name: 'admin_id' })
|
@JoinColumn({ name: 'admin_id' })
|
||||||
@Exclude()
|
@Exclude()
|
||||||
admin: UserEntity;
|
admin: UserEntity;
|
||||||
|
|
||||||
|
@ManyToMany(() => PrivilegeEntity, { onDelete: 'CASCADE' })
|
||||||
|
@JoinTable({
|
||||||
|
name: 'ban_privilege',
|
||||||
|
joinColumn: { name: 'ban_id' },
|
||||||
|
inverseJoinColumn: { name: 'privilege_id' },
|
||||||
|
})
|
||||||
|
privileges: PrivilegeEntity[];
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { Exclude, Expose } from 'class-transformer';
|
|||||||
import { IsOptional, IsString } from 'class-validator';
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
|
DeleteDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
JoinTable,
|
JoinTable,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
@ -89,4 +90,8 @@ export class UserEntity extends MetaEntity {
|
|||||||
})
|
})
|
||||||
@Exclude()
|
@Exclude()
|
||||||
roles: RoleEntity[];
|
roles: RoleEntity[];
|
||||||
|
|
||||||
|
@DeleteDateColumn({ name: 'deleted_at' })
|
||||||
|
@Exclude()
|
||||||
|
deletedAt: Date;
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
|
|
||||||
table.timestamps(true, true);
|
table.timestamps(true, true);
|
||||||
table.timestamp('login_at');
|
table.timestamp('login_at');
|
||||||
|
table.timestamp('deleted_at');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
table.increments('id').primary();
|
table.increments('id').primary();
|
||||||
|
|
||||||
table.text('reason').notNullable();
|
table.text('reason').notNullable();
|
||||||
|
table.boolean('privilege_ban').defaultTo(false);
|
||||||
|
|
||||||
table.string('ip', 255).nullable();
|
table.string('ip', 255).nullable();
|
||||||
table.integer('cidr', 2).nullable().defaultTo(32).unsigned();
|
table.integer('cidr', 2).nullable().defaultTo(32).unsigned();
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.createTable('ban_privilege', (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.integer('privilege_id').nullable().unsigned();
|
||||||
|
table.integer('ban_id').nullable();
|
||||||
|
table
|
||||||
|
.foreign('privilege_id')
|
||||||
|
.references('privileges.id')
|
||||||
|
.onDelete('CASCADE');
|
||||||
|
table.foreign('ban_id').references('bans.id').onDelete('CASCADE');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.dropTable('ban_privilege');
|
||||||
|
}
|
@ -1,21 +1,36 @@
|
|||||||
import { hash } from 'bcrypt';
|
import { hashSync } from 'bcrypt';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
export async function seed(knex: Knex): Promise<void> {
|
const initialUsers = [
|
||||||
const userExists = await knex('users').where({ username: 'freeblox' });
|
{
|
||||||
if (userExists?.length) return;
|
username: 'freeblox',
|
||||||
|
email: 'freeblox@icynet.eu',
|
||||||
|
phone: null,
|
||||||
|
country: 'ee',
|
||||||
|
language: 'en',
|
||||||
|
password: hashSync('FBLXAdmin123', 12),
|
||||||
|
display_name: 'Freeblox',
|
||||||
|
verified: true,
|
||||||
|
activated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: 'noob',
|
||||||
|
email: 'noob@icynet.eu',
|
||||||
|
phone: null,
|
||||||
|
country: 'ee',
|
||||||
|
language: 'en',
|
||||||
|
password: hashSync('FBLXNoob123', 12),
|
||||||
|
display_name: 'Noob',
|
||||||
|
verified: true,
|
||||||
|
activated: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
await knex('users').insert([
|
export async function seed(knex: Knex): Promise<void> {
|
||||||
{
|
for (const user of initialUsers) {
|
||||||
username: 'freeblox',
|
const userExists = await knex('users').where({ username: user.username });
|
||||||
email: 'freeblox@icynet.eu',
|
if (userExists?.length) continue;
|
||||||
phone: null,
|
|
||||||
country: 'ee',
|
await knex('users').insert(user);
|
||||||
language: 'en',
|
}
|
||||||
password: await hash('FBLXAdmin123', 12),
|
|
||||||
display_name: 'Freeblox',
|
|
||||||
verified: true,
|
|
||||||
activated: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
@ -1,99 +1,185 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
export async function seed(knex: Knex): Promise<void> {
|
const initialRoles = [
|
||||||
const initialRoles = [
|
{
|
||||||
{
|
name: 'player',
|
||||||
name: 'player',
|
automatic: true,
|
||||||
automatic: true,
|
id: 0,
|
||||||
id: 0,
|
path: '',
|
||||||
path: '',
|
},
|
||||||
},
|
{
|
||||||
{
|
name: 'member',
|
||||||
name: 'member',
|
id: 0,
|
||||||
id: 0,
|
parent: 'player',
|
||||||
parent: 'player',
|
path: '',
|
||||||
path: '',
|
},
|
||||||
},
|
{
|
||||||
{
|
name: 'moderator',
|
||||||
name: 'moderator',
|
id: 0,
|
||||||
id: 0,
|
parent: 'member',
|
||||||
parent: 'member',
|
path: '',
|
||||||
path: '',
|
},
|
||||||
},
|
{
|
||||||
{
|
name: 'admin',
|
||||||
name: 'admin',
|
id: 0,
|
||||||
id: 0,
|
parent: 'moderator',
|
||||||
parent: 'moderator',
|
path: '',
|
||||||
path: '',
|
},
|
||||||
},
|
{
|
||||||
{
|
name: 'reduced',
|
||||||
name: 'reduced',
|
id: 0,
|
||||||
id: 0,
|
path: '',
|
||||||
path: '',
|
},
|
||||||
},
|
];
|
||||||
];
|
|
||||||
|
|
||||||
const initialPrivileges = [
|
const initialPrivileges = [
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
name: 'web',
|
name: 'web',
|
||||||
roles: ['player', 'reduced'],
|
roles: ['player', 'reduced'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
name: 'play',
|
name: 'report',
|
||||||
roles: ['player'],
|
roles: ['player', 'reduced'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
name: 'shop',
|
name: 'play',
|
||||||
roles: ['player'],
|
roles: ['player'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
name: 'build',
|
name: 'shop',
|
||||||
roles: ['player'],
|
roles: ['player'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
name: 'trade',
|
name: 'community',
|
||||||
roles: ['player'],
|
roles: ['player'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
name: 'oidc',
|
name: 'trade',
|
||||||
roles: ['player'],
|
roles: ['player'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
name: 'host',
|
name: 'oidc',
|
||||||
roles: [],
|
roles: ['player'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
name: 'ban',
|
name: 'host',
|
||||||
roles: ['moderator', 'admin'],
|
roles: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
name: 'permaban',
|
name: 'create:game',
|
||||||
roles: ['admin'],
|
roles: ['player'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
name: 'stopserver',
|
name: 'create:clothing',
|
||||||
roles: ['admin'],
|
roles: ['player'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
name: 'banserver',
|
name: 'create:accessory',
|
||||||
roles: ['admin'],
|
roles: ['member'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
name: 'root',
|
name: 'create:character',
|
||||||
},
|
roles: [],
|
||||||
];
|
},
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'contentmod',
|
||||||
|
roles: ['moderator'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'ban',
|
||||||
|
roles: ['moderator'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'privban',
|
||||||
|
roles: ['moderator'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'permaban',
|
||||||
|
roles: ['admin'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'contentedit',
|
||||||
|
roles: ['admin'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'server:quarantine',
|
||||||
|
roles: ['moderator'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'server:stop',
|
||||||
|
roles: ['admin'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'server:banhost',
|
||||||
|
roles: ['admin'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'server:provision',
|
||||||
|
roles: ['admin'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'root',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function seed(knex: Knex): Promise<void> {
|
||||||
|
const giveUserRole = async (username: string, role: string) => {
|
||||||
|
const userExists = await knex('users').where({ username });
|
||||||
|
if (!userExists?.length) return;
|
||||||
|
|
||||||
|
const adminRole = initialRoles.find((entry) => entry.name === role);
|
||||||
|
|
||||||
|
const bodyUserRole = {
|
||||||
|
user_id: userExists[0].id,
|
||||||
|
role_id: adminRole.id,
|
||||||
|
};
|
||||||
|
if (!(await knex('user_role').where(bodyUserRole))?.length) {
|
||||||
|
await knex('user_role').insert(bodyUserRole);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const giveUserPrivilege = async (username: string, privilege: string[]) => {
|
||||||
|
const userExists = await knex('users').where({ username });
|
||||||
|
if (!userExists?.length) return;
|
||||||
|
|
||||||
|
const privileges = initialPrivileges.filter((entry) =>
|
||||||
|
privilege.includes(entry.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
privileges.map(async (privilege) => {
|
||||||
|
const body = {
|
||||||
|
user_id: userExists[0].id,
|
||||||
|
privilege_id: privilege.id,
|
||||||
|
};
|
||||||
|
if (!(await knex('user_privilege').where(body))?.length) {
|
||||||
|
await knex('user_privilege').insert(body);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
for (const role of initialRoles) {
|
for (const role of initialRoles) {
|
||||||
const exists = await knex('roles').where({
|
const exists = await knex('roles').where({
|
||||||
@ -170,31 +256,7 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add roles to initial user
|
// Add roles to initial user
|
||||||
const userExists = await knex('users').where({ username: 'freeblox' });
|
await giveUserRole('freeblox', 'admin');
|
||||||
if (!userExists?.length) return;
|
await giveUserRole('noob', 'player');
|
||||||
|
await giveUserPrivilege('freeblox', ['root', 'host']);
|
||||||
const adminRole = initialRoles.find((role) => role.name === 'admin');
|
|
||||||
const privileges = initialPrivileges.filter((privilege) =>
|
|
||||||
['host', 'root'].includes(privilege.name),
|
|
||||||
);
|
|
||||||
|
|
||||||
const bodyUserRole = {
|
|
||||||
user_id: userExists[0].id,
|
|
||||||
role_id: adminRole.id,
|
|
||||||
};
|
|
||||||
if (!(await knex('user_role').where(bodyUserRole))?.length) {
|
|
||||||
await knex('user_role').insert(bodyUserRole);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
privileges.map(async (privilege) => {
|
|
||||||
const body = {
|
|
||||||
user_id: userExists[0].id,
|
|
||||||
privilege_id: privilege.id,
|
|
||||||
};
|
|
||||||
if (!(await knex('user_privilege').where(body))?.length) {
|
|
||||||
await knex('user_privilege').insert(body);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
|
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
|
||||||
import { AuthModule } from './auth.module';
|
import { AuthModule } from './auth.module';
|
||||||
|
import { HttpRpcExceptionFilter } from '@freeblox/shared';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
|
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
|
||||||
@ -13,6 +14,7 @@ async function bootstrap() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.useGlobalFilters(new HttpRpcExceptionFilter());
|
||||||
await app.listen();
|
await app.listen();
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
import {
|
import { Injectable } from '@nestjs/common';
|
||||||
BadRequestException,
|
|
||||||
ForbiddenException,
|
|
||||||
Injectable,
|
|
||||||
PreconditionFailedException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { LoginRequest } from '../interfaces/auth.interface';
|
import { LoginRequest } from '../interfaces/auth.interface';
|
||||||
import { JWTService } from './jwt.service';
|
import { JWTService } from './jwt.service';
|
||||||
import { ILike, Repository } from 'typeorm';
|
import { ILike, Repository } from 'typeorm';
|
||||||
@ -13,7 +8,12 @@ import { instanceToPlain } from 'class-transformer';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
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 {
|
||||||
|
BadRequestRpcException,
|
||||||
|
ForbiddenRpcException,
|
||||||
|
PreconditionFailedRpcException,
|
||||||
|
UserInfo,
|
||||||
|
} from '@freeblox/shared';
|
||||||
import { RoleService } from './role.service';
|
import { RoleService } from './role.service';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { RefreshService } from './refresh.service';
|
import { RefreshService } from './refresh.service';
|
||||||
@ -38,7 +38,7 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
async login(body: LoginRequest) {
|
async login(body: LoginRequest) {
|
||||||
if (!body.email || !body.password) {
|
if (!body.email || !body.password) {
|
||||||
throw new BadRequestException('Invalid username or password');
|
throw new BadRequestRpcException('Invalid username or password');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent wildcards
|
// Prevent wildcards
|
||||||
@ -58,20 +58,20 @@ export class AuthService {
|
|||||||
|
|
||||||
// User not found
|
// User not found
|
||||||
if (!userEntity) {
|
if (!userEntity) {
|
||||||
throw new BadRequestException('Invalid username or password');
|
throw new BadRequestRpcException('Invalid username or password');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare passwords
|
// Compare passwords
|
||||||
const passwordMatch = await compare(body.password, userEntity.password);
|
const passwordMatch = await compare(body.password, userEntity.password);
|
||||||
if (!passwordMatch) {
|
if (!passwordMatch) {
|
||||||
throw new BadRequestException('Invalid username or password');
|
throw new BadRequestRpcException('Invalid username or password');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check TOTP
|
// Check TOTP
|
||||||
const userOTPToken = await this.otpService.getUserTOTP(userEntity);
|
const userOTPToken = await this.otpService.getUserTOTP(userEntity);
|
||||||
if (userOTPToken) {
|
if (userOTPToken) {
|
||||||
if (!body.totpToken) {
|
if (!body.totpToken) {
|
||||||
throw new PreconditionFailedException('TOTP Token required');
|
throw new PreconditionFailedRpcException('TOTP Token required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const validate = this.otpService.validateTOTP(
|
const validate = this.otpService.validateTOTP(
|
||||||
@ -80,13 +80,13 @@ export class AuthService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!validate) {
|
if (!validate) {
|
||||||
throw new ForbiddenException('Invalid TOTP Token');
|
throw new ForbiddenRpcException('Invalid TOTP Token');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue access token
|
// Issue access token
|
||||||
const exp = this.config.get('security.jwtTokenExpiry');
|
const exp = this.config.get('security.jwtTokenExpiry');
|
||||||
const issuedToken = await this.issueToken(userEntity);
|
const issuedToken = await this.issueAccessToken(userEntity);
|
||||||
|
|
||||||
// Issue refresh token
|
// Issue refresh token
|
||||||
const refreshToken = await this.refreshService.issueRefreshToken(
|
const refreshToken = await this.refreshService.issueRefreshToken(
|
||||||
@ -112,7 +112,7 @@ export class AuthService {
|
|||||||
|
|
||||||
// Issue new access token
|
// Issue new access token
|
||||||
const exp = this.config.get('security.jwtTokenExpiry');
|
const exp = this.config.get('security.jwtTokenExpiry');
|
||||||
const issuedToken = await this.issueToken(userEntity);
|
const issuedToken = await this.issueAccessToken(userEntity);
|
||||||
|
|
||||||
// Set login time to now
|
// Set login time to now
|
||||||
await this.userRepository.update(
|
await this.userRepository.update(
|
||||||
@ -133,7 +133,7 @@ export class AuthService {
|
|||||||
* @returns User entity
|
* @returns User entity
|
||||||
*/
|
*/
|
||||||
async getUserFromToken(token: string) {
|
async getUserFromToken(token: string) {
|
||||||
const tokenInfo = await this.verifyToken(token);
|
const tokenInfo = await this.verifyAccessToken(token);
|
||||||
const user = await this.userRepository.findOneByOrFail({
|
const user = await this.userRepository.findOneByOrFail({
|
||||||
id: tokenInfo.sub,
|
id: tokenInfo.sub,
|
||||||
activated: true,
|
activated: true,
|
||||||
@ -146,12 +146,11 @@ export class AuthService {
|
|||||||
* @param token JWT token
|
* @param token JWT token
|
||||||
* @returns User token info
|
* @returns User token info
|
||||||
*/
|
*/
|
||||||
async verifyToken(token: string) {
|
async verifyAccessToken(token: string) {
|
||||||
try {
|
try {
|
||||||
return await this.jwtService.verify(token);
|
return await this.jwtService.verify(token);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(token, e);
|
throw new ForbiddenRpcException('Invalid token');
|
||||||
throw new ForbiddenException('Invalid token');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +160,7 @@ export class AuthService {
|
|||||||
* @returns User entity
|
* @returns User entity
|
||||||
*/
|
*/
|
||||||
async getUserById(id: string) {
|
async getUserById(id: string) {
|
||||||
if (!id) throw new BadRequestException('ID is required');
|
if (!id) throw new BadRequestRpcException('ID is required');
|
||||||
return instanceToPlain(await this.userRepository.findOneByOrFail({ id }));
|
return instanceToPlain(await this.userRepository.findOneByOrFail({ id }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,13 +177,26 @@ export class AuthService {
|
|||||||
return instanceToPlain(bans);
|
return instanceToPlain(bans);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async issueToken(user: UserEntity) {
|
/**
|
||||||
|
* Issue new JWT for user
|
||||||
|
* @private
|
||||||
|
* @param user
|
||||||
|
* @returns JWT
|
||||||
|
*/
|
||||||
|
async issueAccessToken(user: UserEntity) {
|
||||||
// Check for active ban
|
// Check for active ban
|
||||||
const bans = await this.banService.getActiveBansForUser(user);
|
const bans = await this.banService.getActiveBansForUser(user);
|
||||||
const banned = !!bans.length;
|
const banned = !!bans.length;
|
||||||
|
|
||||||
// Get all privileges applicable to user
|
// Get all privileges applicable to user
|
||||||
const privileges = await this.roleService.getUserPrivileges(user);
|
const bannedPrivileges = bans.reduce(
|
||||||
|
(privileges, ban) => [...privileges, ...(ban.privileges || [])],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const privileges = await this.roleService.getUserPrivileges(
|
||||||
|
user,
|
||||||
|
bannedPrivileges,
|
||||||
|
);
|
||||||
|
|
||||||
// Issue new token
|
// Issue new token
|
||||||
return this.jwtService.sign({
|
return this.jwtService.sign({
|
||||||
|
@ -23,12 +23,16 @@ export class BanService {
|
|||||||
user: { id: user.id },
|
user: { id: user.id },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
relations: ['privileges'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllBansForUser(user: UserEntity) {
|
async getAllBansForUser(user: UserEntity) {
|
||||||
return this.banRepository.findBy({
|
return this.banRepository.find({
|
||||||
user: { id: user.id },
|
where: {
|
||||||
|
user: { id: user.id },
|
||||||
|
},
|
||||||
|
relations: ['privileges'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenException, Inject, Injectable } from '@nestjs/common';
|
import { ForbiddenRpcException } from '@freeblox/shared';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { EncryptJWT, JWK, KeyLike, SignJWT, jwtDecrypt, jwtVerify } from 'jose';
|
import { EncryptJWT, JWK, KeyLike, SignJWT, jwtDecrypt, jwtVerify } from 'jose';
|
||||||
|
|
||||||
@ -34,7 +35,7 @@ export class JWTService {
|
|||||||
audience,
|
audience,
|
||||||
});
|
});
|
||||||
if (protectedHeader.alg !== alg)
|
if (protectedHeader.alg !== alg)
|
||||||
throw new ForbiddenException('Provided JWT contains invalid headers.');
|
throw new ForbiddenRpcException('Provided JWT contains invalid headers.');
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +64,7 @@ export class JWTService {
|
|||||||
audience,
|
audience,
|
||||||
});
|
});
|
||||||
if (protectedHeader.enc !== alg)
|
if (protectedHeader.enc !== alg)
|
||||||
throw new ForbiddenException('Provided JWT contains invalid headers.');
|
throw new ForbiddenRpcException('Provided JWT contains invalid headers.');
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { UserTokenEntity } from '../database/entities/user-token.entity';
|
import { UserTokenEntity } from '../database/entities/user-token.entity';
|
||||||
import { JWTService } from './jwt.service';
|
import { JWTService } from './jwt.service';
|
||||||
import { UserEntity } from '../database/entities/user.entity';
|
import { UserEntity } from '../database/entities/user.entity';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { UserTokenType, generateString } from '@freeblox/shared';
|
import {
|
||||||
|
UnauthorizedRpcException,
|
||||||
|
UserTokenType,
|
||||||
|
generateString,
|
||||||
|
} from '@freeblox/shared';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -36,11 +40,11 @@ export class RefreshService {
|
|||||||
* @returns New refresh token
|
* @returns New refresh token
|
||||||
*/
|
*/
|
||||||
async useRefreshToken(token: string) {
|
async useRefreshToken(token: string) {
|
||||||
if (!token) throw new UnauthorizedException('Invalid refresh token');
|
if (!token) throw new UnauthorizedRpcException('Invalid refresh token');
|
||||||
const decrypted = await this.jwt.decrypt(token);
|
const decrypted = await this.jwt.decrypt(token);
|
||||||
|
|
||||||
if (!decrypted.token || !decrypted.sub)
|
if (!decrypted.token || !decrypted.sub)
|
||||||
throw new UnauthorizedException('Invalid refresh token');
|
throw new UnauthorizedRpcException('Invalid refresh token');
|
||||||
|
|
||||||
const tokenEntity = await this.userTokenRepository.findOneOrFail({
|
const tokenEntity = await this.userTokenRepository.findOneOrFail({
|
||||||
where: {
|
where: {
|
||||||
@ -51,7 +55,7 @@ export class RefreshService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (decrypted.sub !== tokenEntity.user.id)
|
if (decrypted.sub !== tokenEntity.user.id)
|
||||||
throw new UnauthorizedException('Invalid refresh token');
|
throw new UnauthorizedRpcException('Invalid refresh token');
|
||||||
|
|
||||||
// Using an expired refresh token
|
// Using an expired refresh token
|
||||||
if (tokenEntity.expiresAt.getTime() < Date.now()) {
|
if (tokenEntity.expiresAt.getTime() < Date.now()) {
|
||||||
@ -62,7 +66,7 @@ export class RefreshService {
|
|||||||
await this.userTokenRepository.remove(tokenEntity);
|
await this.userTokenRepository.remove(tokenEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnauthorizedException('Invalid refresh token');
|
throw new UnauthorizedRpcException('Invalid refresh token');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark old token as expired
|
// Mark old token as expired
|
||||||
|
@ -48,7 +48,7 @@ export class RoleService {
|
|||||||
* @param user User
|
* @param user User
|
||||||
* @returns Unique privilege list
|
* @returns Unique privilege list
|
||||||
*/
|
*/
|
||||||
async getUserPrivileges(user: UserEntity) {
|
async getUserPrivileges(user: UserEntity, exclude: PrivilegeEntity[] = []) {
|
||||||
if (!user.privileges || !user.roles) {
|
if (!user.privileges || !user.roles) {
|
||||||
user = await this.userRepository.findOne({
|
user = await this.userRepository.findOne({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
@ -56,7 +56,15 @@ export class RoleService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getApplicablePrivileges(user.roles, user.privileges);
|
const applicable = await this.getApplicablePrivileges(
|
||||||
|
user.roles,
|
||||||
|
user.privileges,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exclude privileges provided
|
||||||
|
return applicable.filter(
|
||||||
|
(privilege) => !exclude.some((entry) => entry.id === privilege.id),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
22
apps/bank/src/bank.controller.spec.ts
Normal file
22
apps/bank/src/bank.controller.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { BankController } from './bank.controller';
|
||||||
|
import { BankService } from './bank.service';
|
||||||
|
|
||||||
|
describe('BankController', () => {
|
||||||
|
let bankController: BankController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [BankController],
|
||||||
|
providers: [BankService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
bankController = app.get<BankController>(BankController);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('root', () => {
|
||||||
|
it('should return "Hello World!"', () => {
|
||||||
|
expect(bankController.getHello()).toBe('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
12
apps/bank/src/bank.controller.ts
Normal file
12
apps/bank/src/bank.controller.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { BankService } from './bank.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class BankController {
|
||||||
|
constructor(private readonly bankService: BankService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
getHello(): string {
|
||||||
|
return this.bankService.getHello();
|
||||||
|
}
|
||||||
|
}
|
29
apps/bank/src/bank.module.ts
Normal file
29
apps/bank/src/bank.module.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { BankController } from './bank.controller';
|
||||||
|
import { BankService } from './bank.service';
|
||||||
|
import { makeKnex, makeTypeOrm, natsClient } from '@freeblox/shared';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { ClientsModule } from '@nestjs/microservices';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
ignoreEnvFile: process.env.NODE_ENV === 'development',
|
||||||
|
load: [makeKnex('bank', __dirname), makeTypeOrm('bank')],
|
||||||
|
}),
|
||||||
|
TypeOrmModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (config: ConfigService) => config.get('typeorm'),
|
||||||
|
}),
|
||||||
|
ClientsModule.register([
|
||||||
|
natsClient('bank'),
|
||||||
|
natsClient('auth'),
|
||||||
|
natsClient('player'),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
controllers: [BankController],
|
||||||
|
providers: [BankService],
|
||||||
|
})
|
||||||
|
export class BankModule {}
|
8
apps/bank/src/bank.service.ts
Normal file
8
apps/bank/src/bank.service.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BankService {
|
||||||
|
getHello(): string {
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
18
apps/bank/src/main.ts
Normal file
18
apps/bank/src/main.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { BankModule } from './bank.module';
|
||||||
|
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
|
||||||
|
BankModule,
|
||||||
|
{
|
||||||
|
transport: Transport.NATS,
|
||||||
|
options: {
|
||||||
|
servers: [String(process.env.NATS_ENTRYPOINT)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.listen();
|
||||||
|
}
|
||||||
|
bootstrap();
|
24
apps/bank/test/app.e2e-spec.ts
Normal file
24
apps/bank/test/app.e2e-spec.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import * as request from 'supertest';
|
||||||
|
import { BankModule } from './../src/bank.module';
|
||||||
|
|
||||||
|
describe('BankController (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [BankModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
9
apps/bank/test/jest-e2e.json
Normal file
9
apps/bank/test/jest-e2e.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
9
apps/bank/tsconfig.app.json
Normal file
9
apps/bank/tsconfig.app.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": false,
|
||||||
|
"outDir": "../../dist/apps/bank"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||||
|
}
|
25
apps/freeblox-web-service/src/filters/exception.filter.ts
Normal file
25
apps/freeblox-web-service/src/filters/exception.filter.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import {
|
||||||
|
ExceptionFilter,
|
||||||
|
Catch,
|
||||||
|
ArgumentsHost,
|
||||||
|
HttpException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
@Catch(HttpException)
|
||||||
|
export class HttpExceptionFilter implements ExceptionFilter {
|
||||||
|
catch(exception: HttpException, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
const request = ctx.getRequest<Request>();
|
||||||
|
const status = exception.getStatus();
|
||||||
|
const message = exception.message;
|
||||||
|
|
||||||
|
response.status(status).json({
|
||||||
|
statusCode: status,
|
||||||
|
message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
path: request.url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
CanActivate,
|
CanActivate,
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
@ -21,7 +23,9 @@ export class AuthGuard implements CanActivate {
|
|||||||
const [, token] = request.headers.authorization.split(' ');
|
const [, token] = request.headers.authorization.split(' ');
|
||||||
const user = await lastValueFrom(
|
const user = await lastValueFrom(
|
||||||
this.authClient.send('auth.verify', { token }),
|
this.authClient.send('auth.verify', { token }),
|
||||||
);
|
).catch((err) => {
|
||||||
|
throw new HttpException(err.response, err.status || HttpStatus.FORBIDDEN);
|
||||||
|
});
|
||||||
|
|
||||||
// Add token contents to locals
|
// Add token contents to locals
|
||||||
response.locals.user = user;
|
response.locals.user = user;
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
CallHandler,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable, catchError, throwError } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CatchRpcExceptionInterceptor implements NestInterceptor {
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
return next.handle().pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
return throwError(
|
||||||
|
() =>
|
||||||
|
new HttpException(
|
||||||
|
err.response,
|
||||||
|
err.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
import { CatchRpcExceptionInterceptor } from './interceptors/catch-rpc-exception.interceptor';
|
||||||
|
import { HttpExceptionFilter } from './filters/exception.filter';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
@ -14,6 +16,9 @@ async function bootstrap() {
|
|||||||
const document = SwaggerModule.createDocument(app, config);
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
SwaggerModule.setup('api', app, document);
|
SwaggerModule.setup('api', app, document);
|
||||||
|
|
||||||
|
app.useGlobalInterceptors(new CatchRpcExceptionInterceptor());
|
||||||
|
app.useGlobalFilters(new HttpExceptionFilter());
|
||||||
|
|
||||||
await app.listen(3000);
|
await app.listen(3000);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
@ -13,7 +13,7 @@ services:
|
|||||||
container_name: fblx-postgres
|
container_name: fblx-postgres
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_MULTIPLE_DATABASES=auth,catalog,game,player,server,session
|
- POSTGRES_MULTIPLE_DATABASES=auth,catalog,game,player,server,session,bank,assets
|
||||||
- POSTGRES_USER=freeblox
|
- POSTGRES_USER=freeblox
|
||||||
- POSTGRES_PASSWORD=FREEBLOXDataBaseDEV@123
|
- POSTGRES_PASSWORD=FREEBLOXDataBaseDEV@123
|
||||||
volumes:
|
volumes:
|
||||||
@ -127,6 +127,40 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./apps:/usr/src/app/apps
|
- ./apps:/usr/src/app/apps
|
||||||
- ./libs:/usr/src/app/libs
|
- ./libs:/usr/src/app/libs
|
||||||
|
bank:
|
||||||
|
container_name: fblx-bank
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
args:
|
||||||
|
- SERVICE=bank
|
||||||
|
networks:
|
||||||
|
- fblx
|
||||||
|
environment:
|
||||||
|
- NATS_ENTRYPOINT=nats://nats:4222
|
||||||
|
- POSTGRES_HOST=postgres
|
||||||
|
- POSTGRES_USER=freeblox
|
||||||
|
- POSTGRES_PASSWORD=FREEBLOXDataBaseDEV@123
|
||||||
|
volumes:
|
||||||
|
- ./apps:/usr/src/app/apps
|
||||||
|
- ./libs:/usr/src/app/libs
|
||||||
|
assets:
|
||||||
|
container_name: fblx-assets
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
args:
|
||||||
|
- SERVICE=assets
|
||||||
|
networks:
|
||||||
|
- fblx
|
||||||
|
environment:
|
||||||
|
- NATS_ENTRYPOINT=nats://nats:4222
|
||||||
|
- POSTGRES_HOST=postgres
|
||||||
|
- POSTGRES_USER=freeblox
|
||||||
|
- POSTGRES_PASSWORD=FREEBLOXDataBaseDEV@123
|
||||||
|
volumes:
|
||||||
|
- ./apps:/usr/src/app/apps
|
||||||
|
- ./libs:/usr/src/app/libs
|
||||||
web-service:
|
web-service:
|
||||||
container_name: fblx-web-service
|
container_name: fblx-web-service
|
||||||
build:
|
build:
|
||||||
|
55
libs/shared/src/exception/rpc.exception.ts
Normal file
55
libs/shared/src/exception/rpc.exception.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { HttpStatus } from '@nestjs/common';
|
||||||
|
|
||||||
|
export class HttpRpcException {
|
||||||
|
isRpcException = true;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly error: string | object,
|
||||||
|
public status: HttpStatus,
|
||||||
|
public stack?: string,
|
||||||
|
) {
|
||||||
|
Error.captureStackTrace(this, HttpRpcException);
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus() {
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
getError() {
|
||||||
|
return this.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
get message() {
|
||||||
|
return this.error.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BadRequestRpcException extends HttpRpcException {
|
||||||
|
constructor(message: string | object) {
|
||||||
|
super(message, HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotFoundRpcException extends HttpRpcException {
|
||||||
|
constructor(message: string | object) {
|
||||||
|
super(message, HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ForbiddenRpcException extends HttpRpcException {
|
||||||
|
constructor(message: string | object) {
|
||||||
|
super(message, HttpStatus.FORBIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnauthorizedRpcException extends HttpRpcException {
|
||||||
|
constructor(message: string | object) {
|
||||||
|
super(message, HttpStatus.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PreconditionFailedRpcException extends HttpRpcException {
|
||||||
|
constructor(message: string | object) {
|
||||||
|
super(message, HttpStatus.PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
}
|
22
libs/shared/src/filters/rpc-exception.filter.ts
Normal file
22
libs/shared/src/filters/rpc-exception.filter.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {
|
||||||
|
Catch,
|
||||||
|
RpcExceptionFilter,
|
||||||
|
ArgumentsHost,
|
||||||
|
HttpException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable, throwError } from 'rxjs';
|
||||||
|
import { HttpRpcException } from '../exception/rpc.exception';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rethrow the custom exceptions
|
||||||
|
*/
|
||||||
|
@Catch(HttpRpcException)
|
||||||
|
export class HttpRpcExceptionFilter
|
||||||
|
implements RpcExceptionFilter<HttpRpcException>
|
||||||
|
{
|
||||||
|
catch(exception: HttpRpcException, host: ArgumentsHost): Observable<any> {
|
||||||
|
return throwError(
|
||||||
|
() => new HttpException(exception.error, exception.getStatus()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -7,3 +7,5 @@ export * from './database/make-knex';
|
|||||||
export * from './database/metaentity';
|
export * from './database/metaentity';
|
||||||
export * from './types/user-token.enum';
|
export * from './types/user-token.enum';
|
||||||
export * from './types/userinfo';
|
export * from './types/userinfo';
|
||||||
|
export * from './exception/rpc.exception';
|
||||||
|
export * from './filters/rpc-exception.filter';
|
||||||
|
@ -80,6 +80,24 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsConfigPath": "apps/catalog/tsconfig.app.json"
|
"tsConfigPath": "apps/catalog/tsconfig.app.json"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"bank": {
|
||||||
|
"type": "application",
|
||||||
|
"root": "apps/bank",
|
||||||
|
"entryFile": "main",
|
||||||
|
"sourceRoot": "apps/bank/src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsConfigPath": "apps/bank/tsconfig.app.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"assets": {
|
||||||
|
"type": "application",
|
||||||
|
"root": "apps/assets",
|
||||||
|
"entryFile": "main",
|
||||||
|
"sourceRoot": "apps/assets/src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsConfigPath": "apps/assets/tsconfig.app.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -12,7 +12,7 @@
|
|||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/apps/freeblox-web-service/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
@ -94,4 +94,4 @@
|
|||||||
"^@freeblox/shared(|/.*)$": "<rootDir>/libs/shared/src/$1"
|
"^@freeblox/shared(|/.*)$": "<rootDir>/libs/shared/src/$1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user