add ALTCHA challenge endpoint and env config.
GET /api/altcha/challenge, nullcart-style env validation, and ALTCHA_ENABLED wiring.
This commit is contained in:
+1
-2
@@ -21,8 +21,7 @@ CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
|||||||
MAX_IMAGES_PER_UPLOAD=10
|
MAX_IMAGES_PER_UPLOAD=10
|
||||||
MULTER_MAX_FILE_BYTES=10485760
|
MULTER_MAX_FILE_BYTES=10485760
|
||||||
|
|
||||||
CAPTCHA_ENABLED=true
|
ALTCHA_ENABLED=true
|
||||||
|
|
||||||
ALTCHA_HMAC_KEY=change-me-use-long-random-secret
|
ALTCHA_HMAC_KEY=change-me-use-long-random-secret
|
||||||
ALTCHA_MAX_NUMBER=50000
|
ALTCHA_MAX_NUMBER=50000
|
||||||
|
|
||||||
|
|||||||
Generated
+70
@@ -14,6 +14,7 @@
|
|||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/typeorm": "^12.0.2",
|
"@nestjs/typeorm": "^12.0.2",
|
||||||
|
"altcha-lib": "^2.6.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.15.1",
|
"class-validator": "^0.15.1",
|
||||||
"pg": "^8.23.1",
|
"pg": "^8.23.1",
|
||||||
@@ -3269,6 +3270,75 @@
|
|||||||
"ajv": "^8.8.2"
|
"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": {
|
"node_modules/ansi-colors": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/typeorm": "^12.0.2",
|
"@nestjs/typeorm": "^12.0.2",
|
||||||
|
"altcha-lib": "^2.6.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.15.1",
|
"class-validator": "^0.15.1",
|
||||||
"pg": "^8.23.1",
|
"pg": "^8.23.1",
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule, ConfigService, registerAs } from '@nestjs/config';
|
import { ConfigModule, ConfigService, registerAs } from '@nestjs/config';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
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 { HealthCheckModule } from './modules/healthCheck/HealthCheckModule';
|
||||||
import { StorageModule } from './modules/storage/StorageModule';
|
import { StorageModule } from './modules/storage/StorageModule';
|
||||||
import { UploadModule } from './modules/upload/UploadModule';
|
import { UploadModule } from './modules/upload/UploadModule';
|
||||||
@@ -11,13 +13,19 @@ import { Config } from './types/Config';
|
|||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
load: [registerAs('postgres', getPostgresConfig), registerAs('app', getAppConfig)]
|
validate,
|
||||||
|
load: [
|
||||||
|
registerAs('postgres', getPostgresConfig),
|
||||||
|
registerAs('app', getAppConfig),
|
||||||
|
registerAs('altcha', getAltchaConfig)
|
||||||
|
]
|
||||||
}),
|
}),
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
useFactory: (configService: ConfigService) => configService.get('postgres') as Config['postgres'],
|
useFactory: (configService: ConfigService) => configService.get('postgres') as Config['postgres'],
|
||||||
inject: [ConfigService]
|
inject: [ConfigService]
|
||||||
}),
|
}),
|
||||||
HealthCheckModule,
|
HealthCheckModule,
|
||||||
|
AltchaModule,
|
||||||
StorageModule,
|
StorageModule,
|
||||||
UploadModule
|
UploadModule
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { AppConfig } from '../types/AppConfig';
|
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';
|
import { NodeEnv } from '../types/NodeEnv';
|
||||||
|
|
||||||
const env = (key: string): string => process.env[key] || '';
|
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 => {
|
export const getAppConfig = (): AppConfig => {
|
||||||
const rawOrigins = env('CORS_ORIGINS');
|
const rawOrigins = env('CORS_ORIGINS');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { plainToClass } from 'class-transformer';
|
||||||
|
import { IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsString, Max, Min, validateSync } from 'class-validator';
|
||||||
|
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()
|
||||||
|
@IsBoolean()
|
||||||
|
POSTGRES_MIGRATIONS_RUN: boolean;
|
||||||
|
|
||||||
|
@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()
|
||||||
|
@IsBoolean()
|
||||||
|
ALTCHA_ENABLED: boolean;
|
||||||
|
|
||||||
|
@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_UPLOAD_TTL_MS: number;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Max(10_000)
|
||||||
|
THROTTLE_UPLOAD_LIMIT: number;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1000)
|
||||||
|
@Max(3_600_000)
|
||||||
|
THROTTLE_HOTLINK_PER_IP_TTL_MS: number;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Max(1_000_000)
|
||||||
|
THROTTLE_HOTLINK_PER_IP_LIMIT: number;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1000)
|
||||||
|
@Max(3_600_000)
|
||||||
|
THROTTLE_HOTLINK_PER_ID_TTL_MS: number;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Max(1_000_000)
|
||||||
|
THROTTLE_HOTLINK_PER_ID_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,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AltchaController } from './controllers/AltchaController';
|
||||||
|
import { AltchaService } from './services/AltchaService';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [AltchaController],
|
||||||
|
providers: [AltchaService],
|
||||||
|
exports: [AltchaService]
|
||||||
|
})
|
||||||
|
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,19 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { createChallenge } 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type AltchaConfig = {
|
||||||
|
enabled: boolean;
|
||||||
|
hmacKey: string;
|
||||||
|
maxNumber: number;
|
||||||
|
};
|
||||||
@@ -1,19 +1,9 @@
|
|||||||
|
import { AltchaConfig } from './AltchaConfig';
|
||||||
import { AppConfig } from './AppConfig';
|
import { AppConfig } from './AppConfig';
|
||||||
|
import { PostgresConfig } from './PostgresConfig';
|
||||||
export interface PostgresConfig {
|
|
||||||
type: 'postgres';
|
|
||||||
url: string;
|
|
||||||
host: string;
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
port: number;
|
|
||||||
database: string;
|
|
||||||
entities: string[];
|
|
||||||
migrations: string[];
|
|
||||||
migrationsRun: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
app: AppConfig;
|
app: AppConfig;
|
||||||
|
altcha: AltchaConfig;
|
||||||
postgres: PostgresConfig;
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user