import { Request, Response } from 'express'; import path from 'path'; import { z } from 'zod'; import nextcloudService from '../services/nextcloud.service'; import userService from '../services/user.service'; import logger from '../utils/logger'; const PollRequestSchema = z.object({ pollEndpoint: z.string().url(), pollToken: z.string().min(1), }); class NextcloudController { async initiateConnect(_req: Request, res: Response): Promise { try { const data = await nextcloudService.initiateLoginFlow(); res.status(200).json({ success: true, data }); } catch (error) { logger.error('initiateConnect error', { error }); res.status(500).json({ success: false, message: 'Nextcloud-Verbindung konnte nicht gestartet werden' }); } } async pollConnect(req: Request, res: Response): Promise { try { const parsed = PollRequestSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parsed.error.flatten().fieldErrors, }); return; } const result = await nextcloudService.pollLoginFlow(parsed.data.pollEndpoint, parsed.data.pollToken); if (!result) { res.status(200).json({ success: true, data: { completed: false } }); return; } await userService.updateNextcloudCredentials(req.user!.id, result.loginName, result.appPassword); res.status(200).json({ success: true, data: { completed: true } }); } catch (error) { logger.error('pollConnect error', { error }); res.status(500).json({ success: false, message: 'Nextcloud-Abfrage fehlgeschlagen' }); } } async getConversations(req: Request, res: Response): Promise { try { const credentials = await userService.getNextcloudCredentials(req.user!.id); if (!credentials) { res.status(200).json({ success: true, data: { connected: false } }); return; } const { totalUnread, conversations } = await nextcloudService.getConversations( credentials.loginName, credentials.appPassword, ); res.status(200).json({ success: true, data: { connected: true, totalUnread, conversations } }); } catch (error: any) { if (error?.code === 'NEXTCLOUD_AUTH_INVALID') { await userService.clearNextcloudCredentials(req.user!.id); res.status(200).json({ success: true, data: { connected: false } }); return; } logger.error('getConversations error', { error }); res.status(500).json({ success: false, message: 'Nextcloud-Gespräche konnten nicht geladen werden' }); } } async disconnect(req: Request, res: Response): Promise { try { await userService.clearNextcloudCredentials(req.user!.id); res.status(200).json({ success: true, data: null }); } catch (error) { logger.error('disconnect error', { error }); res.status(500).json({ success: false, message: 'Nextcloud-Trennung fehlgeschlagen' }); } } async getRooms(req: Request, res: Response): Promise { try { const credentials = await userService.getNextcloudCredentials(req.user!.id); if (!credentials) { res.status(200).json({ success: true, data: { connected: false, rooms: [] } }); return; } const rooms = await nextcloudService.getAllConversations(credentials.loginName, credentials.appPassword); res.status(200).json({ success: true, data: { connected: true, rooms, loginName: credentials.loginName } }); } catch (error: any) { if (error?.code === 'NEXTCLOUD_AUTH_INVALID') { await userService.clearNextcloudCredentials(req.user!.id); res.status(200).json({ success: true, data: { connected: false, rooms: [] } }); return; } logger.error('getRooms error', { error }); res.status(500).json({ success: false, message: 'Nextcloud-Räume konnten nicht geladen werden' }); } } async getMessages(req: Request, res: Response): Promise { try { const credentials = await userService.getNextcloudCredentials(req.user!.id); if (!credentials) { res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } const token = req.params.token as string; if (!token) { res.status(400).json({ success: false, message: 'Room token fehlt' }); return; } const lookIntoFuture = req.query.lookIntoFuture === '1'; const lastKnownMessageId = req.query.lastKnownMessageId ? parseInt(req.query.lastKnownMessageId as string, 10) : undefined; const timeout = req.query.timeout ? Math.min(parseInt(req.query.timeout as string, 10), 25) : 25; const messages = await nextcloudService.getMessages(token, credentials.loginName, credentials.appPassword, { lookIntoFuture, lastKnownMessageId, timeout, }); res.status(200).json({ success: true, data: messages }); } catch (error: any) { if (error?.code === 'NEXTCLOUD_AUTH_INVALID') { await userService.clearNextcloudCredentials(req.user!.id); res.status(200).json({ success: true, data: { connected: false } }); return; } logger.error('getMessages error', { error }); res.status(500).json({ success: false, message: 'Nachrichten konnten nicht geladen werden' }); } } async sendMessage(req: Request, res: Response): Promise { try { const credentials = await userService.getNextcloudCredentials(req.user!.id); if (!credentials) { res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } const token = req.params.token as string; const { message, replyTo } = req.body; if (!token || !message || typeof message !== 'string' || message.trim().length === 0) { res.status(400).json({ success: false, message: 'Token und Nachricht erforderlich' }); return; } if (message.length > 32000) { res.status(400).json({ success: false, message: 'Nachricht zu lang' }); return; } const replyToNum = (typeof replyTo === 'number' && replyTo > 0) ? replyTo : undefined; await nextcloudService.sendMessage(token, message.trim(), credentials.loginName, credentials.appPassword, replyToNum); res.status(200).json({ success: true, data: null }); } catch (error: any) { if (error?.code === 'NEXTCLOUD_AUTH_INVALID') { await userService.clearNextcloudCredentials(req.user!.id); res.status(200).json({ success: true, data: { connected: false } }); return; } logger.error('sendMessage error', { error }); res.status(500).json({ success: false, message: 'Nachricht konnte nicht gesendet werden' }); } } async uploadFile(req: Request, res: Response): Promise { try { const credentials = await userService.getNextcloudCredentials(req.user!.id); if (!credentials) { res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } const token = req.params.token as string; if (!token) { res.status(400).json({ success: false, message: 'Room token fehlt' }); return; } if (!req.file) { res.status(400).json({ success: false, message: 'Keine Datei übermittelt' }); return; } await nextcloudService.uploadFileToTalk( token, req.file.buffer, req.file.originalname, req.file.mimetype, credentials.loginName, credentials.appPassword, ); res.status(200).json({ success: true, data: null }); } catch (error: any) { if (error?.code === 'NEXTCLOUD_AUTH_INVALID') { await userService.clearNextcloudCredentials(req.user!.id); res.status(200).json({ success: true, data: { connected: false } }); return; } logger.error('uploadFile error', { error }); res.status(500).json({ success: false, message: 'Datei konnte nicht hochgeladen werden' }); } } async downloadFile(req: Request, res: Response): Promise { try { const credentials = await userService.getNextcloudCredentials(req.user!.id); if (!credentials) { res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } const filePath = req.query.path as string; if (!filePath) { res.status(400).json({ success: false, message: 'Dateipfad fehlt' }); return; } // Path traversal protection const normalized = path.normalize(filePath); if (normalized.includes('..') || !normalized.startsWith('/')) { res.status(400).json({ success: false, message: 'Ungültiger Dateipfad' }); return; } const response = await nextcloudService.downloadFile( filePath, credentials.loginName, credentials.appPassword, ); const contentType = response.headers['content-type'] ?? 'application/octet-stream'; const contentDisposition = response.headers['content-disposition'] ?? `attachment; filename="${String(req.params.fileId).replace(/["\r\n\\]/g, '_')}"`; res.setHeader('Content-Type', contentType); res.setHeader('Content-Disposition', contentDisposition); if (response.headers['content-length']) { res.setHeader('Content-Length', response.headers['content-length']); } response.data.pipe(res); } catch (error: any) { if (error?.code === 'NEXTCLOUD_AUTH_INVALID') { await userService.clearNextcloudCredentials(req.user!.id); res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } logger.error('downloadFile error', { error }); res.status(500).json({ success: false, message: 'Datei konnte nicht heruntergeladen werden' }); } } async getFilePreview(req: Request, res: Response): Promise { try { const credentials = await userService.getNextcloudCredentials(req.user!.id); if (!credentials) { res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } const fileId = parseInt(req.params.fileId as string, 10); if (isNaN(fileId)) { res.status(400).json({ success: false, message: 'Ungültige Datei-ID' }); return; } const w = parseInt((req.query.w as string) ?? '400', 10) || 400; const h = parseInt((req.query.h as string) ?? '400', 10) || 400; const response = await nextcloudService.getFilePreview( fileId, Math.min(w, 1200), Math.min(h, 1200), credentials.loginName, credentials.appPassword, ); const contentType = response.headers['content-type'] ?? 'image/jpeg'; res.setHeader('Content-Type', contentType); if (response.headers['content-length']) { res.setHeader('Content-Length', response.headers['content-length']); } res.setHeader('Cache-Control', 'private, max-age=300'); response.data.pipe(res); } catch (error: any) { if (error?.code === 'NEXTCLOUD_AUTH_INVALID') { await userService.clearNextcloudCredentials(req.user!.id); res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } logger.error('getFilePreview error', { error }); res.status(500).json({ success: false, message: 'Vorschau konnte nicht geladen werden' }); } } async markRoomAsRead(req: Request, res: Response): Promise { try { const credentials = await userService.getNextcloudCredentials(req.user!.id); if (!credentials) { res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } const token = req.params.token as string; if (!token) { res.status(400).json({ success: false, message: 'Room token fehlt' }); return; } await nextcloudService.markAsRead(token, credentials.loginName, credentials.appPassword); res.status(200).json({ success: true, data: null }); } catch (error: any) { if (error?.code === 'NEXTCLOUD_AUTH_INVALID') { await userService.clearNextcloudCredentials(req.user!.id); res.status(200).json({ success: true, data: { connected: false } }); return; } logger.error('markRoomAsRead error', { error }); res.status(500).json({ success: false, message: 'Raum konnte nicht als gelesen markiert werden' }); } } async searchUsers(req: Request, res: Response): Promise { try { const credentials = await userService.getNextcloudCredentials(req.user!.id); if (!credentials) { res.status(200).json({ success: true, data: [] }); return; } const query = (req.query.search as string) ?? ''; const results = await nextcloudService.searchUsers(query, credentials.loginName, credentials.appPassword); res.status(200).json({ success: true, data: results }); } catch (error: any) { if (error?.code === 'NEXTCLOUD_AUTH_INVALID') { await userService.clearNextcloudCredentials(req.user!.id); res.status(200).json({ success: true, data: [] }); return; } logger.error('searchUsers error', { error }); res.status(500).json({ success: false, message: 'Benutzersuche fehlgeschlagen' }); } } async createRoom(req: Request, res: Response): Promise { try { const credentials = await userService.getNextcloudCredentials(req.user!.id); if (!credentials) { res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } const { roomType, invite, roomName } = req.body; if (typeof roomType !== 'number' || !invite || typeof invite !== 'string') { res.status(400).json({ success: false, message: 'roomType und invite erforderlich' }); return; } const result = await nextcloudService.createRoom(roomType, invite, roomName, credentials.loginName, credentials.appPassword); res.status(200).json({ success: true, data: result }); } catch (error: any) { if (error?.code === 'NEXTCLOUD_AUTH_INVALID') { await userService.clearNextcloudCredentials(req.user!.id); res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } logger.error('createRoom error', { error }); res.status(500).json({ success: false, message: 'Raum konnte nicht erstellt werden' }); } } async addReaction(req: Request, res: Response): Promise { try { const credentials = await userService.getNextcloudCredentials(req.user!.id); if (!credentials) { res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } const token = req.params.token as string; const messageId = parseInt(req.params.messageId as string, 10); const { reaction } = req.body; if (!token || isNaN(messageId) || !reaction) { res.status(400).json({ success: false, message: 'Parameter fehlen' }); return; } await nextcloudService.addReaction(token, messageId, reaction, credentials.loginName, credentials.appPassword); res.status(200).json({ success: true, data: null }); } catch (error: any) { if (error?.code === 'NEXTCLOUD_AUTH_INVALID') { await userService.clearNextcloudCredentials(req.user!.id); res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } logger.error('addReaction error', { error }); res.status(500).json({ success: false, message: 'Reaktion konnte nicht hinzugefügt werden' }); } } async removeReaction(req: Request, res: Response): Promise { try { const credentials = await userService.getNextcloudCredentials(req.user!.id); if (!credentials) { res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } const token = req.params.token as string; const messageId = parseInt(req.params.messageId as string, 10); const reaction = req.query.reaction as string; if (!token || isNaN(messageId) || !reaction) { res.status(400).json({ success: false, message: 'Parameter fehlen' }); return; } await nextcloudService.removeReaction(token, messageId, reaction, credentials.loginName, credentials.appPassword); res.status(200).json({ success: true, data: null }); } catch (error: any) { if (error?.code === 'NEXTCLOUD_AUTH_INVALID') { await userService.clearNextcloudCredentials(req.user!.id); res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } logger.error('removeReaction error', { error }); res.status(500).json({ success: false, message: 'Reaktion konnte nicht entfernt werden' }); } } async getReactions(req: Request, res: Response): Promise { try { const credentials = await userService.getNextcloudCredentials(req.user!.id); if (!credentials) { res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } const token = req.params.token as string; const messageId = parseInt(req.params.messageId as string, 10); if (!token || isNaN(messageId)) { res.status(400).json({ success: false, message: 'Parameter fehlen' }); return; } const data = await nextcloudService.getReactions(token, messageId, credentials.loginName, credentials.appPassword); res.status(200).json({ success: true, data }); } catch (error: any) { if (error?.code === 'NEXTCLOUD_AUTH_INVALID') { await userService.clearNextcloudCredentials(req.user!.id); res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } logger.error('getReactions error', { error }); res.status(500).json({ success: false, message: 'Reaktionen konnten nicht geladen werden' }); } } async getPoll(req: Request, res: Response): Promise { try { const credentials = await userService.getNextcloudCredentials(req.user!.id); if (!credentials) { res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } const token = req.params.token as string; const pollId = parseInt(req.params.pollId as string, 10); if (!token || isNaN(pollId)) { res.status(400).json({ success: false, message: 'Parameter fehlen' }); return; } const data = await nextcloudService.getPollDetails(token, pollId, credentials.loginName, credentials.appPassword); res.status(200).json({ success: true, data }); } catch (error: any) { if (error?.code === 'NEXTCLOUD_AUTH_INVALID') { await userService.clearNextcloudCredentials(req.user!.id); res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' }); return; } logger.error('getPoll error', { error }); res.status(500).json({ success: false, message: 'Abstimmung konnte nicht geladen werden' }); } } } export default new NextcloudController();