Slave/altcha throttle #4
+3
-6
@@ -21,19 +21,16 @@ CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||
MAX_IMAGES_PER_UPLOAD=10
|
||||
MULTER_MAX_FILE_BYTES=10485760
|
||||
|
||||
CAPTCHA_ENABLED=true
|
||||
|
||||
ALTCHA_ENABLED=true
|
||||
ALTCHA_HMAC_KEY=change-me-use-long-random-secret
|
||||
ALTCHA_MAX_NUMBER=50000
|
||||
|
||||
NEVER_MODE_INACTIVITY_DAYS=365
|
||||
|
||||
THROTTLE_TTL_MS=60000
|
||||
THROTTLE_LIMIT=1000
|
||||
THROTTLE_UPLOAD_TTL_MS=60000
|
||||
THROTTLE_UPLOAD_LIMIT=20
|
||||
THROTTLE_HOTLINK_PER_IP_TTL_MS=60000
|
||||
THROTTLE_HOTLINK_PER_IP_LIMIT=1000
|
||||
THROTTLE_HOTLINK_PER_ID_TTL_MS=60000
|
||||
THROTTLE_HOTLINK_PER_ID_LIMIT=120
|
||||
|
||||
VITE_API_BASE_URL=http://localhost:3000/api
|
||||
VITE_CLEARNET_DOMAIN=localhost:5173
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
dist
|
||||
*.tsbuildinfo
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile*
|
||||
coverage
|
||||
*.local
|
||||
uploads
|
||||
Generated
+85
@@ -13,7 +13,9 @@
|
||||
"@nestjs/config": "^4.0.3",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/throttler": "^6.7.1",
|
||||
"@nestjs/typeorm": "^12.0.2",
|
||||
"altcha-lib": "^2.6.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.15.1",
|
||||
"pg": "^8.23.1",
|
||||
@@ -1994,6 +1996,20 @@
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/throttler": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.7.1.tgz",
|
||||
"integrity": "sha512-gAyY1AkU+cSK5DCjFOB43xax9oakameH4xZa9kAgqBqSBQZgmv3DZylLV8KyZNzOGhceL3kDVtby+CF8VAdmEA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0",
|
||||
"@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0",
|
||||
"reflect-metadata": "^0.1.13 || ^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/typeorm": {
|
||||
"version": "12.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-12.0.2.tgz",
|
||||
@@ -3269,6 +3285,75 @@
|
||||
"ajv": "^8.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/altcha-lib": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/altcha-lib/-/altcha-lib-2.6.0.tgz",
|
||||
"integrity": "sha512-mBp/KxMZmQkckWX5iM0gHqCm0qTLGhF9r8hhFjNrBpzP0qHZBadR9oafxVx9cTLHl4h2+f56N4hRIrZzvonYLQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"altcha-lib": "bin/cli.mjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fastify/cookie": "^11.0.0",
|
||||
"@fastify/cors": "^11.0.0",
|
||||
"@fastify/formbody": "^8.0.0 || ^9.0.0",
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0 || ^12.0.0",
|
||||
"@nestjs/core": "^10.0.0 || ^11.0.0 || ^12.0.0",
|
||||
"@nestjs/platform-express": "^10.0.0 || ^11.0.0 || ^12.0.0",
|
||||
"@nestjs/platform-fastify": "^10.0.0 || ^11.0.0 || ^12.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"cookie-parser": "^1.0.0",
|
||||
"cors": "^2.0.0",
|
||||
"express": "^4.0.0 || ^5.0.0",
|
||||
"fastify": "^5.0.0",
|
||||
"h3": "^2.0.0",
|
||||
"hono": "^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@fastify/cookie": {
|
||||
"optional": true
|
||||
},
|
||||
"@fastify/cors": {
|
||||
"optional": true
|
||||
},
|
||||
"@fastify/formbody": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/common": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/platform-express": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/platform-fastify": {
|
||||
"optional": true
|
||||
},
|
||||
"@sveltejs/kit": {
|
||||
"optional": true
|
||||
},
|
||||
"cookie-parser": {
|
||||
"optional": true
|
||||
},
|
||||
"cors": {
|
||||
"optional": true
|
||||
},
|
||||
"express": {
|
||||
"optional": true
|
||||
},
|
||||
"fastify": {
|
||||
"optional": true
|
||||
},
|
||||
"h3": {
|
||||
"optional": true
|
||||
},
|
||||
"hono": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-colors": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
"@nestjs/config": "^4.0.3",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/throttler": "^6.7.1",
|
||||
"@nestjs/typeorm": "^12.0.2",
|
||||
"altcha-lib": "^2.6.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.15.1",
|
||||
"pg": "^8.23.1",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService, registerAs } from '@nestjs/config';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { getAppConfig, getPostgresConfig } from './config';
|
||||
import { getAltchaConfig, getAppConfig, getPostgresConfig } from './config';
|
||||
import { validate } from './config/validate';
|
||||
import { AltchaModule } from './modules/altcha/AltchaModule';
|
||||
import { HealthCheckModule } from './modules/healthCheck/HealthCheckModule';
|
||||
import { StorageModule } from './modules/storage/StorageModule';
|
||||
import { UploadModule } from './modules/upload/UploadModule';
|
||||
@@ -11,15 +15,43 @@ import { Config } from './types/Config';
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [registerAs('postgres', getPostgresConfig), registerAs('app', getAppConfig)]
|
||||
validate,
|
||||
load: [
|
||||
registerAs('postgres', getPostgresConfig),
|
||||
registerAs('app', getAppConfig),
|
||||
registerAs('altcha', getAltchaConfig)
|
||||
]
|
||||
}),
|
||||
ThrottlerModule.forRootAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const { throttle } = configService.get('app') as Config['app'];
|
||||
|
||||
return {
|
||||
throttlers: [
|
||||
{
|
||||
name: 'default',
|
||||
ttl: throttle.ttlMs,
|
||||
limit: throttle.limit
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}),
|
||||
TypeOrmModule.forRootAsync({
|
||||
useFactory: (configService: ConfigService) => configService.get('postgres') as Config['postgres'],
|
||||
inject: [ConfigService]
|
||||
}),
|
||||
HealthCheckModule,
|
||||
AltchaModule,
|
||||
StorageModule,
|
||||
UploadModule
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: ThrottlerGuard
|
||||
}
|
||||
]
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AppConfig } from '../types/AppConfig';
|
||||
import { PostgresConfig } from '../types/Config';
|
||||
import { AltchaConfig } from '../types/AltchaConfig';
|
||||
import { PostgresConfig } from '../types/PostgresConfig';
|
||||
import { NodeEnv } from '../types/NodeEnv';
|
||||
|
||||
const env = (key: string): string => process.env[key] || '';
|
||||
@@ -23,6 +24,14 @@ export const getPostgresConfig = (): PostgresConfig => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getAltchaConfig = (): AltchaConfig => {
|
||||
return {
|
||||
enabled: isEnabled('ALTCHA_ENABLED'),
|
||||
hmacKey: env('ALTCHA_HMAC_KEY'),
|
||||
maxNumber: envInt('ALTCHA_MAX_NUMBER')
|
||||
};
|
||||
};
|
||||
|
||||
export const getAppConfig = (): AppConfig => {
|
||||
const rawOrigins = env('CORS_ORIGINS');
|
||||
|
||||
@@ -32,6 +41,14 @@ export const getAppConfig = (): AppConfig => {
|
||||
corsOrigins: rawOrigins
|
||||
.split(',')
|
||||
.map(o => o.trim())
|
||||
.filter(Boolean)
|
||||
.filter(Boolean),
|
||||
throttle: {
|
||||
ttlMs: envInt('THROTTLE_TTL_MS'),
|
||||
limit: envInt('THROTTLE_LIMIT')
|
||||
},
|
||||
uploadThrottle: {
|
||||
ttlMs: envInt('THROTTLE_UPLOAD_TTL_MS'),
|
||||
limit: envInt('THROTTLE_UPLOAD_LIMIT')
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { getAppConfig } from './index';
|
||||
|
||||
const {
|
||||
uploadThrottle: { limit, ttlMs }
|
||||
} = getAppConfig();
|
||||
|
||||
export const throttleProfiles = {
|
||||
upload: { default: { limit, ttl: ttlMs } }
|
||||
} as const;
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'reflect-metadata';
|
||||
import { validate } from './validate';
|
||||
|
||||
const validConfig = (overrides: Record<string, unknown> = {}): Record<string, unknown> => ({
|
||||
NODE_ENV: 'development',
|
||||
BACKEND_PORT: 3000,
|
||||
POSTGRES_USER: 'postgres',
|
||||
POSTGRES_PASSWORD: 'postgres',
|
||||
POSTGRES_DB: 'private_image',
|
||||
POSTGRES_HOST: 'postgres',
|
||||
POSTGRES_PORT: 5432,
|
||||
POSTGRES_MIGRATIONS_RUN: 'false',
|
||||
CORS_ORIGINS: 'http://localhost:5173',
|
||||
MAX_IMAGES_PER_UPLOAD: 10,
|
||||
MULTER_MAX_FILE_BYTES: 10485760,
|
||||
ALTCHA_ENABLED: 'true',
|
||||
ALTCHA_HMAC_KEY: 'secret',
|
||||
ALTCHA_MAX_NUMBER: 50000,
|
||||
NEVER_MODE_INACTIVITY_DAYS: 365,
|
||||
THROTTLE_TTL_MS: 60000,
|
||||
THROTTLE_LIMIT: 1000,
|
||||
THROTTLE_UPLOAD_TTL_MS: 60000,
|
||||
THROTTLE_UPLOAD_LIMIT: 20,
|
||||
...overrides
|
||||
});
|
||||
|
||||
describe('validate', () => {
|
||||
it('accepts true and false env boolean strings', () => {
|
||||
expect(() => validate(validConfig({ POSTGRES_MIGRATIONS_RUN: 'true' }))).not.toThrow();
|
||||
expect(() => validate(validConfig({ ALTCHA_ENABLED: 'false' }))).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects non-literal boolean env values', () => {
|
||||
expect(() => validate(validConfig({ ALTCHA_ENABLED: 'yes' }))).toThrow();
|
||||
expect(() => validate(validConfig({ POSTGRES_MIGRATIONS_RUN: '1' }))).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { IsEnum, IsIn, IsNotEmpty, IsNumber, IsString, Max, Min, validateSync } from 'class-validator';
|
||||
import { BOOLEAN_STRING_VALUES } from '../consts/booleanStringValues';
|
||||
import type { BooleanStringValue } from '../types/BooleanStringValue';
|
||||
import { NodeEnv } from '../types/NodeEnv';
|
||||
|
||||
class EnvironmentVariables {
|
||||
@IsNotEmpty()
|
||||
@IsEnum(NodeEnv)
|
||||
NODE_ENV: NodeEnv;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(65535)
|
||||
BACKEND_PORT: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
POSTGRES_USER: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
POSTGRES_PASSWORD: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
POSTGRES_DB: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
POSTGRES_HOST: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(65535)
|
||||
POSTGRES_PORT: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsIn(BOOLEAN_STRING_VALUES)
|
||||
POSTGRES_MIGRATIONS_RUN: BooleanStringValue;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
CORS_ORIGINS: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
MAX_IMAGES_PER_UPLOAD: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(50 * 1024 * 1024)
|
||||
MULTER_MAX_FILE_BYTES: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsIn(BOOLEAN_STRING_VALUES)
|
||||
ALTCHA_ENABLED: BooleanStringValue;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
ALTCHA_HMAC_KEY: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(1_000_000)
|
||||
ALTCHA_MAX_NUMBER: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(3650)
|
||||
NEVER_MODE_INACTIVITY_DAYS: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1000)
|
||||
@Max(3_600_000)
|
||||
THROTTLE_TTL_MS: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(1_000_000)
|
||||
THROTTLE_LIMIT: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1000)
|
||||
@Max(3_600_000)
|
||||
THROTTLE_UPLOAD_TTL_MS: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(10_000)
|
||||
THROTTLE_UPLOAD_LIMIT: number;
|
||||
}
|
||||
|
||||
export const validate = (config: Record<string, unknown>) => {
|
||||
const validatedConfig = plainToClass(EnvironmentVariables, config, {
|
||||
enableImplicitConversion: true
|
||||
});
|
||||
|
||||
const errors = validateSync(validatedConfig, {
|
||||
skipMissingProperties: false
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.toString());
|
||||
}
|
||||
|
||||
return validatedConfig;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const BOOLEAN_STRING_VALUES = ['true', 'false'] as const;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AltchaController } from './controllers/AltchaController';
|
||||
import { AltchaGuard } from './guards/AltchaGuard';
|
||||
import { AltchaService } from './services/AltchaService';
|
||||
|
||||
@Module({
|
||||
controllers: [AltchaController],
|
||||
providers: [AltchaService, AltchaGuard],
|
||||
exports: [AltchaGuard]
|
||||
})
|
||||
export class AltchaModule {}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Controller, Get, NotFoundException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AltchaService } from '../services/AltchaService';
|
||||
import { Config } from '../../../types/Config';
|
||||
|
||||
@Controller('altcha')
|
||||
export class AltchaController {
|
||||
constructor(
|
||||
private readonly altchaService: AltchaService,
|
||||
private readonly configService: ConfigService
|
||||
) {}
|
||||
|
||||
@Get('challenge')
|
||||
async getChallenge() {
|
||||
const { enabled } = this.configService.get('altcha') as Config['altcha'];
|
||||
|
||||
if (!enabled) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return this.altchaService.createChallenge();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { BadRequestException, CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Request } from 'express';
|
||||
import { AltchaService } from '../services/AltchaService';
|
||||
import { Config } from '../../../types/Config';
|
||||
|
||||
@Injectable()
|
||||
export class AltchaGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly altchaService: AltchaService,
|
||||
private readonly configService: ConfigService
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const { enabled } = this.configService.get('altcha') as Config['altcha'];
|
||||
|
||||
if (!enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const altcha = request.body?.altcha;
|
||||
|
||||
if (typeof altcha !== 'string' || altcha.length === 0) {
|
||||
throw new BadRequestException('ALTCHA verification required');
|
||||
}
|
||||
|
||||
const verified = await this.altchaService.verifyPayload(altcha);
|
||||
|
||||
if (!verified) {
|
||||
throw new BadRequestException('ALTCHA verification failed');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { createChallenge, solveChallenge } from 'altcha-lib/v1';
|
||||
import type { Payload } from 'altcha-lib/v1/types';
|
||||
import { AltchaService } from './AltchaService';
|
||||
|
||||
const hmacKey = 'test-hmac-key';
|
||||
|
||||
const encodePayload = (payload: Payload): string =>
|
||||
Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
|
||||
|
||||
describe('AltchaService', () => {
|
||||
const configService = {
|
||||
get: jest.fn(() => ({ enabled: true, hmacKey, maxNumber: 100_000 }))
|
||||
} as unknown as ConfigService;
|
||||
|
||||
const service = new AltchaService(configService);
|
||||
|
||||
it('verifies a valid ALTCHA payload', async () => {
|
||||
const challenge = await createChallenge({ hmacKey, number: 42, maxNumber: 100_000 });
|
||||
const { promise } = solveChallenge(challenge.challenge, challenge.salt, challenge.algorithm, 100_000);
|
||||
const solution = await promise;
|
||||
|
||||
expect(solution).not.toBeNull();
|
||||
|
||||
const payload: Payload = {
|
||||
algorithm: challenge.algorithm,
|
||||
challenge: challenge.challenge,
|
||||
number: solution!.number,
|
||||
salt: challenge.salt,
|
||||
signature: challenge.signature
|
||||
};
|
||||
|
||||
await expect(service.verifyPayload(encodePayload(payload))).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('rejects an invalid ALTCHA payload', async () => {
|
||||
await expect(service.verifyPayload('not-valid-base64-json')).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { createChallenge, verifySolution } from 'altcha-lib/v1';
|
||||
import type { Challenge } from 'altcha-lib/v1/types';
|
||||
import { Config } from '../../../types/Config';
|
||||
|
||||
@Injectable()
|
||||
export class AltchaService {
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
async createChallenge(): Promise<Challenge> {
|
||||
const { hmacKey, maxNumber } = this.configService.get('altcha') as Config['altcha'];
|
||||
|
||||
return createChallenge({
|
||||
hmacKey,
|
||||
maxNumber
|
||||
});
|
||||
}
|
||||
|
||||
async verifyPayload(altcha: string): Promise<boolean> {
|
||||
const { hmacKey } = this.configService.get('altcha') as Config['altcha'];
|
||||
|
||||
return verifySolution(altcha, hmacKey);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
|
||||
import { StorageService } from './services/StorageService';
|
||||
|
||||
@Module({
|
||||
providers: [StorageService],
|
||||
exports: [StorageService]
|
||||
providers: [StorageService]
|
||||
})
|
||||
export class StorageModule {}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export type AltchaConfig = {
|
||||
enabled: boolean;
|
||||
hmacKey: string;
|
||||
maxNumber: number;
|
||||
};
|
||||
@@ -1,7 +1,10 @@
|
||||
import { NodeEnv } from './NodeEnv';
|
||||
import { ThrottleConfig } from './ThrottleConfig';
|
||||
|
||||
export type AppConfig = {
|
||||
port: number;
|
||||
nodeEnv: NodeEnv;
|
||||
corsOrigins: string[];
|
||||
throttle: ThrottleConfig;
|
||||
uploadThrottle: ThrottleConfig;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { BOOLEAN_STRING_VALUES } from '../consts/booleanStringValues';
|
||||
|
||||
export type BooleanStringValue = (typeof BOOLEAN_STRING_VALUES)[number];
|
||||
@@ -1,19 +1,9 @@
|
||||
import { AltchaConfig } from './AltchaConfig';
|
||||
import { AppConfig } from './AppConfig';
|
||||
|
||||
export interface PostgresConfig {
|
||||
type: 'postgres';
|
||||
url: string;
|
||||
host: string;
|
||||
username: string;
|
||||
password: string;
|
||||
port: number;
|
||||
database: string;
|
||||
entities: string[];
|
||||
migrations: string[];
|
||||
migrationsRun: boolean;
|
||||
}
|
||||
import { PostgresConfig } from './PostgresConfig';
|
||||
|
||||
export interface Config {
|
||||
app: AppConfig;
|
||||
altcha: AltchaConfig;
|
||||
postgres: PostgresConfig;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface PostgresConfig {
|
||||
type: 'postgres';
|
||||
url: string;
|
||||
host: string;
|
||||
username: string;
|
||||
password: string;
|
||||
port: number;
|
||||
database: string;
|
||||
entities: string[];
|
||||
migrations: string[];
|
||||
migrationsRun: boolean;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type ThrottleConfig = {
|
||||
ttlMs: number;
|
||||
limit: number;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile*
|
||||
npm-debug.log
|
||||
Reference in New Issue
Block a user