TypeScript · 13209 bytes Raw Blame History
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 }