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