zephyrfs/zephyrfs-web / ac9c140

Browse files

desktop integration first pass

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
ac9c14002cf6cb139d8e4933852847deb0b88f4b
Parents
095d099
Tree
d620ca4

2 changed files

StatusFile+-
M server/src/database/schema.sql 32 0
A server/src/routes/desktop-integration.ts 434 0
server/src/database/schema.sqlmodified
@@ -88,6 +88,34 @@ CREATE TABLE user_onboarding (
8888
     FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
8989
 );
9090
 
91
+-- Desktop application sessions for seamless integration
92
+CREATE TABLE desktop_sessions (
93
+    id TEXT PRIMARY KEY,
94
+    desktop_token TEXT UNIQUE NOT NULL,
95
+    user_id TEXT,
96
+    user_type TEXT CHECK (user_type IN ('backup', 'volunteer')),
97
+    machine_id TEXT NOT NULL,
98
+    app_version TEXT NOT NULL,
99
+    os TEXT NOT NULL,
100
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
101
+    expires_at DATETIME NOT NULL,
102
+    storage_config TEXT, -- JSON storage for storage configuration
103
+    last_activity DATETIME DEFAULT CURRENT_TIMESTAMP,
104
+    FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL
105
+);
106
+
107
+-- User preferences for configuration synchronization
108
+CREATE TABLE user_preferences (
109
+    id TEXT PRIMARY KEY,
110
+    user_id TEXT NOT NULL,
111
+    key TEXT NOT NULL,
112
+    value TEXT, -- JSON storage for preference values
113
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
114
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
115
+    FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
116
+    UNIQUE(user_id, key)
117
+);
118
+
91119
 -- Indexes for performance
92120
 CREATE INDEX idx_users_email ON users (email);
93121
 CREATE INDEX idx_users_username ON users (username);
@@ -96,6 +124,10 @@ CREATE INDEX idx_sessions_token ON user_sessions (session_token);
96124
 CREATE INDEX idx_sessions_user_id ON user_sessions (user_id);
97125
 CREATE INDEX idx_email_verifications_token ON email_verifications (token);
98126
 CREATE INDEX idx_password_resets_token ON password_resets (token);
127
+CREATE INDEX idx_desktop_sessions_token ON desktop_sessions (desktop_token);
128
+CREATE INDEX idx_desktop_sessions_user_id ON desktop_sessions (user_id);
129
+CREATE INDEX idx_desktop_sessions_machine_id ON desktop_sessions (machine_id);
130
+CREATE INDEX idx_user_preferences_user_key ON user_preferences (user_id, key);
99131
 
100132
 -- Insert default admin user
101133
 INSERT INTO users (
server/src/routes/desktop-integration.tsadded
@@ -0,0 +1,434 @@
1
+/**
2
+ * Desktop Integration API routes for ZephyrFS
3
+ *
4
+ * Handles communication between the desktop app and web interface
5
+ * for seamless authentication and configuration synchronization.
6
+ */
7
+
8
+import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
9
+import { z } from 'zod';
10
+import { db } from '../database/db.js';
11
+import jwt from 'jsonwebtoken';
12
+import { v4 as uuidv4 } from 'uuid';
13
+import bcrypt from 'bcrypt';
14
+
15
+// Validation schemas
16
+const DesktopTokenSchema = z.object({
17
+  desktop_token: z.string().uuid(),
18
+  desktop_metadata: z.object({
19
+    app_version: z.string(),
20
+    os: z.string(),
21
+    machine_id: z.string(),
22
+    storage_config: z.object({
23
+      storage_folder: z.string(),
24
+      storage_limit: z.number(),
25
+      current_usage: z.number(),
26
+      node_status: z.enum(['Inactive', 'Starting', 'Running', 'Paused', 'Error']),
27
+    }).optional(),
28
+  }),
29
+});
30
+
31
+const RegistrationFlowSchema = z.object({
32
+  desktop_token: z.string().uuid(),
33
+  preferred_user_type: z.enum(['Backup', 'Volunteer']).optional(),
34
+  return_url: z.string().url(),
35
+  storage_ready: z.boolean(),
36
+  capabilities: z.array(z.string()),
37
+});
38
+
39
+const ConfigSyncRequestSchema = z.object({
40
+  desktop_token: z.string().uuid(),
41
+  desktop_config: z.record(z.any()),
42
+  last_sync: z.number(),
43
+});
44
+
45
+interface DesktopSession {
46
+  id: string;
47
+  desktop_token: string;
48
+  user_id: string | null;
49
+  user_type: 'backup' | 'volunteer' | null;
50
+  machine_id: string;
51
+  app_version: string;
52
+  os: string;
53
+  created_at: Date;
54
+  expires_at: Date;
55
+  storage_config: any | null;
56
+}
57
+
58
+/**
59
+ * Register desktop integration routes
60
+ */
61
+export async function desktopIntegrationRoutes(fastify: FastifyInstance) {
62
+  // Middleware to validate desktop token
63
+  const validateDesktopToken = async (request: FastifyRequest, reply: FastifyReply) => {
64
+    const authHeader = request.headers.authorization;
65
+    if (!authHeader || !authHeader.startsWith('Bearer ')) {
66
+      return reply.code(401).send({ error: 'Desktop token required' });
67
+    }
68
+
69
+    const token = authHeader.substring(7);
70
+    try {
71
+      const session = await getDesktopSession(token);
72
+      if (!session) {
73
+        return reply.code(401).send({ error: 'Invalid desktop token' });
74
+      }
75
+
76
+      if (new Date() > session.expires_at) {
77
+        await deleteDesktopSession(token);
78
+        return reply.code(401).send({ error: 'Desktop token expired' });
79
+      }
80
+
81
+      (request as any).desktopSession = session;
82
+    } catch (error) {
83
+      fastify.log.error('Desktop token validation error:', error);
84
+      return reply.code(500).send({ error: 'Token validation failed' });
85
+    }
86
+  };
87
+
88
+  // Register desktop app and create session
89
+  fastify.post('/api/desktop/register', async (request, reply) => {
90
+    try {
91
+      const validatedData = DesktopTokenSchema.parse(request.body);
92
+
93
+      const sessionId = uuidv4();
94
+      const expiresAt = new Date();
95
+      expiresAt.setDate(expiresAt.getDate() + 7); // 7 days
96
+
97
+      const session: DesktopSession = {
98
+        id: sessionId,
99
+        desktop_token: validatedData.desktop_token,
100
+        user_id: null,
101
+        user_type: null,
102
+        machine_id: validatedData.desktop_metadata.machine_id,
103
+        app_version: validatedData.desktop_metadata.app_version,
104
+        os: validatedData.desktop_metadata.os,
105
+        created_at: new Date(),
106
+        expires_at: expiresAt,
107
+        storage_config: validatedData.desktop_metadata.storage_config || null,
108
+      };
109
+
110
+      await storeDesktopSession(session);
111
+
112
+      return reply.send({
113
+        success: true,
114
+        session_id: sessionId,
115
+        expires_at: expiresAt.toISOString(),
116
+        message: 'Desktop session created successfully',
117
+      });
118
+    } catch (error) {
119
+      fastify.log.error('Desktop registration error:', error);
120
+      return reply.code(400).send({ error: 'Invalid registration data' });
121
+    }
122
+  });
123
+
124
+  // Handle registration flow from desktop app
125
+  fastify.post('/api/desktop/registration-flow', async (request, reply) => {
126
+    try {
127
+      const validatedData = RegistrationFlowSchema.parse(request.body);
128
+
129
+      // Store registration flow data for web interface
130
+      await storeRegistrationFlow(validatedData);
131
+
132
+      // Generate a temporary registration token
133
+      const registrationToken = jwt.sign(
134
+        {
135
+          desktop_token: validatedData.desktop_token,
136
+          flow_type: 'registration',
137
+          preferred_user_type: validatedData.preferred_user_type,
138
+        },
139
+        process.env.JWT_SECRET!,
140
+        { expiresIn: '1h' }
141
+      );
142
+
143
+      return reply.send({
144
+        success: true,
145
+        registration_token: registrationToken,
146
+        return_url: validatedData.return_url,
147
+        storage_ready: validatedData.storage_ready,
148
+      });
149
+    } catch (error) {
150
+      fastify.log.error('Registration flow error:', error);
151
+      return reply.code(400).send({ error: 'Invalid registration flow data' });
152
+    }
153
+  });
154
+
155
+  // Get authentication status for desktop app
156
+  fastify.get('/api/desktop/auth-status', {
157
+    preHandler: validateDesktopToken,
158
+  }, async (request, reply) => {
159
+    const session = (request as any).desktopSession as DesktopSession;
160
+
161
+    if (session.user_id) {
162
+      // Get user details
163
+      const user = await db
164
+        .selectFrom('users')
165
+        .select(['id', 'email', 'display_name', 'user_type'])
166
+        .where('id', '=', session.user_id)
167
+        .executeTakeFirst();
168
+
169
+      if (user) {
170
+        return reply.send({
171
+          authenticated: true,
172
+          user_id: user.id,
173
+          user_type: user.user_type === 'backup' ? 'Backup' : 'Volunteer',
174
+          display_name: user.display_name || user.email,
175
+          email: user.email,
176
+        });
177
+      }
178
+    }
179
+
180
+    return reply.send({
181
+      authenticated: false,
182
+      user_id: null,
183
+      user_type: null,
184
+      display_name: null,
185
+      email: null,
186
+    });
187
+  });
188
+
189
+  // Link web authentication to desktop session
190
+  fastify.post('/api/desktop/link-auth', async (request, reply) => {
191
+    try {
192
+      const { desktop_token, web_session_token } = request.body as {
193
+        desktop_token: string;
194
+        web_session_token: string;
195
+      };
196
+
197
+      // Verify web session token
198
+      const webSession = jwt.verify(web_session_token, process.env.JWT_SECRET!) as any;
199
+
200
+      if (!webSession.userId) {
201
+        return reply.code(400).send({ error: 'Invalid web session' });
202
+      }
203
+
204
+      // Update desktop session with user authentication
205
+      const updated = await linkDesktopSessionToUser(
206
+        desktop_token,
207
+        webSession.userId,
208
+        webSession.userType
209
+      );
210
+
211
+      if (!updated) {
212
+        return reply.code(404).send({ error: 'Desktop session not found' });
213
+      }
214
+
215
+      return reply.send({
216
+        success: true,
217
+        message: 'Desktop session linked to user account',
218
+        user_id: webSession.userId,
219
+        user_type: webSession.userType,
220
+      });
221
+    } catch (error) {
222
+      fastify.log.error('Auth linking error:', error);
223
+      return reply.code(500).send({ error: 'Failed to link authentication' });
224
+    }
225
+  });
226
+
227
+  // Sync configuration between desktop and web
228
+  fastify.post('/api/desktop/sync', {
229
+    preHandler: validateDesktopToken,
230
+  }, async (request, reply) => {
231
+    try {
232
+      const validatedData = ConfigSyncRequestSchema.parse(request.body);
233
+      const session = (request as any).desktopSession as DesktopSession;
234
+
235
+      // Update desktop configuration in session
236
+      await updateDesktopConfig(session.desktop_token, validatedData.desktop_config);
237
+
238
+      // Get any configuration updates from web interface
239
+      const webConfig = await getWebConfigUpdates(session.user_id, validatedData.last_sync);
240
+
241
+      return reply.send({
242
+        success: true,
243
+        changes_applied: Object.keys(webConfig).length,
244
+        web_config: webConfig,
245
+        next_sync: Math.floor(Date.now() / 1000) + 300, // 5 minutes from now
246
+      });
247
+    } catch (error) {
248
+      fastify.log.error('Config sync error:', error);
249
+      return reply.code(500).send({ error: 'Configuration sync failed' });
250
+    }
251
+  });
252
+
253
+  // Handle desktop app logout
254
+  fastify.post('/api/desktop/logout', {
255
+    preHandler: validateDesktopToken,
256
+  }, async (request, reply) => {
257
+    const session = (request as any).desktopSession as DesktopSession;
258
+
259
+    // Remove user association but keep desktop session for re-auth
260
+    await unlinkDesktopSessionFromUser(session.desktop_token);
261
+
262
+    return reply.send({
263
+      success: true,
264
+      message: 'Desktop session unlinked from user account',
265
+    });
266
+  });
267
+
268
+  // Get desktop sessions for debugging (admin only)
269
+  fastify.get('/api/admin/desktop-sessions', {
270
+    preHandler: async (request, reply) => {
271
+      // Add admin authentication check here
272
+      // For now, just a simple check
273
+      const adminToken = request.headers['x-admin-token'];
274
+      if (adminToken !== process.env.ADMIN_TOKEN) {
275
+        return reply.code(403).send({ error: 'Admin access required' });
276
+      }
277
+    },
278
+  }, async (request, reply) => {
279
+    const sessions = await getAllDesktopSessions();
280
+    return reply.send({ sessions });
281
+  });
282
+}
283
+
284
+// Database operations for desktop sessions
285
+async function storeDesktopSession(session: DesktopSession): Promise<void> {
286
+  await db
287
+    .insertInto('desktop_sessions')
288
+    .values({
289
+      id: session.id,
290
+      desktop_token: session.desktop_token,
291
+      user_id: session.user_id,
292
+      user_type: session.user_type,
293
+      machine_id: session.machine_id,
294
+      app_version: session.app_version,
295
+      os: session.os,
296
+      created_at: session.created_at,
297
+      expires_at: session.expires_at,
298
+      storage_config: JSON.stringify(session.storage_config),
299
+    })
300
+    .execute();
301
+}
302
+
303
+async function getDesktopSession(token: string): Promise<DesktopSession | null> {
304
+  const result = await db
305
+    .selectFrom('desktop_sessions')
306
+    .selectAll()
307
+    .where('desktop_token', '=', token)
308
+    .executeTakeFirst();
309
+
310
+  if (!result) return null;
311
+
312
+  return {
313
+    id: result.id,
314
+    desktop_token: result.desktop_token,
315
+    user_id: result.user_id,
316
+    user_type: result.user_type as 'backup' | 'volunteer' | null,
317
+    machine_id: result.machine_id,
318
+    app_version: result.app_version,
319
+    os: result.os,
320
+    created_at: result.created_at,
321
+    expires_at: result.expires_at,
322
+    storage_config: result.storage_config ? JSON.parse(result.storage_config) : null,
323
+  };
324
+}
325
+
326
+async function linkDesktopSessionToUser(
327
+  desktopToken: string,
328
+  userId: string,
329
+  userType: string
330
+): Promise<boolean> {
331
+  const result = await db
332
+    .updateTable('desktop_sessions')
333
+    .set({
334
+      user_id: userId,
335
+      user_type: userType === 'backup' ? 'backup' : 'volunteer',
336
+    })
337
+    .where('desktop_token', '=', desktopToken)
338
+    .execute();
339
+
340
+  return result.length > 0;
341
+}
342
+
343
+async function unlinkDesktopSessionFromUser(desktopToken: string): Promise<boolean> {
344
+  const result = await db
345
+    .updateTable('desktop_sessions')
346
+    .set({
347
+      user_id: null,
348
+      user_type: null,
349
+    })
350
+    .where('desktop_token', '=', desktopToken)
351
+    .execute();
352
+
353
+  return result.length > 0;
354
+}
355
+
356
+async function updateDesktopConfig(
357
+  desktopToken: string,
358
+  config: Record<string, any>
359
+): Promise<void> {
360
+  await db
361
+    .updateTable('desktop_sessions')
362
+    .set({
363
+      storage_config: JSON.stringify(config),
364
+    })
365
+    .where('desktop_token', '=', desktopToken)
366
+    .execute();
367
+}
368
+
369
+async function getWebConfigUpdates(
370
+  userId: string | null,
371
+  lastSync: number
372
+): Promise<Record<string, any>> {
373
+  if (!userId) return {};
374
+
375
+  // Get any configuration changes from web interface since last sync
376
+  // This would query user preferences, storage settings, etc.
377
+  const webConfig: Record<string, any> = {};
378
+
379
+  // Example: Get user preferences that changed since last sync
380
+  const preferences = await db
381
+    .selectFrom('user_preferences')
382
+    .selectAll()
383
+    .where('user_id', '=', userId)
384
+    .where('updated_at', '>', new Date(lastSync * 1000))
385
+    .execute();
386
+
387
+  for (const pref of preferences) {
388
+    webConfig[pref.key] = pref.value;
389
+  }
390
+
391
+  return webConfig;
392
+}
393
+
394
+async function deleteDesktopSession(token: string): Promise<void> {
395
+  await db
396
+    .deleteFrom('desktop_sessions')
397
+    .where('desktop_token', '=', token)
398
+    .execute();
399
+}
400
+
401
+async function getAllDesktopSessions(): Promise<DesktopSession[]> {
402
+  const results = await db
403
+    .selectFrom('desktop_sessions')
404
+    .selectAll()
405
+    .execute();
406
+
407
+  return results.map(result => ({
408
+    id: result.id,
409
+    desktop_token: result.desktop_token,
410
+    user_id: result.user_id,
411
+    user_type: result.user_type as 'backup' | 'volunteer' | null,
412
+    machine_id: result.machine_id,
413
+    app_version: result.app_version,
414
+    os: result.os,
415
+    created_at: result.created_at,
416
+    expires_at: result.expires_at,
417
+    storage_config: result.storage_config ? JSON.parse(result.storage_config) : null,
418
+  }));
419
+}
420
+
421
+async function storeRegistrationFlow(flow: z.infer<typeof RegistrationFlowSchema>): Promise<void> {
422
+  // Store registration flow data temporarily for web interface
423
+  // This could use Redis or a temporary database table
424
+  // For now, just log it
425
+  console.log('Registration flow stored:', flow);
426
+}
427
+
428
+// Cleanup expired desktop sessions (should be called periodically)
429
+export async function cleanupExpiredDesktopSessions(): Promise<void> {
430
+  await db
431
+    .deleteFrom('desktop_sessions')
432
+    .where('expires_at', '<', new Date())
433
+    .execute();
434
+}