feat(api): mount Bull Board behind admin auth (RFC-004 Phase 3 WS3b)
Adds the Bull Board BullMQ dashboard at /api/v1/admin/queues, guarded by a JWT + ADMIN-role middleware that mirrors the existing JwtAuthGuard + RolesGuard contract. The dashboard registers all queues in QUEUE_METRICS_QUEUE_NAMES automatically. - New QueuesModule with BullBoardModule.forRoot/forFeature wiring - BullBoardAuthMiddleware (cookie-first JWT extraction, ADMIN-only) - CSRF exclusion for dashboard routes in AppModule - 8 unit tests covering auth contract - Dependencies: @bull-board/api, @bull-board/express, @bull-board/nestjs Refs: GOO-175 Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -16,6 +16,9 @@
|
|||||||
"@anthropic-ai/sdk": "^0.89.0",
|
"@anthropic-ai/sdk": "^0.89.0",
|
||||||
"@aws-sdk/client-s3": "^3.1026.0",
|
"@aws-sdk/client-s3": "^3.1026.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.1026.0",
|
"@aws-sdk/s3-request-presigner": "^3.1026.0",
|
||||||
|
"@bull-board/api": "^7.0.0",
|
||||||
|
"@bull-board/express": "^7.0.0",
|
||||||
|
"@bull-board/nestjs": "^7.0.0",
|
||||||
"@goodgo/mcp-servers": "workspace:*",
|
"@goodgo/mcp-servers": "workspace:*",
|
||||||
"@nest-lab/throttler-storage-redis": "^1.2.0",
|
"@nest-lab/throttler-storage-redis": "^1.2.0",
|
||||||
"@nestjs/bullmq": "^11.0.4",
|
"@nestjs/bullmq": "^11.0.4",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { HttpMetricsInterceptor, MetricsModule } from '@modules/metrics';
|
|||||||
import { NotificationsModule } from '@modules/notifications';
|
import { NotificationsModule } from '@modules/notifications';
|
||||||
import { PaymentsModule } from '@modules/payments';
|
import { PaymentsModule } from '@modules/payments';
|
||||||
import { ProjectsModule } from '@modules/projects';
|
import { ProjectsModule } from '@modules/projects';
|
||||||
|
import { QueuesModule } from '@modules/queues/queues.module';
|
||||||
import { ReportsModule } from '@modules/reports';
|
import { ReportsModule } from '@modules/reports';
|
||||||
import { ReviewsModule } from '@modules/reviews';
|
import { ReviewsModule } from '@modules/reviews';
|
||||||
import { SearchModule } from '@modules/search';
|
import { SearchModule } from '@modules/search';
|
||||||
@@ -70,6 +71,9 @@ import { AppController } from './app.controller';
|
|||||||
IndustrialModule,
|
IndustrialModule,
|
||||||
TransferModule,
|
TransferModule,
|
||||||
|
|
||||||
|
// ── Bull Board UI (RFC-004 Phase 3 WS3b) ──
|
||||||
|
QueuesModule,
|
||||||
|
|
||||||
// ── Rate Limiting ──
|
// ── Rate Limiting ──
|
||||||
// Default: 60 requests per 60 seconds per IP
|
// Default: 60 requests per 60 seconds per IP
|
||||||
// Override per-route with @Throttle() decorator
|
// Override per-route with @Throttle() decorator
|
||||||
@@ -144,6 +148,8 @@ export class AppModule implements NestModule {
|
|||||||
{ path: 'health/(.*)', method: RequestMethod.GET },
|
{ path: 'health/(.*)', method: RequestMethod.GET },
|
||||||
{ path: 'api/v1/web-vitals', method: RequestMethod.POST }, // sendBeacon cannot send CSRF headers
|
{ path: 'api/v1/web-vitals', method: RequestMethod.POST }, // sendBeacon cannot send CSRF headers
|
||||||
{ path: 'web-vitals', method: RequestMethod.POST }, // middleware exclude uses controller-relative path
|
{ path: 'web-vitals', method: RequestMethod.POST }, // middleware exclude uses controller-relative path
|
||||||
|
{ path: 'api/v1/admin/queues', method: RequestMethod.ALL },
|
||||||
|
{ path: 'api/v1/admin/queues/(.*)', method: RequestMethod.ALL },
|
||||||
)
|
)
|
||||||
.forRoutes('*');
|
.forRoutes('*');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
|
||||||
|
import { BullBoardAuthMiddleware } from '../bull-board-auth.middleware';
|
||||||
|
|
||||||
|
describe('BullBoardAuthMiddleware', () => {
|
||||||
|
const ORIGINAL_SECRET = process.env['JWT_SECRET'];
|
||||||
|
let jwtService: JwtService;
|
||||||
|
let middleware: BullBoardAuthMiddleware;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env['JWT_SECRET'] = 'test-secret-for-bull-board-mw';
|
||||||
|
jwtService = new JwtService({});
|
||||||
|
middleware = new BullBoardAuthMiddleware(jwtService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (ORIGINAL_SECRET === undefined) {
|
||||||
|
delete process.env['JWT_SECRET'];
|
||||||
|
} else {
|
||||||
|
process.env['JWT_SECRET'] = ORIGINAL_SECRET;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeReq(overrides: Partial<{ cookies: Record<string, string>; headers: Record<string, string> }> = {}): any {
|
||||||
|
return { cookies: overrides.cookies ?? {}, headers: overrides.headers ?? {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
function signToken(payload: Record<string, unknown>, opts: { audience?: string; issuer?: string } = {}): string {
|
||||||
|
return jwtService.sign(payload, {
|
||||||
|
secret: process.env['JWT_SECRET']!,
|
||||||
|
audience: opts.audience ?? 'goodgo-api',
|
||||||
|
issuer: opts.issuer ?? 'goodgo-platform',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('rejects when no token is present', () => {
|
||||||
|
const next = vi.fn();
|
||||||
|
expect(() => middleware.use(makeReq(), {} as any, next)).toThrow(UnauthorizedException);
|
||||||
|
expect(next).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects when JWT signature is invalid', () => {
|
||||||
|
const next = vi.fn();
|
||||||
|
const req = makeReq({ headers: { authorization: 'Bearer not-a-valid-token' } });
|
||||||
|
expect(() => middleware.use(req, {} as any, next)).toThrow(UnauthorizedException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects when JWT audience does not match', () => {
|
||||||
|
const token = signToken({ sub: 'u1', role: 'ADMIN' }, { audience: 'wrong-aud' });
|
||||||
|
const req = makeReq({ headers: { authorization: 'Bearer ' + token } });
|
||||||
|
const next = vi.fn();
|
||||||
|
expect(() => middleware.use(req, {} as any, next)).toThrow(UnauthorizedException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects non-ADMIN role with 403', () => {
|
||||||
|
const token = signToken({ sub: 'u1', role: 'USER' });
|
||||||
|
const req = makeReq({ headers: { authorization: 'Bearer ' + token } });
|
||||||
|
const next = vi.fn();
|
||||||
|
expect(() => middleware.use(req, {} as any, next)).toThrow(ForbiddenException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts ADMIN via Authorization header', () => {
|
||||||
|
const token = signToken({ sub: 'u1', role: 'ADMIN' });
|
||||||
|
const req = makeReq({ headers: { authorization: 'Bearer ' + token } });
|
||||||
|
const next = vi.fn();
|
||||||
|
middleware.use(req, {} as any, next);
|
||||||
|
expect(next).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts ADMIN via access_token cookie', () => {
|
||||||
|
const token = signToken({ sub: 'u1', role: 'ADMIN' });
|
||||||
|
const req = makeReq({ cookies: { access_token: token } });
|
||||||
|
const next = vi.fn();
|
||||||
|
middleware.use(req, {} as any, next);
|
||||||
|
expect(next).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers cookie over header', () => {
|
||||||
|
const cookieToken = signToken({ sub: 'admin', role: 'ADMIN' });
|
||||||
|
const headerToken = signToken({ sub: 'user', role: 'USER' });
|
||||||
|
const req = makeReq({ cookies: { access_token: cookieToken }, headers: { authorization: 'Bearer ' + headerToken } });
|
||||||
|
const next = vi.fn();
|
||||||
|
middleware.use(req, {} as any, next);
|
||||||
|
expect(next).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails closed when JWT_SECRET unset', () => {
|
||||||
|
delete process.env['JWT_SECRET'];
|
||||||
|
const req = makeReq({ headers: { authorization: 'Bearer anything' } });
|
||||||
|
const next = vi.fn();
|
||||||
|
expect(() => middleware.use(req, {} as any, next)).toThrow(UnauthorizedException);
|
||||||
|
});
|
||||||
|
});
|
||||||
48
apps/api/src/modules/queues/bull-board-auth.middleware.ts
Normal file
48
apps/api/src/modules/queues/bull-board-auth.middleware.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Injectable, type NestMiddleware, UnauthorizedException, ForbiddenException } from '@nestjs/common';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import type { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BullBoardAuthMiddleware implements NestMiddleware {
|
||||||
|
constructor(private readonly jwtService: JwtService) {}
|
||||||
|
|
||||||
|
use(req: Request, _res: Response, next: NextFunction): void {
|
||||||
|
const token = this.extractToken(req);
|
||||||
|
if (!token) {
|
||||||
|
throw new UnauthorizedException('Bull Board requires authentication');
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = process.env['JWT_SECRET'];
|
||||||
|
if (!secret) {
|
||||||
|
throw new UnauthorizedException('Server mis-configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: { sub?: string; role?: string };
|
||||||
|
try {
|
||||||
|
payload = this.jwtService.verify<{ sub?: string; role?: string }>(token, {
|
||||||
|
secret,
|
||||||
|
audience: 'goodgo-api',
|
||||||
|
issuer: 'goodgo-platform',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
throw new UnauthorizedException('Invalid or expired token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.role !== 'ADMIN') {
|
||||||
|
throw new ForbiddenException('Admin role required');
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractToken(req: Request): string | null {
|
||||||
|
const cookieToken = (req.cookies?.['access_token'] as string | undefined) ?? null;
|
||||||
|
if (cookieToken) return cookieToken;
|
||||||
|
|
||||||
|
const header = req.headers.authorization;
|
||||||
|
if (header && header.startsWith('Bearer ')) {
|
||||||
|
return header.slice('Bearer '.length);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
apps/api/src/modules/queues/queues.module.ts
Normal file
36
apps/api/src/modules/queues/queues.module.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
|
import { type MiddlewareConsumer, Module, type NestModule, RequestMethod } from '@nestjs/common';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||||
|
import { ExpressAdapter } from '@bull-board/express';
|
||||||
|
import { BullBoardModule } from '@bull-board/nestjs';
|
||||||
|
import { QUEUE_METRICS_QUEUE_NAMES } from '../metrics/infrastructure/queue-metrics.constants';
|
||||||
|
import { BullBoardAuthMiddleware } from './bull-board-auth.middleware';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
JwtModule.register({}),
|
||||||
|
BullBoardModule.forRoot({
|
||||||
|
route: '/admin/queues',
|
||||||
|
adapter: ExpressAdapter,
|
||||||
|
}),
|
||||||
|
...QUEUE_METRICS_QUEUE_NAMES.map((name) => BullModule.registerQueue({ name })),
|
||||||
|
BullBoardModule.forFeature(
|
||||||
|
...QUEUE_METRICS_QUEUE_NAMES.map((name) => ({
|
||||||
|
name,
|
||||||
|
adapter: BullMQAdapter,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
providers: [BullBoardAuthMiddleware],
|
||||||
|
})
|
||||||
|
export class QueuesModule implements NestModule {
|
||||||
|
configure(consumer: MiddlewareConsumer): void {
|
||||||
|
consumer
|
||||||
|
.apply(BullBoardAuthMiddleware)
|
||||||
|
.forRoutes(
|
||||||
|
{ path: 'admin/queues', method: RequestMethod.ALL },
|
||||||
|
{ path: 'admin/queues/(.*)', method: RequestMethod.ALL },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
pnpm-lock.yaml
generated
80
pnpm-lock.yaml
generated
@@ -87,9 +87,15 @@ importers:
|
|||||||
'@aws-sdk/s3-request-presigner':
|
'@aws-sdk/s3-request-presigner':
|
||||||
specifier: ^3.1026.0
|
specifier: ^3.1026.0
|
||||||
version: 3.1026.0
|
version: 3.1026.0
|
||||||
'@goodgo/contracts-events':
|
'@bull-board/api':
|
||||||
specifier: workspace:*
|
specifier: ^7.0.0
|
||||||
version: link:../../libs/contracts/events
|
version: 7.0.0(@bull-board/ui@7.0.0)
|
||||||
|
'@bull-board/express':
|
||||||
|
specifier: ^7.0.0
|
||||||
|
version: 7.0.0
|
||||||
|
'@bull-board/nestjs':
|
||||||
|
specifier: ^7.0.0
|
||||||
|
version: 7.0.0(@bull-board/api@7.0.0(@bull-board/ui@7.0.0))(@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@goodgo/mcp-servers':
|
'@goodgo/mcp-servers':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../libs/mcp-servers
|
version: link:../../libs/mcp-servers
|
||||||
@@ -189,9 +195,6 @@ importers:
|
|||||||
ioredis:
|
ioredis:
|
||||||
specifier: ^5.4.0
|
specifier: ^5.4.0
|
||||||
version: 5.10.1
|
version: 5.10.1
|
||||||
jsonwebtoken:
|
|
||||||
specifier: ^9.0.3
|
|
||||||
version: 9.0.3
|
|
||||||
nodemailer:
|
nodemailer:
|
||||||
specifier: ^8.0.5
|
specifier: ^8.0.5
|
||||||
version: 8.0.5
|
version: 8.0.5
|
||||||
@@ -265,9 +268,6 @@ importers:
|
|||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.6
|
version: 5.0.6
|
||||||
'@types/jsonwebtoken':
|
|
||||||
specifier: ^9.0.10
|
|
||||||
version: 9.0.10
|
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.5.2
|
specifier: ^25.5.2
|
||||||
version: 25.5.2
|
version: 25.5.2
|
||||||
@@ -776,6 +776,27 @@ packages:
|
|||||||
resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
|
resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@bull-board/api@7.0.0':
|
||||||
|
resolution: {integrity: sha512-ISNspLHVmUWUSq/eLw+wd1FuBBUnqpLbYP2xUNmehpfKhS+NoZWMbBvqjUYVeE/HLfUkRcR1edzMKpl5n9zlSw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@bull-board/ui': 7.0.0
|
||||||
|
|
||||||
|
'@bull-board/express@7.0.0':
|
||||||
|
resolution: {integrity: sha512-3Tc/EyU5PQMTcTzcafFSrmRDiEbJBEU/EaVQ5OVYcuJ7DZCp5Pkvm0/2VtaCe2uywdtwn0ZaynlSIpB27FKX6A==}
|
||||||
|
|
||||||
|
'@bull-board/nestjs@7.0.0':
|
||||||
|
resolution: {integrity: sha512-ypXm0eJHIMQzjN+3fjf84cVxugBg/K4Bpo0eYcV4u/AsteR/dnr6e7F79ICRgg1WWoczqmSMl0JhlmykpyhAMg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@bull-board/api': ^7.0.0
|
||||||
|
'@nestjs/bull-shared': ^10.0.0 || ^11.0.0
|
||||||
|
'@nestjs/common': ^9.0.0 || ^10.0.0 || ^11.0.0
|
||||||
|
'@nestjs/core': ^9.0.0 || ^10.0.0 || ^11.0.0
|
||||||
|
reflect-metadata: ^0.1.13 || ^0.2.0
|
||||||
|
rxjs: ^7.8.1
|
||||||
|
|
||||||
|
'@bull-board/ui@7.0.0':
|
||||||
|
resolution: {integrity: sha512-AnKeklpDn0iMFgu4ukDU6uTNmw4oudl07G4k2Fh95SknKDrXSiWRV0N1TGUawMqyfG1Yi5P/W/8d7raBq/Uw6w==}
|
||||||
|
|
||||||
'@colors/colors@1.5.0':
|
'@colors/colors@1.5.0':
|
||||||
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
|
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
|
||||||
engines: {node: '>=0.1.90'}
|
engines: {node: '>=0.1.90'}
|
||||||
@@ -4418,6 +4439,11 @@ packages:
|
|||||||
effect@3.20.0:
|
effect@3.20.0:
|
||||||
resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==}
|
resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==}
|
||||||
|
|
||||||
|
ejs@5.0.2:
|
||||||
|
resolution: {integrity: sha512-IpbUaI/CAW86l3f+T8zN0iggSc0LmMZLcIW5eRVStLVNCoTXkE0YlncbbH50fp8Cl6zHIky0sW2uUbhBqGw0Jw==}
|
||||||
|
engines: {node: '>=0.12.18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
electron-to-chromium@1.5.332:
|
electron-to-chromium@1.5.332:
|
||||||
resolution: {integrity: sha512-7OOtytmh/rINMLwaFTbcMVvYXO3AUm029X0LcyfYk0B557RlPkdpTpnH9+htMlfu5dKwOmT0+Zs2Aw+lnn6TeQ==}
|
resolution: {integrity: sha512-7OOtytmh/rINMLwaFTbcMVvYXO3AUm029X0LcyfYk0B557RlPkdpTpnH9+htMlfu5dKwOmT0+Zs2Aw+lnn6TeQ==}
|
||||||
|
|
||||||
@@ -6313,6 +6339,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
redis-info@3.1.0:
|
||||||
|
resolution: {integrity: sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==}
|
||||||
|
|
||||||
redis-parser@3.0.0:
|
redis-parser@3.0.0:
|
||||||
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
|
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -8025,6 +8054,33 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
css-tree: 3.2.1
|
css-tree: 3.2.1
|
||||||
|
|
||||||
|
'@bull-board/api@7.0.0(@bull-board/ui@7.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@bull-board/ui': 7.0.0
|
||||||
|
redis-info: 3.1.0
|
||||||
|
|
||||||
|
'@bull-board/express@7.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@bull-board/api': 7.0.0(@bull-board/ui@7.0.0)
|
||||||
|
'@bull-board/ui': 7.0.0
|
||||||
|
ejs: 5.0.2
|
||||||
|
express: 5.2.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@bull-board/nestjs@7.0.0(@bull-board/api@7.0.0(@bull-board/ui@7.0.0))(@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
|
||||||
|
dependencies:
|
||||||
|
'@bull-board/api': 7.0.0(@bull-board/ui@7.0.0)
|
||||||
|
'@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)
|
||||||
|
'@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
reflect-metadata: 0.2.2
|
||||||
|
rxjs: 7.8.2
|
||||||
|
|
||||||
|
'@bull-board/ui@7.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@bull-board/api': 7.0.0(@bull-board/ui@7.0.0)
|
||||||
|
|
||||||
'@colors/colors@1.5.0':
|
'@colors/colors@1.5.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -11786,6 +11842,8 @@ snapshots:
|
|||||||
'@standard-schema/spec': 1.1.0
|
'@standard-schema/spec': 1.1.0
|
||||||
fast-check: 3.23.2
|
fast-check: 3.23.2
|
||||||
|
|
||||||
|
ejs@5.0.2: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.332: {}
|
electron-to-chromium@1.5.332: {}
|
||||||
|
|
||||||
emoji-regex@10.6.0: {}
|
emoji-regex@10.6.0: {}
|
||||||
@@ -13905,6 +13963,10 @@ snapshots:
|
|||||||
|
|
||||||
redis-errors@1.2.0: {}
|
redis-errors@1.2.0: {}
|
||||||
|
|
||||||
|
redis-info@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
lodash: 4.18.1
|
||||||
|
|
||||||
redis-parser@3.0.0:
|
redis-parser@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
redis-errors: 1.2.0
|
redis-errors: 1.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user