Skip to content

Commit 0aa6a93

Browse files
author
Lasim
committed
feat: Add configurable version display in root API response based on global setting
1 parent 80fdfdc commit 0aa6a93

File tree

5 files changed

+127
-13
lines changed

5 files changed

+127
-13
lines changed

services/backend/api-spec.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,13 @@
7373
},
7474
"version": {
7575
"type": "string",
76-
"description": "API version"
76+
"description": "API version (configurable via global.show_version setting)"
7777
}
7878
},
7979
"required": [
8080
"message",
8181
"status",
82-
"timestamp",
83-
"version"
82+
"timestamp"
8483
],
8584
"additionalProperties": false,
8685
"description": "API health check information"

services/backend/api-spec.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,11 @@ paths:
5454
description: Current server timestamp
5555
version:
5656
type: string
57-
description: API version
57+
description: API version (configurable via global.show_version setting)
5858
required:
5959
- message
6060
- status
6161
- timestamp
62-
- version
6362
additionalProperties: false
6463
description: API health check information
6564
/api/health:

services/backend/src/global-settings/global.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ export const globalSettings: GlobalSettingsModule = {
4848
description: 'Enable or disable Swagger API documentation endpoint (/documentation)',
4949
encrypted: false,
5050
required: false
51+
},
52+
{
53+
key: 'global.show_version',
54+
defaultValue: true,
55+
type: 'boolean',
56+
description: 'Show backend version in the root API response. When disabled, version information is hidden from visitors.',
57+
encrypted: false,
58+
required: false
5159
}
5260
]
5361
};

services/backend/src/routes/index.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type FastifyInstance } from 'fastify'
22
import { z } from 'zod'
33
import { zodToJsonSchema } from 'zod-to-json-schema'
44
import { getVersionString } from '../config/version'
5+
import { GlobalSettings } from '../global-settings/helpers'
56
// Import the individual database setup routes
67
import dbStatusRoute from './db/status'
78
import dbSetupRoute from './db/setup'
@@ -22,7 +23,7 @@ const healthCheckResponseSchema = z.object({
2223
message: z.string().describe('Service status message'),
2324
status: z.string().describe('Database connection status'),
2425
timestamp: z.string().describe('Current server timestamp'),
25-
version: z.string().describe('API version')
26+
version: z.string().optional().describe('API version (configurable via global.show_version setting)')
2627
});
2728

2829
export const registerRoutes = (server: FastifyInstance): void => {
@@ -62,13 +63,38 @@ export const registerRoutes = (server: FastifyInstance): void => {
6263
})
6364
}
6465
}
65-
}, async () => {
66-
// Ensure message points to the correct non-versioned API paths
67-
return {
66+
}, async (request) => {
67+
// Check if version should be shown based on global setting
68+
const showVersion = await GlobalSettings.getBoolean('global.show_version', true);
69+
70+
request.log.debug({
71+
operation: 'root_endpoint_version_check',
72+
showVersion,
73+
setting: 'global.show_version'
74+
}, 'Checking version display setting');
75+
76+
// Build base response
77+
const response: Record<string, any> = {
6878
message: 'DeployStack Backend is running.',
6979
status: server.db ? 'Database Connected' : 'Database Not Configured/Connected - Use /api/db/status and /api/db/setup',
70-
timestamp: new Date().toISOString(),
71-
version: getVersionString()
80+
timestamp: new Date().toISOString()
81+
};
82+
83+
// Conditionally include version based on global setting
84+
if (showVersion) {
85+
response.version = getVersionString();
86+
request.log.debug({
87+
operation: 'root_endpoint_response',
88+
includeVersion: true,
89+
version: response.version
90+
}, 'Including version in root endpoint response');
91+
} else {
92+
request.log.debug({
93+
operation: 'root_endpoint_response',
94+
includeVersion: false
95+
}, 'Version hidden from root endpoint response per global setting');
7296
}
97+
98+
return response;
7399
})
74100
}

services/backend/tests/unit/routes/index.test.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ vi.mock('../../../src/routes/teams');
1212
vi.mock('../../../src/routes/cloud-credentials');
1313
vi.mock('../../../src/routes/health');
1414

15+
// Mock the GlobalSettings helper
16+
vi.mock('../../../src/global-settings/helpers', () => ({
17+
GlobalSettings: {
18+
getBoolean: vi.fn()
19+
}
20+
}));
21+
1522
// Import mocked modules
1623
import dbStatusRoute from '../../../src/routes/db/status';
1724
import dbSetupRoute from '../../../src/routes/db/setup';
@@ -21,6 +28,7 @@ import globalSettingsRoute from '../../../src/routes/globalSettings';
2128
import teamsRoute from '../../../src/routes/teams';
2229
import cloudCredentialsRoute from '../../../src/routes/cloud-credentials';
2330
import healthRoute from '../../../src/routes/health';
31+
import { GlobalSettings } from '../../../src/global-settings/helpers';
2432

2533
// Type the mocked functions
2634
const mockDbStatusRoute = dbStatusRoute as MockedFunction<typeof dbStatusRoute>;
@@ -125,12 +133,27 @@ describe('Main Routes Registration', () => {
125133
let mockReply: Partial<FastifyReply>;
126134

127135
beforeEach(async () => {
128-
mockRequest = {};
136+
mockRequest = {
137+
log: {
138+
debug: vi.fn(),
139+
info: vi.fn(),
140+
warn: vi.fn(),
141+
error: vi.fn(),
142+
fatal: vi.fn(),
143+
trace: vi.fn(),
144+
silent: vi.fn(),
145+
child: vi.fn(),
146+
level: 'info'
147+
}
148+
} as any;
129149
mockReply = {
130150
status: vi.fn().mockReturnThis(),
131151
send: vi.fn().mockReturnThis(),
132152
};
133153

154+
// Reset GlobalSettings mock
155+
vi.mocked(GlobalSettings.getBoolean).mockResolvedValue(true);
156+
134157
await registerRoutes(mockFastify as FastifyInstance);
135158
});
136159

@@ -149,6 +172,9 @@ describe('Main Routes Registration', () => {
149172

150173
// Verify timestamp is a valid ISO string
151174
expect(new Date(result.timestamp).toISOString()).toBe(result.timestamp);
175+
176+
// Verify GlobalSettings was called
177+
expect(GlobalSettings.getBoolean).toHaveBeenCalledWith('global.show_version', true);
152178
});
153179

154180
it('should return health check with database connected', async () => {
@@ -166,6 +192,9 @@ describe('Main Routes Registration', () => {
166192

167193
// Verify timestamp is a valid ISO string
168194
expect(new Date(result.timestamp).toISOString()).toBe(result.timestamp);
195+
196+
// Verify GlobalSettings was called
197+
expect(GlobalSettings.getBoolean).toHaveBeenCalledWith('global.show_version', true);
169198
});
170199

171200
it('should return consistent timestamp format', async () => {
@@ -176,11 +205,25 @@ describe('Main Routes Registration', () => {
176205
expect(result.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
177206
});
178207

179-
it('should return correct version', async () => {
208+
it('should return correct version when show_version is true', async () => {
209+
vi.mocked(GlobalSettings.getBoolean).mockResolvedValue(true);
210+
180211
const handler = routeHandlers['GET /'];
181212
const result = await handler(mockRequest, mockReply);
182213

183214
expect(result.version).toBe('0.20.9');
215+
expect(GlobalSettings.getBoolean).toHaveBeenCalledWith('global.show_version', true);
216+
});
217+
218+
it('should not return version when show_version is false', async () => {
219+
vi.mocked(GlobalSettings.getBoolean).mockResolvedValue(false);
220+
221+
const handler = routeHandlers['GET /'];
222+
const result = await handler(mockRequest, mockReply);
223+
224+
expect(result.version).toBeUndefined();
225+
expect(result).not.toHaveProperty('version');
226+
expect(GlobalSettings.getBoolean).toHaveBeenCalledWith('global.show_version', true);
184227
});
185228

186229
it('should handle undefined database gracefully', async () => {
@@ -190,6 +233,7 @@ describe('Main Routes Registration', () => {
190233
const result = await handler(mockRequest, mockReply);
191234

192235
expect(result.status).toBe('Database Not Configured/Connected - Use /api/db/status and /api/db/setup');
236+
expect(GlobalSettings.getBoolean).toHaveBeenCalledWith('global.show_version', true);
193237
});
194238

195239
it('should handle falsy database values', async () => {
@@ -199,6 +243,44 @@ describe('Main Routes Registration', () => {
199243
const result = await handler(mockRequest, mockReply);
200244

201245
expect(result.status).toBe('Database Not Configured/Connected - Use /api/db/status and /api/db/setup');
246+
expect(GlobalSettings.getBoolean).toHaveBeenCalledWith('global.show_version', true);
247+
});
248+
249+
it('should log debug information about version display', async () => {
250+
vi.mocked(GlobalSettings.getBoolean).mockResolvedValue(false);
251+
252+
const handler = routeHandlers['GET /'];
253+
await handler(mockRequest, mockReply);
254+
255+
expect(mockRequest.log?.debug).toHaveBeenCalledWith({
256+
operation: 'root_endpoint_version_check',
257+
showVersion: false,
258+
setting: 'global.show_version'
259+
}, 'Checking version display setting');
260+
261+
expect(mockRequest.log?.debug).toHaveBeenCalledWith({
262+
operation: 'root_endpoint_response',
263+
includeVersion: false
264+
}, 'Version hidden from root endpoint response per global setting');
265+
});
266+
267+
it('should log when version is included', async () => {
268+
vi.mocked(GlobalSettings.getBoolean).mockResolvedValue(true);
269+
270+
const handler = routeHandlers['GET /'];
271+
const result = await handler(mockRequest, mockReply);
272+
273+
expect(mockRequest.log?.debug).toHaveBeenCalledWith({
274+
operation: 'root_endpoint_version_check',
275+
showVersion: true,
276+
setting: 'global.show_version'
277+
}, 'Checking version display setting');
278+
279+
expect(mockRequest.log?.debug).toHaveBeenCalledWith({
280+
operation: 'root_endpoint_response',
281+
includeVersion: true,
282+
version: result.version
283+
}, 'Including version in root endpoint response');
202284
});
203285
});
204286

0 commit comments

Comments
 (0)