File size: 3,884 Bytes
46252cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import { Controller, Get, Put, NotImplementedException } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config';
import { RequireRole } from '../auth/decorators/auth.decorators';
import { ApiKeyRole } from '../auth/entities/api-key.entity';
import { isSwaggerEnabled } from '../../config/bootstrap-security';

interface Settings {
  general: {
    apiBaseUrl: string;
    autoReconnect: boolean;
    debugMode: boolean;
  };
  api: {
    rateLimit: number;
    rateLimitWindow: number;
    enableDocs: boolean;
  };
  notifications: {
    emailEnabled: boolean;
    notificationEmail: string;
    webhookAlerts: boolean;
  };
}

@ApiTags('settings')
@Controller('settings')
export class SettingsController {
  private settings: Settings;

  constructor(private readonly configService: ConfigService) {
    // Initialize with values from configuration (reads from .env)
    const port = this.configService.get<number>('port', 2785);

    this.settings = {
      general: {
        // The real advertised base URL (BASE_URL — the same value the startup banner and ingress URLs
        // use), not a hardcoded localhost guess that ignores the operator's configured host.
        apiBaseUrl: process.env.BASE_URL || `http://localhost:${port}`,
        // The engine auto-reconnects on a transient disconnect by default (there is no global off
        // switch; reconnection is bounded per-session by RECONNECT_MAX_ATTEMPTS). Reporting a hardcoded
        // `false` for a non-existent `engine.autoReconnect` key was actively misleading.
        autoReconnect: true,
        debugMode: this.configService.get<boolean>('database.logging', false),
      },
      api: {
        rateLimit: this.configService.get<number>('api.rateLimit.mediumLimit', 100),
        rateLimitWindow: this.configService.get<number>('api.rateLimit.mediumTtl', 60000),
        // Reflect the REAL ENABLE_SWAGGER gate (off by default in production), not a hardcoded `true`
        // — otherwise the panel reports docs enabled in production where they are actually disabled.
        enableDocs: isSwaggerEnabled(process.env.ENABLE_SWAGGER, process.env.NODE_ENV),
      },
      notifications: {
        emailEnabled: false,
        notificationEmail: '',
        webhookAlerts: true,
      },
    };
  }

  @Get()
  @RequireRole(ApiKeyRole.ADMIN)
  @ApiOperation({ summary: 'Get application settings' })
  @ApiResponse({ status: 200, description: 'Current settings' })
  get(): Settings {
    // Settings expose environment-derived configuration (debug flag, reconnect policy, rate-limit
    // thresholds, base URL). Gate the read at ADMIN, matching the PUT below and the rest of the
    // admin-config surface — a VIEWER or session-scoped key has no business reading server config.
    return this.settings;
  }

  @Put()
  @RequireRole(ApiKeyRole.ADMIN)
  @ApiOperation({ summary: 'Settings are read-only at runtime (environment-derived)' })
  @ApiResponse({
    status: 501,
    description: 'Settings are derived from environment configuration and cannot be changed at runtime',
  })
  update(): never {
    // Every Settings field is derived from environment variables and consumed at boot /
    // decorator-evaluation time (ThrottlerModule.forRootAsync, port, webhook timeout, DB logging),
    // and ConfigService is immutable at runtime — so a runtime write cannot actually take effect.
    // The previous handler mutated an in-memory copy and returned 200 'updated' while persisting
    // nothing and applying nothing: a false success. Be honest instead of pretending it worked.
    throw new NotImplementedException(
      'Settings are derived from environment configuration and are read-only at runtime. ' +
        'Change the corresponding environment variable and restart the service.',
    );
  }
}