This commit is contained in:
Evert Prants 2024-03-12 17:49:06 +02:00
parent 001dc0b63a
commit fb4154c9e5
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
27 changed files with 2844 additions and 2768 deletions

View File

@ -8,7 +8,7 @@
"html"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"[sql]": {
"editor.defaultFormatter": "adpyke.vscode-sql-formatter"

4990
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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": {

View File

@ -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,

View File

@ -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;
}

View File

@ -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,

View File

@ -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
View 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 {}

View 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 {}

View 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;
}
}

View File

@ -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,

View File

@ -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 {

View File

@ -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;
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 {}

View File

@ -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,73 +17,48 @@ 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),
};
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];
public oauth = new OAuth2Provider(
this._oauthAdapter,
async (req, res, client, scope) => {
const fullClient = await this.clientService.getById(client.id as string);
let allowedScopes = [...ALWAYS_AVAILABLE];
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;
Object.keys(SCOPE_DESCRIPTION).forEach((item) => {
if (scope.includes(item)) {
allowedScopes.push(SCOPE_DESCRIPTION[item]);
} else {
disallowedScopes.push(SCOPE_DESCRIPTION[item]);
}
});
res.render('authorize', {
csrf: req.csrfToken(),
user: req.user,
client: fullClient,
allowedScopes,
disallowedScopes,
});
},
);
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', {
csrf: req.csrfToken(),
user: req.user,
client: fullClient,
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(' ');
}
}

View File

@ -5,20 +5,23 @@ import * as redis from 'redis';
export type Redis = ReturnType<typeof redis.createClient>;
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',
useFactory: async (config: ConfigurationService): Promise<Redis> => {
useFactory: async (url: string): Promise<Redis> => {
const redisClient = redis.createClient({
url:
process.env.REDIS_URL ||
config.get<string>('app.redis_url') ||
'redis://localhost:6379',
url,
});
await redisClient.connect();
return redisClient;
return redisClient.connect();
},
inject: [ConfigurationService],
inject: ['REDIS_URL'],
} as FactoryProvider<Redis>,
];

View File

@ -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,

View File

@ -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 {

View File

@ -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,

View File

@ -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 {

View File

@ -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(

View File

@ -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(' ');
}
}

View File

@ -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(),
);
}
}

View File

@ -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 {}