1
+ const fs = require ( 'fs' ) . promises ;
2
+ const path = require ( 'path' ) ;
3
+ const { loadConfiguration } = require ( './configLoader' ) ;
4
+ const { initializeApiClients } = require ( './apiClients' ) ;
5
+
6
+ class ConfigApi {
7
+ constructor ( ) {
8
+ this . envPath = path . join ( __dirname , '../.env' ) ;
9
+ }
10
+
11
+ /**
12
+ * Get current configuration (without secrets)
13
+ */
14
+ async getConfig ( ) {
15
+ try {
16
+ const config = await this . readEnvFile ( ) ;
17
+
18
+ return {
19
+ proxmox : config . PROXMOX_HOST ? {
20
+ host : config . PROXMOX_HOST ,
21
+ port : config . PROXMOX_PORT || '8006' ,
22
+ tokenId : config . PROXMOX_TOKEN_ID ,
23
+ // Don't send the secret
24
+ } : null ,
25
+ pbs : config . PBS_HOST ? {
26
+ host : config . PBS_HOST ,
27
+ port : config . PBS_PORT || '8007' ,
28
+ tokenId : config . PBS_TOKEN_ID ,
29
+ // Don't send the secret
30
+ } : null
31
+ } ;
32
+ } catch ( error ) {
33
+ console . error ( 'Error reading configuration:' , error ) ;
34
+ return { proxmox : null , pbs : null } ;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Save configuration to .env file
40
+ */
41
+ async saveConfig ( config ) {
42
+ try {
43
+ // Read existing .env file to preserve other settings
44
+ const existingConfig = await this . readEnvFile ( ) ;
45
+
46
+ // Update with new values
47
+ if ( config . proxmox ) {
48
+ existingConfig . PROXMOX_HOST = config . proxmox . host ;
49
+ existingConfig . PROXMOX_PORT = config . proxmox . port || '8006' ;
50
+ existingConfig . PROXMOX_TOKEN_ID = config . proxmox . tokenId ;
51
+ existingConfig . PROXMOX_TOKEN_SECRET = config . proxmox . tokenSecret ;
52
+ }
53
+
54
+ if ( config . pbs ) {
55
+ existingConfig . PBS_HOST = config . pbs . host ;
56
+ existingConfig . PBS_PORT = config . pbs . port || '8007' ;
57
+ existingConfig . PBS_TOKEN_ID = config . pbs . tokenId ;
58
+ existingConfig . PBS_TOKEN_SECRET = config . pbs . tokenSecret ;
59
+ }
60
+
61
+ // Write back to .env file
62
+ await this . writeEnvFile ( existingConfig ) ;
63
+
64
+ // Reload configuration in the application
65
+ await this . reloadConfiguration ( ) ;
66
+
67
+ return { success : true } ;
68
+ } catch ( error ) {
69
+ console . error ( 'Error saving configuration:' , error ) ;
70
+ throw error ;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Test configuration by attempting to connect
76
+ */
77
+ async testConfig ( config ) {
78
+ try {
79
+ // Create temporary endpoint configuration
80
+ const testEndpoints = [ {
81
+ id : 'test-primary' ,
82
+ name : 'Test Primary' ,
83
+ host : config . proxmox . host ,
84
+ port : parseInt ( config . proxmox . port ) || 8006 ,
85
+ tokenId : config . proxmox . tokenId ,
86
+ tokenSecret : config . proxmox . tokenSecret ,
87
+ enabled : true
88
+ } ] ;
89
+
90
+ const testPbsConfigs = config . pbs ? [ {
91
+ id : 'test-pbs' ,
92
+ name : 'Test PBS' ,
93
+ host : config . pbs . host ,
94
+ port : parseInt ( config . pbs . port ) || 8007 ,
95
+ tokenId : config . pbs . tokenId ,
96
+ tokenSecret : config . pbs . tokenSecret
97
+ } ] : [ ] ;
98
+
99
+ // Try to initialize API clients with test config
100
+ const { apiClients, pbsApiClients } = await initializeApiClients ( testEndpoints , testPbsConfigs ) ;
101
+
102
+ // Try a simple API call to verify connection
103
+ const testClient = apiClients . get ( 'test-primary' ) ;
104
+ if ( testClient ) {
105
+ await testClient . client . get ( '/nodes' ) ;
106
+ }
107
+
108
+ return { success : true } ;
109
+ } catch ( error ) {
110
+ console . error ( 'Configuration test failed:' , error ) ;
111
+ return {
112
+ success : false ,
113
+ error : error . message || 'Failed to connect to Proxmox server'
114
+ } ;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Read .env file and parse it
120
+ */
121
+ async readEnvFile ( ) {
122
+ try {
123
+ const content = await fs . readFile ( this . envPath , 'utf8' ) ;
124
+ const config = { } ;
125
+
126
+ content . split ( '\n' ) . forEach ( line => {
127
+ const trimmedLine = line . trim ( ) ;
128
+ if ( trimmedLine && ! trimmedLine . startsWith ( '#' ) ) {
129
+ const [ key , ...valueParts ] = trimmedLine . split ( '=' ) ;
130
+ if ( key ) {
131
+ // Handle values that might contain = signs
132
+ let value = valueParts . join ( '=' ) . trim ( ) ;
133
+ // Remove quotes if present
134
+ if ( ( value . startsWith ( '"' ) && value . endsWith ( '"' ) ) ||
135
+ ( value . startsWith ( "'" ) && value . endsWith ( "'" ) ) ) {
136
+ value = value . slice ( 1 , - 1 ) ;
137
+ }
138
+ config [ key . trim ( ) ] = value ;
139
+ }
140
+ }
141
+ } ) ;
142
+
143
+ return config ;
144
+ } catch ( error ) {
145
+ if ( error . code === 'ENOENT' ) {
146
+ // .env file doesn't exist yet
147
+ return { } ;
148
+ }
149
+ throw error ;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Write configuration back to .env file
155
+ */
156
+ async writeEnvFile ( config ) {
157
+ const lines = [ ] ;
158
+
159
+ // Add header
160
+ lines . push ( '# Pulse Configuration' ) ;
161
+ lines . push ( '# Generated by Pulse Web Configuration' ) ;
162
+ lines . push ( '' ) ;
163
+
164
+ // Group related settings
165
+ const groups = {
166
+ 'Proxmox VE Settings' : [ 'PROXMOX_HOST' , 'PROXMOX_PORT' , 'PROXMOX_TOKEN_ID' , 'PROXMOX_TOKEN_SECRET' ] ,
167
+ 'Proxmox Backup Server Settings' : [ 'PBS_HOST' , 'PBS_PORT' , 'PBS_TOKEN_ID' , 'PBS_TOKEN_SECRET' ] ,
168
+ 'Other Settings' : [ ] // Will contain all other keys
169
+ } ;
170
+
171
+ // Find other keys not in predefined groups
172
+ Object . keys ( config ) . forEach ( key => {
173
+ let found = false ;
174
+ Object . values ( groups ) . forEach ( groupKeys => {
175
+ if ( groupKeys . includes ( key ) ) found = true ;
176
+ } ) ;
177
+ if ( ! found && key !== '' ) {
178
+ groups [ 'Other Settings' ] . push ( key ) ;
179
+ }
180
+ } ) ;
181
+
182
+ // Write each group
183
+ Object . entries ( groups ) . forEach ( ( [ groupName , keys ] ) => {
184
+ if ( keys . length > 0 && keys . some ( key => config [ key ] ) ) {
185
+ lines . push ( `# ${ groupName } ` ) ;
186
+ keys . forEach ( key => {
187
+ if ( config [ key ] !== undefined && config [ key ] !== '' ) {
188
+ const value = config [ key ] ;
189
+ // Quote values that contain spaces or special characters
190
+ const needsQuotes = value . includes ( ' ' ) || value . includes ( '#' ) || value . includes ( '=' ) ;
191
+ lines . push ( `${ key } =${ needsQuotes ? `"${ value } "` : value } ` ) ;
192
+ }
193
+ } ) ;
194
+ lines . push ( '' ) ;
195
+ }
196
+ } ) ;
197
+
198
+ await fs . writeFile ( this . envPath , lines . join ( '\n' ) , 'utf8' ) ;
199
+ }
200
+
201
+ /**
202
+ * Reload configuration without restarting the server
203
+ */
204
+ async reloadConfiguration ( ) {
205
+ try {
206
+ // Clear the require cache for dotenv
207
+ delete require . cache [ require . resolve ( 'dotenv' ) ] ;
208
+
209
+ // Reload environment variables
210
+ require ( 'dotenv' ) . config ( ) ;
211
+
212
+ // Reload configuration
213
+ const { endpoints, pbsConfigs, isConfigPlaceholder } = loadConfiguration ( ) ;
214
+
215
+ // Get state manager instance
216
+ const stateManager = require ( './state' ) ;
217
+
218
+ // Update configuration status
219
+ stateManager . setConfigPlaceholderStatus ( isConfigPlaceholder ) ;
220
+ stateManager . setEndpointConfigurations ( endpoints , pbsConfigs ) ;
221
+
222
+ // Reinitialize API clients
223
+ const { apiClients, pbsApiClients } = await initializeApiClients ( endpoints , pbsConfigs ) ;
224
+
225
+ // Update global references
226
+ if ( global . pulseApiClients ) {
227
+ global . pulseApiClients . apiClients = apiClients ;
228
+ global . pulseApiClients . pbsApiClients = pbsApiClients ;
229
+ }
230
+
231
+ // Update global config placeholder status
232
+ if ( global . pulseConfigStatus ) {
233
+ global . pulseConfigStatus . isPlaceholder = isConfigPlaceholder ;
234
+ }
235
+
236
+ console . log ( 'Configuration reloaded successfully' ) ;
237
+ return true ;
238
+ } catch ( error ) {
239
+ console . error ( 'Error reloading configuration:' , error ) ;
240
+ throw error ;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Set up API routes
246
+ */
247
+ setupRoutes ( app ) {
248
+ // Get current configuration
249
+ app . get ( '/api/config' , async ( req , res ) => {
250
+ try {
251
+ const config = await this . getConfig ( ) ;
252
+ res . json ( config ) ;
253
+ } catch ( error ) {
254
+ res . status ( 500 ) . json ( { error : 'Failed to read configuration' } ) ;
255
+ }
256
+ } ) ;
257
+
258
+ // Save configuration
259
+ app . post ( '/api/config' , async ( req , res ) => {
260
+ try {
261
+ await this . saveConfig ( req . body ) ;
262
+ res . json ( { success : true } ) ;
263
+ } catch ( error ) {
264
+ res . status ( 500 ) . json ( {
265
+ success : false ,
266
+ error : error . message || 'Failed to save configuration'
267
+ } ) ;
268
+ }
269
+ } ) ;
270
+
271
+ // Test configuration
272
+ app . post ( '/api/config/test' , async ( req , res ) => {
273
+ try {
274
+ const result = await this . testConfig ( req . body ) ;
275
+ res . json ( result ) ;
276
+ } catch ( error ) {
277
+ res . status ( 500 ) . json ( {
278
+ success : false ,
279
+ error : error . message || 'Failed to test configuration'
280
+ } ) ;
281
+ }
282
+ } ) ;
283
+
284
+ // Reload configuration
285
+ app . post ( '/api/config/reload' , async ( req , res ) => {
286
+ try {
287
+ await this . reloadConfiguration ( ) ;
288
+ res . json ( { success : true } ) ;
289
+ } catch ( error ) {
290
+ res . status ( 500 ) . json ( {
291
+ success : false ,
292
+ error : error . message || 'Failed to reload configuration'
293
+ } ) ;
294
+ }
295
+ } ) ;
296
+ }
297
+ }
298
+
299
+ module . exports = ConfigApi ;
0 commit comments