chore: http.paths - access log, auth, metrics

This commit is contained in:
devlikepro committed 2026-08-31 17:40:01 +07:00
1 parent b7c4eac93b
commit 2595ccfe7b
13 files changed
+218 -70

No files matched your search

+5
View File
@@ -11,8 +11,13 @@ import { UniqueAppResolver } from '@waha/apps/app_sdk/services/UniqueAppResolver
import { Auth } from '@waha/core/auth/config';
import { AppRuntimeConfig } from '@waha/apps/app_sdk/apps/AppRuntime';
import { GetApps } from '@waha/apps/app_sdk/apps/registry';
import { HttpPathsRegistration } from '@waha/plugins/http.paths.module';
const QUEUES_IMPORTS_REQUIRED = [
HttpPathsRegistration(
{ prefix: '/jobs', include: { authBasic: false } },
{ prefix: '/jobs/', include: { authBasic: false, accessLog: false } },
),
BullModule.forRoot({
connection: {
url: process.env.REDIS_URL || 'redis://:redis@localhost:6379',
+5
View File
@@ -39,10 +39,15 @@ import { TaskContactsPullConsumer } from '@waha/apps/chatwoot/consumers/task/con
import { TaskMessagesPullConsumer } from '@waha/apps/chatwoot/consumers/task/messages.pull';
import { BullModule } from '@nestjs/bullmq';
import { QueueManager } from '@waha/apps/chatwoot/services/QueueManager';
import { HttpPathsRegistration } from '@waha/plugins/http.paths.module';
const CONTROLLERS = [ChatwootWebhookController, ChatwootLocalesController];
const IMPORTS = lodash.flatten([
HttpPathsRegistration({
prefix: '/webhooks/',
include: { authBasic: false },
}),
BullModule.registerFlowProducer({
name: FlowProducerName.MESSAGES_PULL_FLOW,
}),
+7 -1
View File
@@ -3,6 +3,7 @@ import { AppName } from '@waha/apps/app_sdk/apps/apps';
import { McpController } from '@waha/apps/mcp/api/mcp.controller';
import { McpService } from '@waha/apps/mcp/mcp.service';
import { McpAppService } from '@waha/apps/mcp/services/McpAppService';
import { HttpPathsRegistration } from '@waha/plugins/http.paths.module';
const McpAppModule: AppModule = {
name: AppName.mcp,
@@ -18,7 +19,12 @@ const McpAppModule: AppModule = {
unique: false,
},
nestjs: {
imports: [],
imports: [
HttpPathsRegistration(
{ prefix: '/mcp', include: { authBasic: false } },
{ prefix: 'mcp', include: { authApiKey: true } },
),
],
controllers: [McpController],
providers: [McpService, McpAppService],
},
+3 -17
View File
@@ -4,8 +4,7 @@ import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger';
import { DECORATORS } from '@nestjs/swagger/dist/constants';
import { GetAppsApiTags } from '@waha/apps/app_sdk/api/tags';
import { BasicAuthFunction } from '@waha/core/auth/basicAuth';
import { DashboardConfigServiceCore } from '@waha/core/config/DashboardConfigServiceCore';
import { getPrometheusExcludePaths } from '@waha/modules/waha-prometheus/prometheus.config';
import { HttpPathsService } from '@waha/plugins/HttpPathsService';
import { Logger } from 'nestjs-pino';
import { WhatsappConfigService } from '../config.service';
@@ -194,21 +193,8 @@ export class SwaggerConfiguratorCore {
setUpAuth(credentials: [string, string]): void {
const [username, password] = credentials;
const dashboardConfig = this.app.get(DashboardConfigServiceCore);
const config = this.app.get(WhatsappConfigService);
const exclude = lodash.uniq([
'/api/',
'/mcp',
dashboardConfig.dashboardUri,
'/health',
'/ping',
'/ws',
'/webhooks/',
'/jobs',
'/jobs/',
...config.getExcludedFullPaths(),
...getPrometheusExcludePaths(),
]);
const httpPaths = this.app.get(HttpPathsService);
const exclude = lodash.uniq(httpPaths.globalAuthExcludePrefixes());
const authFunction = BasicAuthFunction(username, password, exclude);
this.app.use(authFunction);
+56 -38
View File
@@ -29,15 +29,14 @@ import { MediaLocalStorageModule } from '@waha/core/media/local/media.local.stor
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
import { MediaPsqlStorageModule } from '@waha/core/media/psql/media.psql.storage.module';
import { MediaS3StorageModule } from '@waha/core/media/s3/media.s3.storage.module';
import { HttpPathsModule } from '@waha/plugins/http.paths.module';
import { HttpPathsService } from '@waha/plugins/HttpPathsService';
import { SessionPluginsModule } from '@waha/plugins/session.plugins.module';
import { isPresenceAutoOnlineEnabled } from '@waha/modules/waha-maintain-online-status/maintain-online-status.config';
import { MaintainOnlineStatusModule } from '@waha/modules/waha-maintain-online-status/maintain-online-status.module';
import { isJidEngine } from '@waha/modules/waha-wid-jid/wid-jid.plugins';
import { MessageSourceModule } from '@waha/modules/waha-message-source/message-source.module';
import {
getPrometheusPath,
isPrometheusEnabled,
} from '@waha/modules/waha-prometheus/prometheus.config';
import { isPrometheusEnabled } from '@waha/modules/waha-prometheus/prometheus.config';
import { PrometheusModule } from '@waha/modules/waha-prometheus/prometheus.module';
import { WebhookModule } from '@waha/modules/waha-webhook/webhook.module';
import { SessionRuntimeInfoModule } from '@waha/modules/waha-session-runtime-info/session-runtime-info.module';
@@ -94,40 +93,37 @@ import { SessionService } from '@waha/core/services/SessionService';
export const IMPORTS_CORE = [
...AppsModuleExports.imports,
LoggerModule.forRoot({
renameContext: 'name',
pinoHttp: {
level: getPinoLogLevel(),
useLevel: getPinoHttpUseLevel(),
transport: getPinoTransport(),
autoLogging: {
ignore: (req) => {
return (
req.url.startsWith('/ping') ||
req.url.startsWith(getPrometheusPath()) ||
req.url.startsWith('/dashboard/') ||
req.url.startsWith('/api/files/') ||
req.url.startsWith('/api/s3/') ||
req.url.startsWith('/jobs/')
);
LoggerModule.forRootAsync({
imports: [HttpPathsModule],
inject: [HttpPathsService],
useFactory: (httpPaths: HttpPathsService) => {
return {
renameContext: 'name',
pinoHttp: {
level: getPinoLogLevel(),
useLevel: getPinoHttpUseLevel(),
transport: getPinoTransport(),
autoLogging: {
ignore: (req) => httpPaths.isAccessLogIgnored(req.url),
},
redact: {
paths: ['req.query["x-api-key"]'],
censor: '[REDACTED]',
},
customAttributeKeys: { req: 'req', res: 'res' },
serializers: {
req: (req) => ({
method: req.method,
url: redactUrlParams('x-api-key', req.url, req.query),
query: req.query,
params: req.params,
}),
res: (res) => ({
statusCode: res.statusCode,
}),
},
},
},
redact: {
paths: ['req.query["x-api-key"]'],
censor: '[REDACTED]',
},
customAttributeKeys: { req: 'req', res: 'res' },
serializers: {
req: (req) => ({
method: req.method,
url: redactUrlParams('x-api-key', req.url, req.query),
query: req.query,
params: req.params,
}),
res: (res) => ({
statusCode: res.statusCode,
}),
},
};
},
}),
ConfigModule.forRoot({
@@ -154,6 +150,7 @@ export const IMPORTS_CORE = [
}),
PassportModule,
TerminusModule,
HttpPathsModule,
SessionPluginsModule,
SessionRuntimeInfoModule,
WebhookModule,
@@ -281,8 +278,29 @@ export class AppModuleCore {
constructor(
protected config: WhatsappConfigService,
private dashboardConfig: DashboardConfigServiceCore,
private httpPaths: HttpPathsService,
) {
this.startTimestamp = Date.now();
this.httpPaths.register(
{ prefix: '/ping', include: { accessLog: false, authBasic: false } },
{ prefix: '/api/', include: { authBasic: false } },
{ prefix: 'api', include: { authApiKey: true } },
{ prefix: '/health', include: { authBasic: false } },
{ prefix: 'health', include: { authApiKey: true } },
{ prefix: '/ws', include: { authBasic: false } },
{
prefix: this.dashboardConfig.dashboardUri,
include: { authBasic: false },
},
{
prefix: this.dashboardConfig.dashboardUri + '/',
include: { accessLog: false },
},
);
// WHATSAPP_API_KEY_EXCLUDE_PATH - env-driven auth exclusions
for (const path of this.config.getExcludedFullPaths()) {
this.httpPaths.register({ prefix: path, include: { authBasic: false } });
}
}
static getHttpsOptions(logger: Logger) {
@@ -311,7 +329,7 @@ export class AppModuleCore {
consumer
.apply(ApiKeyAuthMiddleware)
.exclude(...exclude)
.forRoutes('api', 'health', 'mcp');
.forRoutes(...this.httpPaths.apiKeyRoutes());
// Dashboard
const dashboardCredentials = this.dashboardConfig.credentials;
@@ -4,9 +4,12 @@ import { WhatsappConfigService } from '@waha/config.service';
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
import { MediaLocalStorageFactory } from '@waha/core/media/local/MediaLocalStorageFactory';
import { MediaStorageFactory } from '@waha/core/media/MediaStorageFactory';
import { HttpPathsModule } from '@waha/plugins/http.paths.module';
import { HttpPathsService } from '@waha/plugins/HttpPathsService';
@Module({
imports: [
HttpPathsModule,
ServeStaticModule.forRootAsync({
imports: [],
extraProviders: [MediaLocalStorageConfig, WhatsappConfigService],
@@ -31,4 +34,11 @@ import { MediaStorageFactory } from '@waha/core/media/MediaStorageFactory';
],
exports: [MediaStorageFactory],
})
export class MediaLocalStorageModule {}
export class MediaLocalStorageModule {
constructor(config: MediaLocalStorageConfig, httpPaths: HttpPathsService) {
httpPaths.register({
prefix: config.filesUri + '/',
include: { accessLog: false, metrics: false },
});
}
}
@@ -3,11 +3,14 @@ import { WhatsappConfigService } from '@waha/config.service';
import { EngineConfigService } from '@waha/core/config/EngineConfigService';
import { MediaStorageFactory } from '@waha/core/media/MediaStorageFactory';
import { PsqlFilesController } from '@waha/core/media/psql/api/psql.files.controller';
import { HttpPathsModule } from '@waha/plugins/http.paths.module';
import { HttpPathsService } from '@waha/plugins/HttpPathsService';
import { MediaPsqlStorageConfig } from './MediaPsqlStorageConfig';
import { MediaPsqlStorageFactory } from './MediaPsqlStorageFactory';
@Module({
imports: [HttpPathsModule],
providers: [
{
provide: MediaStorageFactory,
@@ -21,4 +24,11 @@ import { MediaPsqlStorageFactory } from './MediaPsqlStorageFactory';
exports: [MediaStorageFactory],
controllers: [PsqlFilesController],
})
export class MediaPsqlStorageModule {}
export class MediaPsqlStorageModule {
constructor(config: MediaPsqlStorageConfig, httpPaths: HttpPathsService) {
httpPaths.register({
prefix: config.filesUri + '/',
include: { accessLog: false, metrics: false },
});
}
}
+11 -1
View File
@@ -10,8 +10,11 @@ import {
S3ProxyUrl,
S3Url,
} from '@waha/core/media/s3/MediaS3UrlResolver';
import { HttpPathsModule } from '@waha/plugins/http.paths.module';
import { HttpPathsService } from '@waha/plugins/HttpPathsService';
@Module({
imports: [HttpPathsModule],
providers: [
{
provide: S3Client,
@@ -44,4 +47,11 @@ import {
exports: [MediaStorageFactory],
controllers: [S3ProxyController],
})
export class MediaS3StorageModule {}
export class MediaS3StorageModule {
constructor(httpPaths: HttpPathsService) {
httpPaths.register({
prefix: '/api/s3/',
include: { accessLog: false, metrics: false },
});
}
}
@@ -46,13 +46,6 @@ export function getPrometheusPath(): string {
return process.env[Env.WAHA_PROMETHEUS_PATH] || DEFAULT_PATH;
}
export function getPrometheusExcludePaths(): string[] {
if (!isPrometheusEnabled(process.env)) {
return [];
}
return [getPrometheusPath()];
}
@Injectable()
export class PrometheusConfigService {
private eventUnmask = new EventWildUnmask(WAHAEvents, WAHAEventsWild);
@@ -1,18 +1,18 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { PrometheusConfigService } from '@waha/modules/waha-prometheus/prometheus.config';
import { WahaMetrics } from '@waha/modules/waha-prometheus/prometheus.metrics';
import { HttpPathsService } from '@waha/plugins/HttpPathsService';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class HttpMetricsMiddleware implements NestMiddleware {
constructor(
private metrics: WahaMetrics,
private config: PrometheusConfigService,
private httpPaths: HttpPathsService,
) {}
use(req: Request, res: Response, next: NextFunction): void {
const pathname = (req.originalUrl || req.url || '').split('?')[0];
if (pathname.startsWith(this.config.path)) {
if (this.httpPaths.isHttpMetricsIgnored(pathname)) {
next();
return;
}
@@ -10,12 +10,15 @@ import { EventMetricsSubscriber } from '@waha/modules/waha-prometheus/prometheus
import { HttpMetricsMiddleware } from '@waha/modules/waha-prometheus/prometheus.http.middleware';
import { WahaMetrics } from '@waha/modules/waha-prometheus/prometheus.metrics';
import { SessionMetricsCollector } from '@waha/modules/waha-prometheus/prometheus.sessions.collector';
import { HttpPathsModule } from '@waha/plugins/http.paths.module';
import { HttpPathsService } from '@waha/plugins/HttpPathsService';
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: PrometheusEnvSchema,
}),
HttpPathsModule,
],
providers: [
PrometheusConfigService,
@@ -32,7 +35,15 @@ import { SessionMetricsCollector } from '@waha/modules/waha-prometheus/prometheu
* optional basic auth via WAHA_PROMETHEUS_USERNAME and WAHA_PROMETHEUS_PASSWORD.
*/
export class PrometheusModule implements NestModule {
constructor(private config: PrometheusConfigService) {}
constructor(
private config: PrometheusConfigService,
httpPaths: HttpPathsService,
) {
httpPaths.register({
prefix: this.config.path,
include: { accessLog: false, authBasic: false, metrics: false },
});
}
configure(consumer: MiddlewareConsumer) {
const credentials = this.config.credentials;
+65
View File
@@ -0,0 +1,65 @@
import { Injectable } from '@nestjs/common';
/**
* What the path takes part in.
*/
export interface HttpPathInclude {
/** Log requests to the path in the HTTP access log (default true). */
accessLog?: boolean;
/** Protect the path with the global (Swagger) basic auth (default true). */
authBasic?: boolean;
/** Protect the route with the api key middleware (default false). */
authApiKey?: boolean;
/** Count requests to the path in the HTTP metrics (default true). */
metrics?: boolean;
}
/**
* A URL prefix contributed by a module - matched with url.startsWith(prefix);
* for authApiKey the prefix is passed verbatim to MiddlewareConsumer.forRoutes().
*/
export interface HttpPathContribution {
prefix: string;
include?: HttpPathInclude;
}
/**
* Collects HTTP path contributions from modules, so core auth, logging and metrics can honor them
* without importing the modules.
*/
@Injectable()
export class HttpPathsService {
private contributions: HttpPathContribution[] = [];
register(...contributions: HttpPathContribution[]): void {
this.contributions.push(...contributions);
}
isAccessLogIgnored(url: string): boolean {
return this.contributions.some(
(contribution) =>
contribution.include?.accessLog === false &&
url.startsWith(contribution.prefix),
);
}
globalAuthExcludePrefixes(): string[] {
return this.contributions
.filter((contribution) => contribution.include?.authBasic === false)
.map((contribution) => contribution.prefix);
}
isHttpMetricsIgnored(pathname: string): boolean {
return this.contributions.some(
(contribution) =>
contribution.include?.metrics === false &&
pathname.startsWith(contribution.prefix),
);
}
apiKeyRoutes(): string[] {
return this.contributions
.filter((contribution) => contribution.include?.authApiKey === true)
.map((contribution) => contribution.prefix);
}
}
+29
View File
@@ -0,0 +1,29 @@
import { Inject, Module, Type } from '@nestjs/common';
import {
HttpPathContribution,
HttpPathsService,
} from '@waha/plugins/HttpPathsService';
@Module({
providers: [HttpPathsService],
exports: [HttpPathsService],
})
export class HttpPathsModule {}
/**
* Import-time registration for modules that have no class of their own (e.g. object-style apps) -
* returns a module that registers the contributions in HttpPathsService at bootstrap.
*/
export function HttpPathsRegistration(
...contributions: HttpPathContribution[]
): Type<any> {
@Module({
imports: [HttpPathsModule],
})
class HttpPathsRegistrationModule {
constructor(@Inject(HttpPathsService) httpPaths: HttpPathsService) {
httpPaths.register(...contributions);
}
}
return HttpPathsRegistrationModule;
}