Files
dashboard/backend/src/controllers/nextcloud.controller.ts
Matthias Hochmeister 215528a521 update
2026-03-16 14:41:08 +01:00

429 lines
19 KiB
TypeScript

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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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();