updates
This commit is contained in:
parent
001dc0b63a
commit
fb4154c9e5
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -8,7 +8,7 @@
|
||||
"html"
|
||||
],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"[sql]": {
|
||||
"editor.defaultFormatter": "adpyke.vscode-sql-formatter"
|
||||
|
4968
package-lock.json
generated
4968
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
113
package.json
113
package.json
@ -25,87 +25,90 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@icynet/oauth2-provider": "^1.0.8",
|
||||
"@nestjs/common": "^10.2.7",
|
||||
"@nestjs/core": "^10.2.7",
|
||||
"@nestjs/platform-express": "^10.2.7",
|
||||
"@nestjs/serve-static": "^4.0.0",
|
||||
"@nestjs/swagger": "^7.1.13",
|
||||
"@nestjs/throttler": "^5.0.0",
|
||||
"@nestjs/cache-manager": "^2.2.1",
|
||||
"@nestjs/common": "^10.3.3",
|
||||
"@nestjs/core": "^10.3.3",
|
||||
"@nestjs/platform-express": "^10.3.3",
|
||||
"@nestjs/serve-static": "^4.0.1",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/throttler": "^5.1.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cache-manager": "^5.4.0",
|
||||
"cache-manager-redis-yet": "^4.1.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"connect-redis": "^7.1.0",
|
||||
"class-validator": "^0.14.1",
|
||||
"connect-redis": "^7.1.1",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"cropperjs": "^1.6.1",
|
||||
"csrf": "^3.1.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"express-session": "^1.17.3",
|
||||
"dotenv": "^16.4.4",
|
||||
"express-session": "^1.18.0",
|
||||
"express-useragent": "^1.0.15",
|
||||
"geoip-lite": "^1.4.7",
|
||||
"image-size": "^1.0.2",
|
||||
"geoip-lite": "^1.4.10",
|
||||
"image-size": "^1.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"marked": "^9.1.0",
|
||||
"marked": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"multer": "^1.4.4",
|
||||
"mysql2": "^3.6.1",
|
||||
"nodemailer": "^6.9.6",
|
||||
"mysql2": "^3.9.1",
|
||||
"nodemailer": "^6.9.9",
|
||||
"otplib": "^12.0.1",
|
||||
"pug": "^3.0.2",
|
||||
"qrcode": "^1.5.3",
|
||||
"redis": "^4.6.10",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"redis": "^4.6.13",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"rimraf": "^5.0.5",
|
||||
"rxjs": "^7.8.1",
|
||||
"thirty-two": "^1.0.2",
|
||||
"toml": "^3.0.0",
|
||||
"typeorm": "^0.3.17",
|
||||
"typeorm": "^0.3.20",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.22.20",
|
||||
"@babel/preset-typescript": "^7.23.0",
|
||||
"@nestjs/cli": "^10.1.18",
|
||||
"@nestjs/schematics": "^10.0.2",
|
||||
"@nestjs/testing": "^10.2.7",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/cookie-parser": "^1.4.4",
|
||||
"@types/cors": "^2.8.14",
|
||||
"@types/express": "^4.17.18",
|
||||
"@types/express-session": "^1.17.8",
|
||||
"@types/express-useragent": "^1.0.3",
|
||||
"@types/geoip-lite": "^1.4.2",
|
||||
"@types/jest": "29.5.5",
|
||||
"@types/jsonwebtoken": "^9.0.3",
|
||||
"@babel/preset-env": "^7.23.9",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@nestjs/cli": "^10.3.2",
|
||||
"@nestjs/schematics": "^10.1.1",
|
||||
"@nestjs/testing": "^10.3.3",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/cookie-parser": "^1.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/express-session": "^1.17.10",
|
||||
"@types/express-useragent": "^1.0.5",
|
||||
"@types/geoip-lite": "^1.4.4",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/marked": "^4.0.4",
|
||||
"@types/mime-types": "^2.1.2",
|
||||
"@types/multer": "^1.4.8",
|
||||
"@types/node": "^20.8.4",
|
||||
"@types/nodemailer": "^6.4.11",
|
||||
"@types/qrcode": "^1.5.2",
|
||||
"@types/supertest": "^2.0.14",
|
||||
"@types/uuid": "^9.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.5",
|
||||
"@typescript-eslint/parser": "^6.7.5",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/multer": "^1.4.11",
|
||||
"@types/node": "^20.11.19",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.1",
|
||||
"@typescript-eslint/parser": "^7.0.1",
|
||||
"babel-loader": "^9.1.3",
|
||||
"css-loader": "^6.8.1",
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"css-loader": "^6.10.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"jest": "^29.7.0",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"prettier": "^3.0.3",
|
||||
"sass": "^1.69.1",
|
||||
"sass-loader": "^13.3.2",
|
||||
"mini-css-extract-plugin": "^2.8.0",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.71.0",
|
||||
"sass-loader": "^14.1.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
"supertest": "^6.3.4",
|
||||
"text-loader": "^0.0.1",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.5.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.2.2",
|
||||
"webpack": "^5.88.2",
|
||||
"typescript": "^5.3.3",
|
||||
"webpack": "^5.90.2",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"jest": {
|
||||
|
@ -12,6 +12,7 @@ import { JWTModule } from './modules/jwt/jwt.module';
|
||||
import { SSRFrontEndModule } from './modules/ssr-front-end/ssr-front-end.module';
|
||||
import { UtilityModule } from './modules/utility/utility.module';
|
||||
import { WellKnownModule } from './modules/well-known/well-known.module';
|
||||
import { CommonCacheModule } from './modules/cache/cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -31,6 +32,7 @@ import { WellKnownModule } from './modules/well-known/well-known.module';
|
||||
]),
|
||||
ConfigurationModule,
|
||||
UtilityModule,
|
||||
CommonCacheModule,
|
||||
JWTModule,
|
||||
SSRFrontEndModule,
|
||||
WellKnownModule,
|
||||
|
@ -5,20 +5,23 @@ import {
|
||||
HttpException,
|
||||
} from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { IPLimitService } from 'src/modules/iplimit/iplimit.service';
|
||||
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
|
||||
import { AuditService } from 'src/modules/objects/audit/audit.service';
|
||||
import { IPLimitService } from 'src/modules/utility/services/iplimit.service';
|
||||
|
||||
@Injectable()
|
||||
export class LoginAntispamGuard implements CanActivate {
|
||||
constructor(private iplimit: IPLimitService, private audit: AuditService) {}
|
||||
constructor(
|
||||
private iplimit: IPLimitService,
|
||||
private audit: AuditService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
|
||||
if (['GET', 'OPTIONS'].includes(request.method)) return true;
|
||||
|
||||
const known = this.iplimit.getAddressLimit(request.ip);
|
||||
const known = await this.iplimit.getAddressLimit(request.ip);
|
||||
if (known && known.attempts > 3) {
|
||||
if (known.attempts > 5) {
|
||||
let reported = false;
|
||||
@ -34,7 +37,11 @@ export class LoginAntispamGuard implements CanActivate {
|
||||
}
|
||||
|
||||
const limitMinutes = known.attempts > 10 ? 30 : 10; // Half-Hour
|
||||
this.iplimit.limitUntil(request.ip, limitMinutes * 60 * 1000, reported);
|
||||
await this.iplimit.limitUntil(
|
||||
request.ip,
|
||||
limitMinutes * 60 * 1000,
|
||||
reported,
|
||||
);
|
||||
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, known.attempts * 1000),
|
||||
@ -49,7 +56,7 @@ export class LoginAntispamGuard implements CanActivate {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
this.iplimit.limitUntil(request.ip, 30 * 1000); // 30 seconds
|
||||
await this.iplimit.limitUntil(request.ip, 30 * 1000); // 30 seconds
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -8,7 +8,10 @@ import { UserService } from 'src/modules/objects/user/user.service';
|
||||
*/
|
||||
@Injectable()
|
||||
export class OAuth2Guard implements CanActivate {
|
||||
constructor(private _oauth2: OAuth2Service, private _user: UserService) {}
|
||||
constructor(
|
||||
private _oauth2: OAuth2Service,
|
||||
private _user: UserService,
|
||||
) {}
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
|
@ -38,7 +38,7 @@ import { TokenService } from 'src/modules/utility/services/token.service';
|
||||
import { PageOptions } from 'src/types/pagination.interfaces';
|
||||
import { AdminService } from './admin.service';
|
||||
import { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
||||
|
||||
const RELATIONS = ['urls', 'picture', 'owner'];
|
||||
const SET_CLIENT_FIELDS = [
|
||||
@ -510,6 +510,7 @@ export class OAuth2AdminController {
|
||||
}
|
||||
|
||||
@Throttle({ default: { limit: 3, ttl: 60000 } })
|
||||
@UseGuards(ThrottlerGuard)
|
||||
@Post('clients/:id/picture')
|
||||
@Scopes('management')
|
||||
@Privileges(['admin', 'admin:oauth2'], 'self:oauth2')
|
||||
|
21
src/modules/cache/cache.module.ts
vendored
Normal file
21
src/modules/cache/cache.module.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { redisStore } from 'cache-manager-redis-yet';
|
||||
import { RedisModule } from '../redis/redis.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CacheModule.registerAsync({
|
||||
imports: [RedisModule],
|
||||
useFactory: (redisUrl: string) => {
|
||||
return {
|
||||
store: redisStore,
|
||||
url: redisUrl,
|
||||
};
|
||||
},
|
||||
inject: ['REDIS_URL'],
|
||||
}),
|
||||
],
|
||||
exports: [CacheModule],
|
||||
})
|
||||
export class CommonCacheModule {}
|
10
src/modules/iplimit/iplimit.module.ts
Normal file
10
src/modules/iplimit/iplimit.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { IPLimitService } from './iplimit.service';
|
||||
import { CommonCacheModule } from '../cache/cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [CommonCacheModule],
|
||||
providers: [IPLimitService],
|
||||
exports: [IPLimitService],
|
||||
})
|
||||
export class IPLimitModule {}
|
47
src/modules/iplimit/iplimit.service.ts
Normal file
47
src/modules/iplimit/iplimit.service.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Cache } from 'cache-manager';
|
||||
import { TokenService } from '../utility/services/token.service';
|
||||
|
||||
export interface IPLimit {
|
||||
ip: string;
|
||||
attempts: number;
|
||||
reported: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class IPLimitService {
|
||||
constructor(
|
||||
@Inject(CACHE_MANAGER)
|
||||
private readonly cache: Cache,
|
||||
private readonly token: TokenService,
|
||||
) {}
|
||||
|
||||
public async getAddressLimit(ip: string) {
|
||||
const ipHash = this.token.insecureHash(ip);
|
||||
const entry = await this.cache.get<IPLimit>(`iplimit-${ipHash}`);
|
||||
if (!entry) return null;
|
||||
return entry;
|
||||
}
|
||||
|
||||
public async limitUntil(ip: string, expires: number, reported = false) {
|
||||
const ipHash = this.token.insecureHash(ip);
|
||||
const existing = await this.cache.get<IPLimit>(`iplimit-${ipHash}`);
|
||||
if (existing) {
|
||||
existing.attempts++;
|
||||
if (reported) existing.reported = true;
|
||||
await this.cache.set(`iplimit-${ipHash}`, existing, expires + Date.now());
|
||||
return existing;
|
||||
}
|
||||
|
||||
const newObj = {
|
||||
ip,
|
||||
attempts: 0,
|
||||
reported,
|
||||
};
|
||||
|
||||
await this.cache.set(`iplimit-${ipHash}`, newObj, expires + Date.now());
|
||||
|
||||
return newObj;
|
||||
}
|
||||
}
|
@ -3,10 +3,22 @@ import {
|
||||
OAuth2AccessToken,
|
||||
} from '@icynet/oauth2-provider';
|
||||
import { OAuth2TokenType } from 'src/modules/objects/oauth2-token/oauth2-token.entity';
|
||||
import { OAuth2Service } from '../oauth2.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service';
|
||||
import { UserService } from 'src/modules/objects/user/user.service';
|
||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
||||
import { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service';
|
||||
|
||||
@Injectable()
|
||||
export class AccessTokenAdapter implements OAuth2AccessTokenAdapter {
|
||||
constructor(private _service: OAuth2Service) {}
|
||||
constructor(
|
||||
private readonly clientService: OAuth2ClientService,
|
||||
private readonly userService: UserService,
|
||||
private readonly token: TokenService,
|
||||
private readonly form: FormUtilityService,
|
||||
private readonly tokenService: OAuth2TokenService,
|
||||
) {}
|
||||
|
||||
public ttl = 604800;
|
||||
|
||||
@ -20,18 +32,18 @@ export class AccessTokenAdapter implements OAuth2AccessTokenAdapter {
|
||||
scope: string | string[],
|
||||
ttl: number,
|
||||
): Promise<string> {
|
||||
const client = await this._service.clientService.getById(clientId);
|
||||
const user = await this._service.userService.getById(userId);
|
||||
const accessToken = this._service.token.generateString(64);
|
||||
const client = await this.clientService.getById(clientId);
|
||||
const user = await this.userService.getById(userId);
|
||||
const accessToken = this.token.generateString(64);
|
||||
|
||||
// Standardize scope value
|
||||
const scopes = (
|
||||
!Array.isArray(scope) ? this._service.splitScope(scope) : scope
|
||||
!Array.isArray(scope) ? this.form.splitScope(scope) : scope
|
||||
).join(' ');
|
||||
|
||||
const expiresAt = new Date(Date.now() + ttl * 1000);
|
||||
|
||||
this._service.tokenService.insertToken(
|
||||
this.tokenService.insertToken(
|
||||
accessToken,
|
||||
OAuth2TokenType.ACCESS_TOKEN,
|
||||
client,
|
||||
@ -47,7 +59,7 @@ export class AccessTokenAdapter implements OAuth2AccessTokenAdapter {
|
||||
token: string | OAuth2AccessToken,
|
||||
): Promise<OAuth2AccessToken> {
|
||||
const findBy = typeof token === 'string' ? token : token.token;
|
||||
const find = await this._service.tokenService.fetchByToken(
|
||||
const find = await this.tokenService.fetchByToken(
|
||||
findBy,
|
||||
OAuth2TokenType.ACCESS_TOKEN,
|
||||
);
|
||||
@ -75,7 +87,7 @@ export class AccessTokenAdapter implements OAuth2AccessTokenAdapter {
|
||||
userId: number,
|
||||
clientId: string,
|
||||
): Promise<OAuth2AccessToken> {
|
||||
const find = await this._service.tokenService.fetchByUserIdClientId(
|
||||
const find = await this.tokenService.fetchByUserIdClientId(
|
||||
userId,
|
||||
clientId,
|
||||
OAuth2TokenType.ACCESS_TOKEN,
|
||||
|
@ -1,16 +1,22 @@
|
||||
import { OAuth2ClientAdapter, OAuth2Client } from '@icynet/oauth2-provider';
|
||||
import { OAuth2ClientURLType } from 'src/modules/objects/oauth2-client/oauth2-client-url.entity';
|
||||
import { OAuth2Service } from '../oauth2.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
||||
import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service';
|
||||
|
||||
@Injectable()
|
||||
export class ClientAdapter implements OAuth2ClientAdapter {
|
||||
constructor(private _service: OAuth2Service) {}
|
||||
constructor(
|
||||
private readonly form: FormUtilityService,
|
||||
private readonly clientService: OAuth2ClientService,
|
||||
) {}
|
||||
|
||||
getId(client: OAuth2Client): string {
|
||||
return client.id as string;
|
||||
}
|
||||
|
||||
async fetchById(id: string): Promise<OAuth2Client> {
|
||||
const find = await this._service.clientService.getById(id);
|
||||
const find = await this.clientService.getById(id);
|
||||
|
||||
if (!find) {
|
||||
return null;
|
||||
@ -18,7 +24,7 @@ export class ClientAdapter implements OAuth2ClientAdapter {
|
||||
|
||||
return {
|
||||
id: find.client_id,
|
||||
scope: this._service.splitScope(find.scope),
|
||||
scope: this.form.splitScope(find.scope),
|
||||
grants: find.grants.split(' '),
|
||||
secret: find.client_secret,
|
||||
};
|
||||
@ -33,7 +39,7 @@ export class ClientAdapter implements OAuth2ClientAdapter {
|
||||
}
|
||||
|
||||
async hasRedirectUri(client: OAuth2Client): Promise<boolean> {
|
||||
const redirectUris = await this._service.clientService.getClientURLs(
|
||||
const redirectUris = await this.clientService.getClientURLs(
|
||||
client.id as string,
|
||||
OAuth2ClientURLType.REDIRECT_URI,
|
||||
);
|
||||
@ -45,14 +51,14 @@ export class ClientAdapter implements OAuth2ClientAdapter {
|
||||
client: OAuth2Client,
|
||||
redirectUri: string,
|
||||
): Promise<boolean> {
|
||||
return this._service.clientService.checkRedirectURI(
|
||||
return this.clientService.checkRedirectURI(
|
||||
client.id as string,
|
||||
redirectUri,
|
||||
);
|
||||
}
|
||||
|
||||
transformScope(scope: string | string[]): string[] {
|
||||
return Array.isArray(scope) ? scope : this._service.splitScope(scope);
|
||||
return Array.isArray(scope) ? scope : this.form.splitScope(scope);
|
||||
}
|
||||
|
||||
checkScope(client: OAuth2Client, scope: string[]): boolean {
|
||||
|
@ -1,9 +1,21 @@
|
||||
import { OAuth2CodeAdapter, OAuth2Code } from '@icynet/oauth2-provider';
|
||||
import { OAuth2TokenType } from 'src/modules/objects/oauth2-token/oauth2-token.entity';
|
||||
import { OAuth2Service } from '../oauth2.service';
|
||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||
import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service';
|
||||
import { UserService } from 'src/modules/objects/user/user.service';
|
||||
import { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service';
|
||||
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class CodeAdapter implements OAuth2CodeAdapter {
|
||||
constructor(private _service: OAuth2Service) {}
|
||||
constructor(
|
||||
private readonly token: TokenService,
|
||||
private readonly tokenService: OAuth2TokenService,
|
||||
private readonly clientService: OAuth2ClientService,
|
||||
private readonly userService: UserService,
|
||||
private readonly form: FormUtilityService,
|
||||
) {}
|
||||
|
||||
ttl = 3600;
|
||||
challengeMethods = ['plain', 'S256'];
|
||||
@ -17,13 +29,13 @@ export class CodeAdapter implements OAuth2CodeAdapter {
|
||||
codeChallenge?: string,
|
||||
codeChallengeMethod?: 'plain' | 'S256',
|
||||
): Promise<string> {
|
||||
const client = await this._service.clientService.getById(clientId);
|
||||
const user = await this._service.userService.getById(userId);
|
||||
const accessToken = this._service.token.generateString(64);
|
||||
const client = await this.clientService.getById(clientId);
|
||||
const user = await this.userService.getById(userId);
|
||||
const accessToken = this.token.generateString(64);
|
||||
|
||||
// Standardize scope value
|
||||
const scopes = (
|
||||
!Array.isArray(scope) ? this._service.splitScope(scope) : scope
|
||||
!Array.isArray(scope) ? this.form.splitScope(scope) : scope
|
||||
).join(' ');
|
||||
|
||||
const expiresAt = new Date(Date.now() + ttl * 1000);
|
||||
@ -34,7 +46,7 @@ export class CodeAdapter implements OAuth2CodeAdapter {
|
||||
)}:${codeChallenge}`
|
||||
: null;
|
||||
|
||||
this._service.tokenService.insertToken(
|
||||
this.tokenService.insertToken(
|
||||
accessToken,
|
||||
OAuth2TokenType.CODE,
|
||||
client,
|
||||
@ -50,7 +62,7 @@ export class CodeAdapter implements OAuth2CodeAdapter {
|
||||
|
||||
async fetchByCode(code: string | OAuth2Code): Promise<OAuth2Code> {
|
||||
const findBy = typeof code === 'string' ? code : code.code;
|
||||
const find = await this._service.tokenService.fetchByToken(
|
||||
const find = await this.tokenService.fetchByToken(
|
||||
findBy,
|
||||
OAuth2TokenType.CODE,
|
||||
);
|
||||
@ -80,11 +92,11 @@ export class CodeAdapter implements OAuth2CodeAdapter {
|
||||
|
||||
async removeByCode(code: string | OAuth2Code): Promise<boolean> {
|
||||
const findBy = typeof code === 'string' ? code : code.code;
|
||||
const find = await this._service.tokenService.fetchByToken(
|
||||
const find = await this.tokenService.fetchByToken(
|
||||
findBy,
|
||||
OAuth2TokenType.CODE,
|
||||
);
|
||||
this._service.tokenService.remove(find);
|
||||
this.tokenService.remove(find);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,20 @@
|
||||
import { JWTAdapter, OAuth2User, OAuth2Client } from '@icynet/oauth2-provider';
|
||||
import { OAuth2Service } from '../oauth2.service';
|
||||
import {
|
||||
JWTAdapter as OAuth2JWTAdapter,
|
||||
OAuth2User,
|
||||
OAuth2Client,
|
||||
} from '@icynet/oauth2-provider';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigurationService } from 'src/modules/config/config.service';
|
||||
import { JWTService } from 'src/modules/jwt/jwt.service';
|
||||
import { UserService } from 'src/modules/objects/user/user.service';
|
||||
|
||||
export class IcyJWTAdapter implements JWTAdapter {
|
||||
constructor(private _client: OAuth2Service) {}
|
||||
@Injectable()
|
||||
export class JWTAdapter implements OAuth2JWTAdapter {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly config: ConfigurationService,
|
||||
private readonly jwtService: JWTService,
|
||||
) {}
|
||||
|
||||
async issueIdToken(
|
||||
rawUser: OAuth2User,
|
||||
@ -10,7 +22,7 @@ export class IcyJWTAdapter implements JWTAdapter {
|
||||
scope: string[],
|
||||
nonce?: string,
|
||||
): Promise<string> {
|
||||
const user = await this._client.userService.getById(rawUser.id as number);
|
||||
const user = await this.userService.getById(rawUser.id as number);
|
||||
|
||||
const userData: Record<string, unknown> = {
|
||||
name: user.display_name,
|
||||
@ -26,12 +38,12 @@ export class IcyJWTAdapter implements JWTAdapter {
|
||||
}
|
||||
|
||||
if (scope.includes('picture') && user.picture) {
|
||||
userData.picture = `${this._client.config.get('app.base_url')}/uploads/${
|
||||
userData.picture = `${this.config.get('app.base_url')}/uploads/${
|
||||
user.picture.file
|
||||
}`;
|
||||
}
|
||||
|
||||
return this._client.jwt.issue(
|
||||
return this.jwtService.issue(
|
||||
userData,
|
||||
user.uuid as string,
|
||||
rawClient.id as string,
|
||||
|
@ -3,10 +3,22 @@ import {
|
||||
OAuth2RefreshToken,
|
||||
} from '@icynet/oauth2-provider';
|
||||
import { OAuth2TokenType } from 'src/modules/objects/oauth2-token/oauth2-token.entity';
|
||||
import { OAuth2Service } from '../oauth2.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service';
|
||||
import { UserService } from 'src/modules/objects/user/user.service';
|
||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||
import { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service';
|
||||
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
||||
|
||||
@Injectable()
|
||||
export class RefreshTokenAdapter implements OAuth2RefreshTokenAdapter {
|
||||
constructor(private _service: OAuth2Service) {}
|
||||
constructor(
|
||||
private readonly clientService: OAuth2ClientService,
|
||||
private readonly userService: UserService,
|
||||
private readonly token: TokenService,
|
||||
private readonly tokenService: OAuth2TokenService,
|
||||
private readonly form: FormUtilityService,
|
||||
) {}
|
||||
|
||||
invalidateOld = false;
|
||||
|
||||
@ -15,18 +27,18 @@ export class RefreshTokenAdapter implements OAuth2RefreshTokenAdapter {
|
||||
clientId: string,
|
||||
scope: string | string[],
|
||||
): Promise<string> {
|
||||
const client = await this._service.clientService.getById(clientId);
|
||||
const user = await this._service.userService.getById(userId);
|
||||
const accessToken = this._service.token.generateString(64);
|
||||
const client = await this.clientService.getById(clientId);
|
||||
const user = await this.userService.getById(userId);
|
||||
const accessToken = this.token.generateString(64);
|
||||
|
||||
// Standardize scope value
|
||||
const scopes = (
|
||||
!Array.isArray(scope) ? this._service.splitScope(scope) : scope
|
||||
!Array.isArray(scope) ? this.form.splitScope(scope) : scope
|
||||
).join(' ');
|
||||
|
||||
const expiresAt = new Date(Date.now() + 3.154e7 * 1000);
|
||||
|
||||
this._service.tokenService.insertToken(
|
||||
this.tokenService.insertToken(
|
||||
accessToken,
|
||||
OAuth2TokenType.REFRESH_TOKEN,
|
||||
client,
|
||||
@ -42,7 +54,7 @@ export class RefreshTokenAdapter implements OAuth2RefreshTokenAdapter {
|
||||
token: string | OAuth2RefreshToken,
|
||||
): Promise<OAuth2RefreshToken> {
|
||||
const findBy = typeof token === 'string' ? token : token.token;
|
||||
const find = await this._service.tokenService.fetchByToken(
|
||||
const find = await this.tokenService.fetchByToken(
|
||||
findBy,
|
||||
OAuth2TokenType.REFRESH_TOKEN,
|
||||
);
|
||||
@ -62,24 +74,24 @@ export class RefreshTokenAdapter implements OAuth2RefreshTokenAdapter {
|
||||
userId: number,
|
||||
clientId: string,
|
||||
): Promise<boolean> {
|
||||
const find = await this._service.tokenService.fetchByUserIdClientId(
|
||||
const find = await this.tokenService.fetchByUserIdClientId(
|
||||
userId,
|
||||
clientId,
|
||||
OAuth2TokenType.REFRESH_TOKEN,
|
||||
);
|
||||
|
||||
await this._service.tokenService.remove(find);
|
||||
await this.tokenService.remove(find);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async removeByRefreshToken(token: string): Promise<boolean> {
|
||||
const find = await this._service.tokenService.fetchByToken(
|
||||
const find = await this.tokenService.fetchByToken(
|
||||
token,
|
||||
OAuth2TokenType.REFRESH_TOKEN,
|
||||
);
|
||||
|
||||
await this._service.tokenService.remove(find);
|
||||
await this.tokenService.remove(find);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -1,18 +1,26 @@
|
||||
import { OAuth2UserAdapter, OAuth2User } from '@icynet/oauth2-provider';
|
||||
import { OAuth2Service } from '../oauth2.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { ParamsDictionary } from 'express-serve-static-core';
|
||||
import { ParsedQs } from 'qs';
|
||||
import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service';
|
||||
import { UserService } from 'src/modules/objects/user/user.service';
|
||||
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserAdapter implements OAuth2UserAdapter {
|
||||
constructor(private _service: OAuth2Service) {}
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly clientService: OAuth2ClientService,
|
||||
private readonly form: FormUtilityService,
|
||||
) {}
|
||||
|
||||
getId(user: OAuth2User): number {
|
||||
return user.id as number;
|
||||
}
|
||||
|
||||
async fetchById(id: number): Promise<OAuth2User> {
|
||||
const find = await this._service.userService.getById(id);
|
||||
const find = await this.userService.getById(id);
|
||||
|
||||
if (!find) {
|
||||
return null;
|
||||
@ -26,7 +34,7 @@ export class UserAdapter implements OAuth2UserAdapter {
|
||||
}
|
||||
|
||||
async fetchByUsername(username: string): Promise<OAuth2User> {
|
||||
const find = await this._service.userService.getByUsername(username);
|
||||
const find = await this.userService.getByUsername(username);
|
||||
|
||||
if (!find) {
|
||||
return null;
|
||||
@ -40,7 +48,7 @@ export class UserAdapter implements OAuth2UserAdapter {
|
||||
}
|
||||
|
||||
checkPassword(user: OAuth2User, password: string): Promise<boolean> {
|
||||
return this._service.userService.comparePasswords(user.password, password);
|
||||
return this.userService.comparePasswords(user.password, password);
|
||||
}
|
||||
|
||||
async fetchFromRequest(
|
||||
@ -54,10 +62,10 @@ export class UserAdapter implements OAuth2UserAdapter {
|
||||
clientId: string,
|
||||
scope: string | string[],
|
||||
): Promise<boolean> {
|
||||
return this._service.clientService.hasAuthorized(
|
||||
return this.clientService.hasAuthorized(
|
||||
userId,
|
||||
clientId,
|
||||
this._service.splitScope(scope),
|
||||
this.form.splitScope(scope),
|
||||
);
|
||||
}
|
||||
|
||||
@ -66,12 +74,12 @@ export class UserAdapter implements OAuth2UserAdapter {
|
||||
clientId: string,
|
||||
scope: string | string[],
|
||||
): Promise<boolean> {
|
||||
const client = await this._service.clientService.getById(clientId);
|
||||
const user = await this._service.userService.getById(userId);
|
||||
await this._service.clientService.createAuthorization(
|
||||
const client = await this.clientService.getById(clientId);
|
||||
const user = await this.userService.getById(userId);
|
||||
await this.clientService.createAuthorization(
|
||||
user,
|
||||
client,
|
||||
this._service.splitScope(scope),
|
||||
this.form.splitScope(scope),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
@ -5,6 +5,12 @@ import { UploadModule } from 'src/modules/objects/upload/upload.module';
|
||||
import { UserModule } from 'src/modules/objects/user/user.module';
|
||||
import { JWTModule } from '../jwt/jwt.module';
|
||||
import { OAuth2Service } from './oauth2.service';
|
||||
import { AccessTokenAdapter } from './adapter/access-token.adapter';
|
||||
import { ClientAdapter } from './adapter/client.adapter';
|
||||
import { CodeAdapter } from './adapter/code.adapter';
|
||||
import { JWTAdapter } from './adapter/jwt.adapter';
|
||||
import { RefreshTokenAdapter } from './adapter/refresh-token.adapter';
|
||||
import { UserAdapter } from './adapter/user.adapter';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -14,7 +20,15 @@ import { OAuth2Service } from './oauth2.service';
|
||||
OAuth2TokenModule,
|
||||
JWTModule,
|
||||
],
|
||||
providers: [OAuth2Service],
|
||||
providers: [
|
||||
AccessTokenAdapter,
|
||||
ClientAdapter,
|
||||
CodeAdapter,
|
||||
JWTAdapter,
|
||||
RefreshTokenAdapter,
|
||||
UserAdapter,
|
||||
OAuth2Service,
|
||||
],
|
||||
exports: [OAuth2Service, JWTModule],
|
||||
})
|
||||
export class OAuth2Module {}
|
||||
|
@ -1,15 +1,10 @@
|
||||
import { OAuth2AdapterModel, OAuth2Provider } from '@icynet/oauth2-provider';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigurationService } from 'src/modules/config/config.service';
|
||||
import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service';
|
||||
import { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service';
|
||||
import { UserService } from 'src/modules/objects/user/user.service';
|
||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||
import { JWTService } from '../jwt/jwt.service';
|
||||
import { AccessTokenAdapter } from './adapter/access-token.adapter';
|
||||
import { ClientAdapter } from './adapter/client.adapter';
|
||||
import { CodeAdapter } from './adapter/code.adapter';
|
||||
import { IcyJWTAdapter } from './adapter/jwt.adapter';
|
||||
import { JWTAdapter } from './adapter/jwt.adapter';
|
||||
import { RefreshTokenAdapter } from './adapter/refresh-token.adapter';
|
||||
import { UserAdapter } from './adapter/user.adapter';
|
||||
|
||||
@ -22,19 +17,8 @@ const ALWAYS_AVAILABLE = ['Username and display name'];
|
||||
const ALWAYS_UNAVAILABLE = ['Password and other account settings'];
|
||||
|
||||
@Injectable()
|
||||
export class OAuth2Service {
|
||||
private _oauthAdapter: OAuth2AdapterModel = {
|
||||
accessToken: new AccessTokenAdapter(this),
|
||||
refreshToken: new RefreshTokenAdapter(this),
|
||||
user: new UserAdapter(this),
|
||||
client: new ClientAdapter(this),
|
||||
code: new CodeAdapter(this),
|
||||
jwt: new IcyJWTAdapter(this),
|
||||
};
|
||||
|
||||
public oauth = new OAuth2Provider(
|
||||
this._oauthAdapter,
|
||||
async (req, res, client, scope) => {
|
||||
export class OAuth2Service implements OAuth2AdapterModel {
|
||||
public oauth = new OAuth2Provider(this, async (req, res, client, scope) => {
|
||||
const fullClient = await this.clientService.getById(client.id as string);
|
||||
let allowedScopes = [...ALWAYS_AVAILABLE];
|
||||
let disallowedScopes = [...ALWAYS_UNAVAILABLE];
|
||||
@ -62,33 +46,19 @@ export class OAuth2Service {
|
||||
allowedScopes,
|
||||
disallowedScopes,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
constructor(
|
||||
public token: TokenService,
|
||||
public jwt: JWTService,
|
||||
public config: ConfigurationService,
|
||||
public userService: UserService,
|
||||
public clientService: OAuth2ClientService,
|
||||
public tokenService: OAuth2TokenService,
|
||||
public accessToken: AccessTokenAdapter,
|
||||
public refreshToken: RefreshTokenAdapter,
|
||||
public user: UserAdapter,
|
||||
public client: ClientAdapter,
|
||||
public code: CodeAdapter,
|
||||
public jwt: JWTAdapter,
|
||||
) {
|
||||
if (!!process.env.DEBUG_OAUTH2) {
|
||||
this.oauth.logger.setLogLevel('debug');
|
||||
}
|
||||
}
|
||||
|
||||
public splitScope(scope: string | string[]): string[] {
|
||||
if (!scope) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Array.isArray(scope)) {
|
||||
return scope;
|
||||
}
|
||||
|
||||
return scope.includes(',')
|
||||
? scope.split(',').map((item) => item.trim())
|
||||
: scope.split(' ');
|
||||
}
|
||||
}
|
||||
|
@ -6,19 +6,22 @@ export type Redis = ReturnType<typeof redis.createClient>;
|
||||
|
||||
export const redisProviders = [
|
||||
{
|
||||
provide: 'REDIS_CLIENT',
|
||||
useFactory: async (config: ConfigurationService): Promise<Redis> => {
|
||||
const redisClient = redis.createClient({
|
||||
url:
|
||||
provide: 'REDIS_URL',
|
||||
useFactory: (config: ConfigurationService) =>
|
||||
process.env.REDIS_URL ||
|
||||
config.get<string>('app.redis_url') ||
|
||||
'redis://localhost:6379',
|
||||
inject: [ConfigurationService],
|
||||
},
|
||||
{
|
||||
provide: 'REDIS_CLIENT',
|
||||
useFactory: async (url: string): Promise<Redis> => {
|
||||
const redisClient = redis.createClient({
|
||||
url,
|
||||
});
|
||||
|
||||
await redisClient.connect();
|
||||
|
||||
return redisClient;
|
||||
return redisClient.connect();
|
||||
},
|
||||
inject: [ConfigurationService],
|
||||
inject: ['REDIS_URL'],
|
||||
} as FactoryProvider<Redis>,
|
||||
];
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
Session,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { Request, Response } from 'express';
|
||||
import { SessionData } from 'express-session';
|
||||
import { LoginAntispamGuard } from 'src/guards/login-antispam.guard';
|
||||
@ -301,6 +301,7 @@ export class LoginController {
|
||||
|
||||
@Post('password')
|
||||
@Throttle({ default: { limit: 3, ttl: 60000 } })
|
||||
@UseGuards(ThrottlerGuard)
|
||||
public async setNewPassword(
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
|
@ -9,9 +9,16 @@ import { UserTokenModule } from 'src/modules/objects/user-token/user-token.modul
|
||||
import { UserModule } from 'src/modules/objects/user/user.module';
|
||||
import { SessionModule } from '../session/session.module';
|
||||
import { LoginController } from './login.controller';
|
||||
import { IPLimitModule } from 'src/modules/iplimit/iplimit.module';
|
||||
|
||||
@Module({
|
||||
imports: [UserModule, UserTokenModule, AuditModule, SessionModule],
|
||||
imports: [
|
||||
UserModule,
|
||||
UserTokenModule,
|
||||
AuditModule,
|
||||
SessionModule,
|
||||
IPLimitModule,
|
||||
],
|
||||
controllers: [LoginController],
|
||||
})
|
||||
export class LoginModule implements NestModule {
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
UnauthorizedException,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { Request, Response } from 'express';
|
||||
import { LoginAntispamGuard } from 'src/guards/login-antispam.guard';
|
||||
import { ConfigurationService } from 'src/modules/config/config.service';
|
||||
@ -40,6 +40,7 @@ export class RegisterController {
|
||||
|
||||
@Post()
|
||||
@Throttle({ default: { limit: 3, ttl: 10000 } })
|
||||
@UseGuards(ThrottlerGuard)
|
||||
public async registerRequest(
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
|
@ -8,9 +8,10 @@ import { AuditModule } from 'src/modules/objects/audit/audit.module';
|
||||
import { UserModule } from 'src/modules/objects/user/user.module';
|
||||
import { SessionModule } from '../session/session.module';
|
||||
import { RegisterController } from './register.controller';
|
||||
import { IPLimitModule } from 'src/modules/iplimit/iplimit.module';
|
||||
|
||||
@Module({
|
||||
imports: [UserModule, AuditModule, SessionModule],
|
||||
imports: [UserModule, AuditModule, SessionModule, IPLimitModule],
|
||||
controllers: [RegisterController],
|
||||
})
|
||||
export class RegisterModule implements NestModule {
|
||||
|
@ -12,10 +12,11 @@ import {
|
||||
Res,
|
||||
UnauthorizedException,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { Request, Response } from 'express';
|
||||
import { unlink } from 'fs/promises';
|
||||
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
|
||||
@ -90,6 +91,7 @@ export class SettingsController {
|
||||
}
|
||||
|
||||
@Throttle({ default: { limit: 3, ttl: 60000 } })
|
||||
@UseGuards(ThrottlerGuard)
|
||||
@Post('avatar')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadAvatarFile(
|
||||
|
@ -110,4 +110,18 @@ export class FormUtilityService {
|
||||
...additional,
|
||||
};
|
||||
}
|
||||
|
||||
public splitScope(scope: string | string[]): string[] {
|
||||
if (!scope) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Array.isArray(scope)) {
|
||||
return scope;
|
||||
}
|
||||
|
||||
return scope.includes(',')
|
||||
? scope.split(',').map((item) => item.trim())
|
||||
: scope.split(' ');
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
export interface IPLimit {
|
||||
ip: string;
|
||||
attempts: number;
|
||||
expires: number;
|
||||
reported: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class IPLimitService {
|
||||
public limitedAddresses: IPLimit[] = [];
|
||||
|
||||
public getAddressLimit(ip: string) {
|
||||
this.flush();
|
||||
const entry = this.limitedAddresses.find((item) => item.ip === ip);
|
||||
if (!entry) return null;
|
||||
return entry;
|
||||
}
|
||||
|
||||
public limitUntil(ip: string, expires: number, reported = false) {
|
||||
const existing = this.limitedAddresses.find((item) => item.ip === ip);
|
||||
if (existing) {
|
||||
existing.attempts++;
|
||||
existing.expires = expires + Date.now();
|
||||
if (reported) existing.reported = true;
|
||||
return existing;
|
||||
}
|
||||
|
||||
const newObj = {
|
||||
ip,
|
||||
expires: expires + Date.now(),
|
||||
attempts: 0,
|
||||
reported,
|
||||
};
|
||||
|
||||
this.limitedAddresses.push(newObj);
|
||||
|
||||
return newObj;
|
||||
}
|
||||
|
||||
public flush() {
|
||||
this.limitedAddresses = this.limitedAddresses.filter(
|
||||
(entry) => entry.expires > Date.now(),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { FormUtilityService } from './services/form-utility.service';
|
||||
import { IPLimitService } from './services/iplimit.service';
|
||||
import { PaginationService } from './services/paginate.service';
|
||||
import { QRCodeService } from './services/qr-code.service';
|
||||
import { TokenService } from './services/token.service';
|
||||
@ -12,14 +11,7 @@ import { TokenService } from './services/token.service';
|
||||
FormUtilityService,
|
||||
QRCodeService,
|
||||
PaginationService,
|
||||
IPLimitService,
|
||||
],
|
||||
exports: [
|
||||
TokenService,
|
||||
FormUtilityService,
|
||||
QRCodeService,
|
||||
PaginationService,
|
||||
IPLimitService,
|
||||
],
|
||||
exports: [TokenService, FormUtilityService, QRCodeService, PaginationService],
|
||||
})
|
||||
export class UtilityModule {}
|
||||
|
Reference in New Issue
Block a user