Compare commits
No commits in common. "529e4a96d737cc1ffe0881f3b0c4459e5f4775fc" and "efdbad4ed709d2829127f378ef7b3976ba1cc1b5" have entirely different histories.
529e4a96d7
...
efdbad4ed7
@ -2,7 +2,6 @@ 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 {
|
||||||
@ -12,24 +11,4 @@ export class AuthController {
|
|||||||
login({ body }: { body: LoginRequest }) {
|
login({ body }: { body: LoginRequest }) {
|
||||||
return this.authService.login(body);
|
return this.authService.login(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@MessagePattern('auth.loginByRefreshToken')
|
|
||||||
loginByRefreshToken({ token }: { token: string }) {
|
|
||||||
return this.authService.loginByRefreshToken(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
@MessagePattern('auth.verify')
|
|
||||||
verify({ token }: { token: string }) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -9,23 +9,6 @@ import knex from 'knex';
|
|||||||
import { security } from './config/security.config';
|
import { security } from './config/security.config';
|
||||||
import { keysProviders } from './providers/keys.providers';
|
import { keysProviders } from './providers/keys.providers';
|
||||||
import { JWTService } from './services/jwt.service';
|
import { JWTService } from './services/jwt.service';
|
||||||
import { UserEntity } from './database/entities/user.entity';
|
|
||||||
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';
|
|
||||||
import { RefreshService } from './services/refresh.service';
|
|
||||||
|
|
||||||
const entities = [
|
|
||||||
UserEntity,
|
|
||||||
UserTokenEntity,
|
|
||||||
BanEntity,
|
|
||||||
PrivilegeEntity,
|
|
||||||
RoleEntity,
|
|
||||||
];
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -36,24 +19,12 @@ const entities = [
|
|||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: (config: ConfigService) => ({
|
useFactory: (config: ConfigService) => config.get('typeorm'),
|
||||||
...config.get('typeorm'),
|
|
||||||
entities,
|
|
||||||
}),
|
}),
|
||||||
}),
|
|
||||||
TypeOrmModule.forFeature(entities),
|
|
||||||
ClientsModule.register([natsClient('auth')]),
|
ClientsModule.register([natsClient('auth')]),
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [
|
providers: [AuthService, ...keysProviders, JWTService],
|
||||||
...keysProviders,
|
|
||||||
JWTService,
|
|
||||||
OTPService,
|
|
||||||
BanService,
|
|
||||||
RoleService,
|
|
||||||
RefreshService,
|
|
||||||
AuthService,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class AuthModule implements OnModuleInit {
|
export class AuthModule implements OnModuleInit {
|
||||||
constructor(private readonly config: ConfigService) {}
|
constructor(private readonly config: ConfigService) {}
|
||||||
|
@ -2,11 +2,7 @@ import { registerAs } from '@nestjs/config';
|
|||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
|
|
||||||
export const security = registerAs('security', () => ({
|
export const security = registerAs('security', () => ({
|
||||||
jwtAlgorithm: String(process.env.JWT_ALGORITHM || 'RS512'),
|
algorithm: String(process.env.JWT_ALGORITHM || 'RS512'),
|
||||||
jwtTokenExpiry: Number(process.env.JWT_EXPIRY) || 60 * 60,
|
|
||||||
refreshTokenExpiry: Number(process.env.REFRESH_EXPIRY) || 30 * 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)),
|
||||||
secretKey: String(process.env.SECRET_KEY),
|
|
||||||
secretAlgorithm: String(process.env.REFRESH_ALGORITHM || 'A256CBC-HS512'),
|
|
||||||
}));
|
}));
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
import {
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
Entity,
|
|
||||||
JoinColumn,
|
|
||||||
ManyToOne,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { UserEntity } from './user.entity';
|
|
||||||
import { Exclude, Expose } from 'class-transformer';
|
|
||||||
|
|
||||||
@Entity('bans')
|
|
||||||
@Expose()
|
|
||||||
export class BanEntity {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@Column({ nullable: false })
|
|
||||||
reason: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
ip: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true, default: 32 })
|
|
||||||
cidr: number;
|
|
||||||
|
|
||||||
@Column({ type: 'timestamp', name: 'expires_at', nullable: true })
|
|
||||||
expiresAt: Date;
|
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at' })
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', nullable: true })
|
|
||||||
@JoinColumn({ name: 'user_id' })
|
|
||||||
@Exclude()
|
|
||||||
user: UserEntity;
|
|
||||||
|
|
||||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'admin_id' })
|
|
||||||
@Exclude()
|
|
||||||
admin: UserEntity;
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
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[];
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
import { UserTokenType } from '@freeblox/shared';
|
|
||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { UserEntity } from './user.entity';
|
|
||||||
|
|
||||||
@Entity('user_tokens')
|
|
||||||
export class UserTokenEntity {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@Column({ nullable: false, type: 'text' })
|
|
||||||
token: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true, type: 'text' })
|
|
||||||
nonce: string;
|
|
||||||
|
|
||||||
@Column({ type: 'enum', enum: UserTokenType, nullable: false })
|
|
||||||
type: UserTokenType;
|
|
||||||
|
|
||||||
@Column({ type: 'timestamp', name: 'expires_at', nullable: true })
|
|
||||||
expiresAt: Date;
|
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at' })
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'user_id' })
|
|
||||||
user: UserEntity;
|
|
||||||
|
|
||||||
@ManyToOne(() => UserTokenEntity, { onDelete: 'CASCADE', nullable: true })
|
|
||||||
@JoinColumn({ name: 'previous_id' })
|
|
||||||
previous: UserTokenEntity;
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
import { MetaEntity } from '@freeblox/shared';
|
|
||||||
import { Exclude, Expose } from 'class-transformer';
|
|
||||||
import { IsOptional, IsString } from 'class-validator';
|
|
||||||
import {
|
|
||||||
Column,
|
|
||||||
Entity,
|
|
||||||
JoinTable,
|
|
||||||
ManyToMany,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { PrivilegeEntity } from './privilege.entity';
|
|
||||||
import { RoleEntity } from './role.entity';
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@Entity('users')
|
|
||||||
export class UserEntity extends MetaEntity {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
@IsString()
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
@IsString()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
@IsString()
|
|
||||||
email: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
phone: string;
|
|
||||||
|
|
||||||
@Column({ length: 2 })
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
country: string;
|
|
||||||
|
|
||||||
@Column({ default: 'en', length: 2 })
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
language: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
@IsString()
|
|
||||||
@Exclude()
|
|
||||||
password: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true, name: 'display_name' })
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
displayName: string;
|
|
||||||
|
|
||||||
@Column({ default: false })
|
|
||||||
@IsString()
|
|
||||||
verified: boolean;
|
|
||||||
|
|
||||||
@Column({ default: true })
|
|
||||||
@IsString()
|
|
||||||
activated: boolean;
|
|
||||||
|
|
||||||
@Column({ name: 'login_at' })
|
|
||||||
@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[];
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
|
||||||
return knex.schema.createTable('bans', (table) => {
|
|
||||||
table.increments('id').primary();
|
|
||||||
|
|
||||||
table.text('reason').notNullable();
|
|
||||||
|
|
||||||
table.string('ip', 255).nullable();
|
|
||||||
table.integer('cidr', 2).nullable().defaultTo(32).unsigned();
|
|
||||||
|
|
||||||
table.uuid('user_id').nullable();
|
|
||||||
table.uuid('admin_id').nullable();
|
|
||||||
|
|
||||||
table.timestamp('expires_at').nullable();
|
|
||||||
table.timestamp('created_at').notNullable().defaultTo('now()');
|
|
||||||
|
|
||||||
table.foreign('user_id').references('users.id').onDelete('CASCADE');
|
|
||||||
table.foreign('admin_id').references('users.id').onDelete('SET NULL');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
|
||||||
return knex.schema.dropTable('bans');
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
|
||||||
return knex.schema.createTable('user_tokens', (table) => {
|
|
||||||
table.increments('id').primary();
|
|
||||||
|
|
||||||
table.text('token').notNullable();
|
|
||||||
table.text('nonce').nullable();
|
|
||||||
table
|
|
||||||
.enum('type', [
|
|
||||||
'generic',
|
|
||||||
'activation',
|
|
||||||
'deactivation',
|
|
||||||
'password',
|
|
||||||
'login',
|
|
||||||
'gdpr',
|
|
||||||
'totp',
|
|
||||||
'public_key',
|
|
||||||
'recovery',
|
|
||||||
'refresh',
|
|
||||||
])
|
|
||||||
.notNullable();
|
|
||||||
|
|
||||||
table.uuid('user_id').notNullable();
|
|
||||||
table.integer('previous_id').nullable().unsigned();
|
|
||||||
|
|
||||||
table.timestamp('expires_at').nullable();
|
|
||||||
table.timestamp('created_at').notNullable().defaultTo('now()');
|
|
||||||
|
|
||||||
table.foreign('user_id').references('users.id').onDelete('CASCADE');
|
|
||||||
table
|
|
||||||
.foreign('previous_id')
|
|
||||||
.references('user_tokens.id')
|
|
||||||
.onDelete('CASCADE');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
|
||||||
return knex.schema.dropTable('user_tokens');
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
|
||||||
return knex.schema.createTable('privileges', (table) => {
|
|
||||||
table.increments('id').primary();
|
|
||||||
table.text('privilege').notNullable();
|
|
||||||
table.boolean('automatic').defaultTo(false);
|
|
||||||
|
|
||||||
table.uuid('created_by').nullable();
|
|
||||||
table.uuid('updated_by').nullable();
|
|
||||||
|
|
||||||
table.timestamps(true, true);
|
|
||||||
|
|
||||||
table.foreign('created_by').references('users.id').onDelete('SET NULL');
|
|
||||||
table.foreign('updated_by').references('users.id').onDelete('SET NULL');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
|
||||||
return knex.schema.dropTable('privileges');
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
|
||||||
return knex.schema.createTable('user_privilege', (table) => {
|
|
||||||
table.increments('id').primary();
|
|
||||||
table.integer('privilege_id').nullable().unsigned();
|
|
||||||
table.uuid('user_id').nullable();
|
|
||||||
table
|
|
||||||
.foreign('privilege_id')
|
|
||||||
.references('privileges.id')
|
|
||||||
.onDelete('CASCADE');
|
|
||||||
table.foreign('user_id').references('users.id').onDelete('CASCADE');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
|
||||||
return knex.schema.dropTable('user_privilege');
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
|
||||||
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);
|
|
||||||
|
|
||||||
table.uuid('created_by').nullable();
|
|
||||||
table.uuid('updated_by').nullable();
|
|
||||||
|
|
||||||
table.timestamps(true, true);
|
|
||||||
|
|
||||||
table.foreign('parent_id').references('roles.id').onDelete('SET NULL');
|
|
||||||
table.foreign('created_by').references('users.id').onDelete('SET NULL');
|
|
||||||
table.foreign('updated_by').references('users.id').onDelete('SET NULL');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
|
||||||
return knex.schema.dropTable('roles');
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
|
||||||
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');
|
|
||||||
table.foreign('user_id').references('users.id').onDelete('CASCADE');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
|
||||||
return knex.schema.dropTable('user_role');
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
|
||||||
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');
|
|
||||||
table
|
|
||||||
.foreign('privilege_id')
|
|
||||||
.references('privileges.id')
|
|
||||||
.onDelete('CASCADE');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
|
||||||
return knex.schema.dropTable('role_privilege');
|
|
||||||
}
|
|
@ -1,200 +0,0 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
|
|
||||||
export async function seed(knex: Knex): Promise<void> {
|
|
||||||
const initialRoles = [
|
|
||||||
{
|
|
||||||
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: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const initialPrivileges = [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
name: 'web',
|
|
||||||
roles: ['player', 'reduced'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
name: 'play',
|
|
||||||
roles: ['player'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
name: 'shop',
|
|
||||||
roles: ['player'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
name: 'build',
|
|
||||||
roles: ['player'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
name: 'trade',
|
|
||||||
roles: ['player'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
name: 'oidc',
|
|
||||||
roles: ['player'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
name: 'host',
|
|
||||||
roles: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
name: 'ban',
|
|
||||||
roles: ['moderator', 'admin'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
name: 'permaban',
|
|
||||||
roles: ['admin'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
name: 'stopserver',
|
|
||||||
roles: ['admin'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
name: 'banserver',
|
|
||||||
roles: ['admin'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
name: 'root',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const role of initialRoles) {
|
|
||||||
const exists = await knex('roles').where({
|
|
||||||
role: role.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (exists?.length) {
|
|
||||||
role.id = exists[0].id;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parentId: number | null = null;
|
|
||||||
if (role.parent) {
|
|
||||||
const findRole = initialRoles.find(
|
|
||||||
(parent) => parent.name === role.parent,
|
|
||||||
);
|
|
||||||
if (findRole) {
|
|
||||||
parentId = findRole.id;
|
|
||||||
role.path += findRole.path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [created] = await knex('roles')
|
|
||||||
.insert([
|
|
||||||
{
|
|
||||||
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) {
|
|
||||||
const exists = await knex('privileges').where({
|
|
||||||
privilege: privilege.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (exists?.length) {
|
|
||||||
privilege.id = exists[0].id;
|
|
||||||
} else {
|
|
||||||
const [created] = await knex('privileges')
|
|
||||||
.insert([
|
|
||||||
{
|
|
||||||
privilege: privilege.name,
|
|
||||||
automatic: false,
|
|
||||||
created_at: new Date(),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.returning(['id']);
|
|
||||||
privilege.id = created.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (privilege.roles?.length) {
|
|
||||||
for (const role of privilege.roles) {
|
|
||||||
const foundRole = initialRoles.find((item) => item.name === role);
|
|
||||||
if (!foundRole) continue;
|
|
||||||
const body = {
|
|
||||||
role_id: foundRole.id,
|
|
||||||
privilege_id: privilege.id,
|
|
||||||
};
|
|
||||||
const exists = await knex('role_privilege').where(body);
|
|
||||||
if (exists?.length) continue;
|
|
||||||
await knex('role_privilege').insert(body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add roles to initial user
|
|
||||||
const userExists = await knex('users').where({ username: 'freeblox' });
|
|
||||||
if (!userExists?.length) return;
|
|
||||||
|
|
||||||
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,5 +1,4 @@
|
|||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
totpToken?: string;
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
export interface TokenResponse {
|
|
||||||
token: string;
|
|
||||||
expires_in: number;
|
|
||||||
}
|
|
@ -16,12 +16,6 @@ export const keysProviders = [
|
|||||||
.readFile(config.get('security.privateKeyPath'), 'utf-8')
|
.readFile(config.get('security.privateKeyPath'), 'utf-8')
|
||||||
.then((key) => jose.importPKCS8(key, 'RS512')),
|
.then((key) => jose.importPKCS8(key, 'RS512')),
|
||||||
},
|
},
|
||||||
<FactoryProvider>{
|
|
||||||
provide: 'APP_SECRET_KEY',
|
|
||||||
inject: [ConfigService],
|
|
||||||
useFactory: async (config: ConfigService) =>
|
|
||||||
jose.base64url.decode(config.get('security.secretKey')),
|
|
||||||
},
|
|
||||||
<FactoryProvider>{
|
<FactoryProvider>{
|
||||||
provide: 'APP_PUBLIC_KEY',
|
provide: 'APP_PUBLIC_KEY',
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
|
@ -1,201 +1,9 @@
|
|||||||
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 { ILike, Repository } from 'typeorm';
|
|
||||||
import { UserEntity } from '../database/entities/user.entity';
|
|
||||||
import { compare } from 'bcrypt';
|
|
||||||
import { instanceToPlain } from 'class-transformer';
|
|
||||||
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';
|
|
||||||
import { RefreshService } from './refresh.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
constructor(
|
|
||||||
private readonly jwtService: JWTService,
|
|
||||||
private readonly otpService: OTPService,
|
|
||||||
private readonly banService: BanService,
|
|
||||||
private readonly roleService: RoleService,
|
|
||||||
private readonly refreshService: RefreshService,
|
|
||||||
@InjectRepository(UserEntity)
|
|
||||||
private readonly userRepository: Repository<UserEntity>,
|
|
||||||
private readonly config: ConfigService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Login by username/email and password
|
|
||||||
* @param body Username/email and password
|
|
||||||
* @returns JWT token
|
|
||||||
*/
|
|
||||||
async login(body: LoginRequest) {
|
async login(body: LoginRequest) {
|
||||||
if (!body.email || !body.password) {
|
return { test: body.email };
|
||||||
throw new BadRequestException('Invalid username or password');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent wildcards
|
|
||||||
const userInput = body.email?.replace(/%/g, '');
|
|
||||||
const userEntity = await this.userRepository.findOne({
|
|
||||||
where: [
|
|
||||||
{
|
|
||||||
username: ILike(userInput),
|
|
||||||
activated: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
email: ILike(userInput),
|
|
||||||
activated: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// User not found
|
|
||||||
if (!userEntity) {
|
|
||||||
throw new BadRequestException('Invalid username or password');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare passwords
|
|
||||||
const passwordMatch = await compare(body.password, userEntity.password);
|
|
||||||
if (!passwordMatch) {
|
|
||||||
throw new BadRequestException('Invalid username or password');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check TOTP
|
|
||||||
const userOTPToken = await this.otpService.getUserTOTP(userEntity);
|
|
||||||
if (userOTPToken) {
|
|
||||||
if (!body.totpToken) {
|
|
||||||
throw new PreconditionFailedException('TOTP Token required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const validate = this.otpService.validateTOTP(
|
|
||||||
userOTPToken.token,
|
|
||||||
body.totpToken,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!validate) {
|
|
||||||
throw new ForbiddenException('Invalid TOTP Token');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Issue access token
|
|
||||||
const exp = this.config.get('security.jwtTokenExpiry');
|
|
||||||
const issuedToken = await this.issueToken(userEntity);
|
|
||||||
|
|
||||||
// Issue refresh token
|
|
||||||
const refreshToken = await this.refreshService.issueRefreshToken(
|
|
||||||
userEntity,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set login time to now
|
|
||||||
await this.userRepository.update(
|
|
||||||
{ id: userEntity.id },
|
|
||||||
{ loginAt: new Date() },
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
token: issuedToken,
|
|
||||||
refresh: refreshToken,
|
|
||||||
expires_in: exp,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async loginByRefreshToken(token: string) {
|
|
||||||
const refreshToken = await this.refreshService.useRefreshToken(token);
|
|
||||||
const userEntity = refreshToken.user;
|
|
||||||
|
|
||||||
// Issue new access token
|
|
||||||
const exp = this.config.get('security.jwtTokenExpiry');
|
|
||||||
const issuedToken = await this.issueToken(userEntity);
|
|
||||||
|
|
||||||
// Set login time to now
|
|
||||||
await this.userRepository.update(
|
|
||||||
{ id: userEntity.id },
|
|
||||||
{ loginAt: new Date() },
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
token: issuedToken,
|
|
||||||
refresh: refreshToken.token,
|
|
||||||
expires_in: exp,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user from token
|
|
||||||
* @param token JWT Token
|
|
||||||
* @returns User entity
|
|
||||||
*/
|
|
||||||
async getUserFromToken(token: string) {
|
|
||||||
const tokenInfo = await this.verifyToken(token);
|
|
||||||
const user = await this.userRepository.findOneByOrFail({
|
|
||||||
id: tokenInfo.sub,
|
|
||||||
activated: true,
|
|
||||||
});
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
async getUserBans(userInfo: UserInfo) {
|
|
||||||
const user = await this.userRepository.findOneByOrFail({
|
|
||||||
id: userInfo.sub,
|
|
||||||
activated: true,
|
|
||||||
});
|
|
||||||
const bans = await this.banService.getAllBansForUser(user);
|
|
||||||
return instanceToPlain(bans);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async issueToken(user: UserEntity) {
|
|
||||||
// Check for active ban
|
|
||||||
const bans = await this.banService.getActiveBansForUser(user);
|
|
||||||
const banned = !!bans.length;
|
|
||||||
|
|
||||||
// Get all privileges applicable to user
|
|
||||||
const privileges = await this.roleService.getUserPrivileges(user);
|
|
||||||
|
|
||||||
// Issue new token
|
|
||||||
return this.jwtService.sign({
|
|
||||||
sub: user.id,
|
|
||||||
username: user.username,
|
|
||||||
display_name: user.displayName,
|
|
||||||
language: user.language,
|
|
||||||
banned: banned,
|
|
||||||
privileges: banned
|
|
||||||
? []
|
|
||||||
: privileges.map((privilege) => privilege.privilege),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { BanEntity } from '../database/entities/ban.entity';
|
|
||||||
import { MoreThan, Repository } from 'typeorm';
|
|
||||||
import { UserEntity } from '../database/entities/user.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class BanService {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(BanEntity)
|
|
||||||
private readonly banRepository: Repository<BanEntity>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async getActiveBansForUser(user: UserEntity) {
|
|
||||||
return this.banRepository.find({
|
|
||||||
where: [
|
|
||||||
{
|
|
||||||
expiresAt: null,
|
|
||||||
user: { id: user.id },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expiresAt: MoreThan(new Date()),
|
|
||||||
user: { id: user.id },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAllBansForUser(user: UserEntity) {
|
|
||||||
return this.banRepository.findBy({
|
|
||||||
user: { id: user.id },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +1,30 @@
|
|||||||
import { ForbiddenException, Inject, Injectable } from '@nestjs/common';
|
import { ForbiddenException, 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 { JWK, KeyLike, SignJWT, jwtVerify } from 'jose';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JWTService {
|
export class JWTService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('APP_PRIVATE_KEY') private readonly privateKey: KeyLike,
|
@Inject('APP_PRIVATE_KEY') private readonly privateKey: KeyLike,
|
||||||
@Inject('APP_SECRET_KEY') private readonly secretKey: KeyLike,
|
|
||||||
@Inject('APP_PUBLIC_KEY') private readonly publicKey: KeyLike,
|
@Inject('APP_PUBLIC_KEY') private readonly publicKey: KeyLike,
|
||||||
@Inject('APP_PUBLIC_KEY_JWK') private readonly publicKeyJWK: JWK,
|
@Inject('APP_PUBLIC_KEY_JWK') private readonly publicKeyJWK: JWK,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
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.jwtAlgorithm');
|
const alg = this.config.get('security.algorithm');
|
||||||
const exp =
|
|
||||||
this.config.get('security.jwtTokenExpiry') +
|
|
||||||
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(exp)
|
.setExpirationTime('8d')
|
||||||
.sign(this.privateKey);
|
.sign(this.privateKey);
|
||||||
return jwt;
|
return jwt;
|
||||||
}
|
}
|
||||||
|
|
||||||
async verify(jwt: string, audience = 'urn:freeblox:service') {
|
async verify(jwt: string, audience = 'urn:freeblox:service') {
|
||||||
const alg = this.config.get('security.jwtAlgorithm');
|
const alg = this.config.get('security.algorithm');
|
||||||
const { payload, protectedHeader } = await jwtVerify(jwt, this.publicKey, {
|
const { payload, protectedHeader } = await jwtVerify(jwt, this.publicKey, {
|
||||||
issuer: 'urn:freeblox:auth',
|
issuer: 'urn:freeblox:auth',
|
||||||
audience,
|
audience,
|
||||||
@ -37,33 +33,4 @@ export class JWTService {
|
|||||||
throw new ForbiddenException('Provided JWT contains invalid headers.');
|
throw new ForbiddenException('Provided JWT contains invalid headers.');
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
async encrypt(
|
|
||||||
data: Record<string, unknown>,
|
|
||||||
audience = 'urn:freeblox:service',
|
|
||||||
) {
|
|
||||||
const alg = this.config.get('security.secretAlgorithm');
|
|
||||||
const exp =
|
|
||||||
this.config.get('security.refreshTokenExpiry') +
|
|
||||||
Math.floor(Date.now() / 1000);
|
|
||||||
const jwt = await new EncryptJWT(data)
|
|
||||||
.setProtectedHeader({ alg: 'dir', enc: alg })
|
|
||||||
.setIssuedAt()
|
|
||||||
.setIssuer('urn:freeblox:auth')
|
|
||||||
.setAudience(audience)
|
|
||||||
.setExpirationTime(exp)
|
|
||||||
.encrypt(this.secretKey);
|
|
||||||
return jwt;
|
|
||||||
}
|
|
||||||
|
|
||||||
async decrypt(jwt: string, audience = 'urn:freeblox:service') {
|
|
||||||
const alg = this.config.get('security.secretAlgorithm');
|
|
||||||
const { payload, protectedHeader } = await jwtDecrypt(jwt, this.secretKey, {
|
|
||||||
issuer: 'urn:freeblox:auth',
|
|
||||||
audience,
|
|
||||||
});
|
|
||||||
if (protectedHeader.enc !== alg)
|
|
||||||
throw new ForbiddenException('Provided JWT contains invalid headers.');
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { UserTokenEntity } from '../database/entities/user-token.entity';
|
|
||||||
import { authenticator as totp } from 'otplib';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { UserEntity } from '../database/entities/user.entity';
|
|
||||||
import { UserTokenType, generateString } from '@freeblox/shared';
|
|
||||||
|
|
||||||
totp.options = {
|
|
||||||
window: 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class OTPService {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(UserTokenEntity)
|
|
||||||
private readonly userTokenRepository: Repository<UserTokenEntity>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the user has TOTP enabled
|
|
||||||
* @param user User object
|
|
||||||
* @returns true if the user has TOTP enabled
|
|
||||||
*/
|
|
||||||
public async userHasTOTP(user: UserEntity): Promise<boolean> {
|
|
||||||
return !!(await this.getUserTOTP(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the TOTP token of a user
|
|
||||||
* @param user User object
|
|
||||||
* @returns TOTP token
|
|
||||||
*/
|
|
||||||
public async getUserTOTP(user: UserEntity): Promise<UserTokenEntity> {
|
|
||||||
return this.userTokenRepository.findOne({
|
|
||||||
where: { user: { id: user.id }, type: UserTokenType.TOTP },
|
|
||||||
relations: ['user'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public validateTOTP(secret: string, token: string): boolean {
|
|
||||||
return totp.verify({ token, secret });
|
|
||||||
}
|
|
||||||
|
|
||||||
public getTOTPURL(secret: string, username: string): string {
|
|
||||||
return totp.keyuri(username, 'Freeblox', secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
public createTOTPSecret(): string {
|
|
||||||
return totp.generateSecret();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async activateTOTP(
|
|
||||||
user: UserEntity,
|
|
||||||
secret: string,
|
|
||||||
): Promise<UserTokenEntity[]> {
|
|
||||||
const totp = new UserTokenEntity();
|
|
||||||
const recovery = new UserTokenEntity();
|
|
||||||
|
|
||||||
totp.user = user;
|
|
||||||
totp.token = secret;
|
|
||||||
totp.type = UserTokenType.TOTP;
|
|
||||||
|
|
||||||
recovery.user = user;
|
|
||||||
recovery.token = Array.from({ length: 8 }, () => generateString(8)).join(
|
|
||||||
' ',
|
|
||||||
);
|
|
||||||
recovery.type = UserTokenType.RECOVERY;
|
|
||||||
|
|
||||||
await this.userTokenRepository.save(totp);
|
|
||||||
await this.userTokenRepository.save(recovery);
|
|
||||||
|
|
||||||
return [totp, recovery];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deactivateTOTP(token: UserTokenEntity): Promise<void> {
|
|
||||||
if (!token) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.userTokenRepository.delete({
|
|
||||||
type: UserTokenType.RECOVERY,
|
|
||||||
user: { id: token.user.id },
|
|
||||||
});
|
|
||||||
await this.userTokenRepository.remove(token);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,120 +0,0 @@
|
|||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { UserTokenEntity } from '../database/entities/user-token.entity';
|
|
||||||
import { JWTService } from './jwt.service';
|
|
||||||
import { UserEntity } from '../database/entities/user.entity';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { UserTokenType, generateString } from '@freeblox/shared';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RefreshService {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(UserTokenEntity)
|
|
||||||
private readonly userTokenRepository: Repository<UserTokenEntity>,
|
|
||||||
private readonly jwt: JWTService,
|
|
||||||
private readonly config: ConfigService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find new refresh token for old token
|
|
||||||
* @param token Old token
|
|
||||||
* @returns New Token
|
|
||||||
*/
|
|
||||||
async findNewIteration(token: UserTokenEntity) {
|
|
||||||
return this.userTokenRepository.findOne({
|
|
||||||
where: {
|
|
||||||
previous: { id: token.id },
|
|
||||||
type: UserTokenType.REFRESH,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consume a refresh token.
|
|
||||||
* @param token Refresh token
|
|
||||||
* @returns New refresh token
|
|
||||||
*/
|
|
||||||
async useRefreshToken(token: string) {
|
|
||||||
if (!token) throw new UnauthorizedException('Invalid refresh token');
|
|
||||||
const decrypted = await this.jwt.decrypt(token);
|
|
||||||
|
|
||||||
if (!decrypted.token || !decrypted.sub)
|
|
||||||
throw new UnauthorizedException('Invalid refresh token');
|
|
||||||
|
|
||||||
const tokenEntity = await this.userTokenRepository.findOneOrFail({
|
|
||||||
where: {
|
|
||||||
token: decrypted.token as string,
|
|
||||||
type: UserTokenType.REFRESH,
|
|
||||||
},
|
|
||||||
relations: ['user'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (decrypted.sub !== tokenEntity.user.id)
|
|
||||||
throw new UnauthorizedException('Invalid refresh token');
|
|
||||||
|
|
||||||
// Using an expired refresh token
|
|
||||||
if (tokenEntity.expiresAt.getTime() < Date.now()) {
|
|
||||||
const newIteration = await this.findNewIteration(tokenEntity);
|
|
||||||
// We have already issued a new refresh token, this is probably stolen
|
|
||||||
// ..so we delete the whole tree.
|
|
||||||
if (newIteration) {
|
|
||||||
await this.userTokenRepository.remove(tokenEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new UnauthorizedException('Invalid refresh token');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark old token as expired
|
|
||||||
await this.userTokenRepository.update(
|
|
||||||
{ id: tokenEntity.id },
|
|
||||||
{ expiresAt: new Date() },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Issue a new refresh token
|
|
||||||
const newToken = await this.issueRefreshToken(
|
|
||||||
tokenEntity.user,
|
|
||||||
tokenEntity,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
token: newToken,
|
|
||||||
user: tokenEntity.user,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Issue a refresh token.
|
|
||||||
* @param user User
|
|
||||||
* @param previous Previous refresh token
|
|
||||||
* @returns New Refresh token
|
|
||||||
*/
|
|
||||||
async issueRefreshToken(user: UserEntity, previous?: UserTokenEntity) {
|
|
||||||
const newRefreshToken = await this.createRefreshToken(user);
|
|
||||||
const expiry = new Date(
|
|
||||||
Date.now() + this.config.get('security.refreshTokenExpiry') * 1000,
|
|
||||||
);
|
|
||||||
await this.userTokenRepository.save({
|
|
||||||
token: newRefreshToken.token,
|
|
||||||
user,
|
|
||||||
type: UserTokenType.REFRESH,
|
|
||||||
expiresAt: expiry,
|
|
||||||
previous,
|
|
||||||
});
|
|
||||||
return newRefreshToken.encrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate refresh token and encrypt it
|
|
||||||
* @param user User to issue the token to
|
|
||||||
* @returns Encrypted and unencrypted tokens
|
|
||||||
*/
|
|
||||||
private async createRefreshToken(user: UserEntity) {
|
|
||||||
const token = generateString(512);
|
|
||||||
const encrypted = await this.jwt.encrypt({ sub: user.id, token });
|
|
||||||
return {
|
|
||||||
encrypted,
|
|
||||||
token,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
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, []];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
|
||||||
|
|
||||||
export const User = createParamDecorator(
|
|
||||||
(data: unknown, ctx: ExecutionContext) => {
|
|
||||||
const response = ctx.switchToHttp().getResponse();
|
|
||||||
return response.locals.user;
|
|
||||||
},
|
|
||||||
);
|
|
@ -1,30 +0,0 @@
|
|||||||
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,7 +9,6 @@ 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,64 +2,25 @@ 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 {
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
ApiBearerAuth,
|
|
||||||
ApiOkResponse,
|
|
||||||
ApiOperation,
|
|
||||||
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';
|
|
||||||
import { LoginResponseDto } from './dtos/login-response.dto';
|
|
||||||
import { LoginByRefreshTokenDto } from './dtos/login-refresh-token.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 {
|
||||||
constructor(@Inject('auth') private auth: ClientProxy) {}
|
constructor(@Inject('auth') private auth: ClientProxy) {}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
@ApiOperation({ summary: 'Login by username or email and password' })
|
|
||||||
@ApiOkResponse({ type: LoginResponseDto })
|
|
||||||
async login(@Body() body: LoginDto) {
|
async login(@Body() body: LoginDto) {
|
||||||
return this.auth.send('auth.login', { body });
|
return this.auth.send('auth.login', { body });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('refresh')
|
|
||||||
@ApiOperation({ summary: 'Login by refresh token' })
|
|
||||||
@ApiOkResponse({ type: LoginResponseDto })
|
|
||||||
async refresh(@Body() body: LoginByRefreshTokenDto) {
|
|
||||||
return this.auth.send('auth.loginByRefreshToken', { token: body.token });
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('me')
|
|
||||||
@ApiOperation({ summary: 'Current user information' })
|
|
||||||
@ApiOkResponse({ type: UserDto })
|
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
async myInfo(@User() user: UserInfo): Promise<UserDto> {
|
|
||||||
return lastValueFrom(this.auth.send('auth.getUserById', { id: user.sub }));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('bans')
|
|
||||||
@ApiOperation({ summary: 'Current user ban history' })
|
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
async banInfo(@User() user: UserInfo) {
|
|
||||||
return lastValueFrom(this.auth.send('auth.getUserBans', { user }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class LoginByRefreshTokenDto {
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
token: string;
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
|
|
||||||
export class LoginResponseDto {
|
|
||||||
@ApiProperty()
|
|
||||||
token: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
refresh: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
expires_in: number;
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -37,7 +37,6 @@ services:
|
|||||||
- POSTGRES_PASSWORD=FREEBLOXDataBaseDEV@123
|
- POSTGRES_PASSWORD=FREEBLOXDataBaseDEV@123
|
||||||
- PRIVATE_KEY_FILE=private/jwt.private.pem
|
- PRIVATE_KEY_FILE=private/jwt.private.pem
|
||||||
- PUBLIC_KEY_FILE=private/jwt.public.pem
|
- PUBLIC_KEY_FILE=private/jwt.public.pem
|
||||||
- SECRET_KEY=mkt9Hngcmhbd9wX4EzGbGysDWzCo793XvvswOS+wolTVM83I1K2b/j41WwsCfsv1iS901N2rTHu2hZHbsYO3RQ==
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./apps:/usr/src/app/apps
|
- ./apps:/usr/src/app/apps
|
||||||
- ./libs:/usr/src/app/libs
|
- ./libs:/usr/src/app/libs
|
||||||
|
@ -12,5 +12,6 @@ export const makeTypeOrm = (database: string) =>
|
|||||||
username: String(process.env.POSTGRES_USER),
|
username: String(process.env.POSTGRES_USER),
|
||||||
password: String(process.env.POSTGRES_PASSWORD),
|
password: String(process.env.POSTGRES_PASSWORD),
|
||||||
database,
|
database,
|
||||||
|
autoLoadEntities: true,
|
||||||
} as TypeOrmModuleOptions),
|
} as TypeOrmModuleOptions),
|
||||||
);
|
);
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
import { Exclude } from 'class-transformer';
|
|
||||||
import { Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
|
||||||
|
|
||||||
export class MetaEntity {
|
|
||||||
@CreateDateColumn({ name: 'created_at' })
|
|
||||||
@Exclude()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn({ name: 'updated_at' })
|
|
||||||
@Exclude()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserMetaEntity extends MetaEntity {
|
|
||||||
@Column({ name: 'created_by' })
|
|
||||||
@Exclude()
|
|
||||||
createdBy: string;
|
|
||||||
|
|
||||||
@Column({ name: 'updated_by' })
|
|
||||||
@Exclude()
|
|
||||||
updatedBy: string;
|
|
||||||
}
|
|
@ -1,9 +1,5 @@
|
|||||||
export * from './shared.module';
|
export * from './shared.module';
|
||||||
export * from './shared.service';
|
export * from './shared.service';
|
||||||
export * from './utils/nats-client';
|
export * from './utils/nats-client';
|
||||||
export * from './utils/tokens';
|
|
||||||
export * from './database/make-typeorm';
|
export * from './database/make-typeorm';
|
||||||
export * from './database/make-knex';
|
export * from './database/make-knex';
|
||||||
export * from './database/metaentity';
|
|
||||||
export * from './types/user-token.enum';
|
|
||||||
export * from './types/userinfo';
|
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
export enum UserTokenType {
|
|
||||||
GENERIC = 'generic',
|
|
||||||
ACTIVATION = 'activation',
|
|
||||||
DEACTIVATION = 'deactivation',
|
|
||||||
PASSWORD = 'password',
|
|
||||||
LOGIN = 'login',
|
|
||||||
GDPR = 'gdpr',
|
|
||||||
TOTP = 'totp',
|
|
||||||
PUBLIC_KEY = 'public_key',
|
|
||||||
RECOVERY = 'recovery',
|
|
||||||
REFRESH = 'refresh',
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
export interface UserInfo {
|
|
||||||
sub: string;
|
|
||||||
privileges?: string[];
|
|
||||||
username: string;
|
|
||||||
display_name: string;
|
|
||||||
language: string;
|
|
||||||
banned?: boolean;
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
import * as crypto from 'crypto';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
|
|
||||||
export const generateString = (length: number): string =>
|
|
||||||
crypto.randomBytes(length).toString('hex').slice(0, length);
|
|
||||||
|
|
||||||
export const generateSecret = (): string =>
|
|
||||||
crypto.randomBytes(256 / 8).toString('hex');
|
|
||||||
|
|
||||||
export const insecureHash = (input: string): string =>
|
|
||||||
crypto.createHash('md5').update(input).digest('hex');
|
|
||||||
|
|
||||||
export const createUUID = (): string => v4();
|
|
@ -37,13 +37,11 @@
|
|||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"knex": "^2.4.2",
|
"knex": "^2.4.2",
|
||||||
"nats": "^2.15.1",
|
"nats": "^2.15.1",
|
||||||
"otplib": "^12.0.1",
|
|
||||||
"pg": "^8.11.1",
|
"pg": "^8.11.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"typeorm": "^0.3.17",
|
"typeorm": "^0.3.17"
|
||||||
"uuid": "^9.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.5",
|
"@nestjs/cli": "^10.0.5",
|
||||||
|
@ -53,9 +53,6 @@ dependencies:
|
|||||||
nats:
|
nats:
|
||||||
specifier: ^2.15.1
|
specifier: ^2.15.1
|
||||||
version: 2.15.1
|
version: 2.15.1
|
||||||
otplib:
|
|
||||||
specifier: ^12.0.1
|
|
||||||
version: 12.0.1
|
|
||||||
pg:
|
pg:
|
||||||
specifier: ^8.11.1
|
specifier: ^8.11.1
|
||||||
version: 8.11.1
|
version: 8.11.1
|
||||||
@ -71,9 +68,6 @@ dependencies:
|
|||||||
typeorm:
|
typeorm:
|
||||||
specifier: ^0.3.17
|
specifier: ^0.3.17
|
||||||
version: 0.3.17(pg@8.11.1)(ts-node@10.9.1)
|
version: 0.3.17(pg@8.11.1)(ts-node@10.9.1)
|
||||||
uuid:
|
|
||||||
specifier: ^9.0.0
|
|
||||||
version: 9.0.0
|
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@nestjs/cli':
|
'@nestjs/cli':
|
||||||
@ -1242,39 +1236,6 @@ packages:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
/@otplib/core@12.0.1:
|
|
||||||
resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@otplib/plugin-crypto@12.0.1:
|
|
||||||
resolution: {integrity: sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==}
|
|
||||||
dependencies:
|
|
||||||
'@otplib/core': 12.0.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@otplib/plugin-thirty-two@12.0.1:
|
|
||||||
resolution: {integrity: sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==}
|
|
||||||
dependencies:
|
|
||||||
'@otplib/core': 12.0.1
|
|
||||||
thirty-two: 1.0.2
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@otplib/preset-default@12.0.1:
|
|
||||||
resolution: {integrity: sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==}
|
|
||||||
dependencies:
|
|
||||||
'@otplib/core': 12.0.1
|
|
||||||
'@otplib/plugin-crypto': 12.0.1
|
|
||||||
'@otplib/plugin-thirty-two': 12.0.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@otplib/preset-v11@12.0.1:
|
|
||||||
resolution: {integrity: sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==}
|
|
||||||
dependencies:
|
|
||||||
'@otplib/core': 12.0.1
|
|
||||||
'@otplib/plugin-crypto': 12.0.1
|
|
||||||
'@otplib/plugin-thirty-two': 12.0.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@pkgjs/parseargs@0.11.0:
|
/@pkgjs/parseargs@0.11.0:
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@ -4558,14 +4519,6 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/otplib@12.0.1:
|
|
||||||
resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==}
|
|
||||||
dependencies:
|
|
||||||
'@otplib/core': 12.0.1
|
|
||||||
'@otplib/preset-default': 12.0.1
|
|
||||||
'@otplib/preset-v11': 12.0.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/p-limit@2.3.0:
|
/p-limit@2.3.0:
|
||||||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -5449,11 +5402,6 @@ packages:
|
|||||||
any-promise: 1.3.0
|
any-promise: 1.3.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/thirty-two@1.0.2:
|
|
||||||
resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==}
|
|
||||||
engines: {node: '>=0.2.6'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/through@2.3.8:
|
/through@2.3.8:
|
||||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
Loading…
Reference in New Issue
Block a user