const express = require('express'); const router = express.Router(); const { CalendarUser } = require('../models'); const CalendarService = require('../services/CalendarService'); // Get all calendar users router.get('/users', async (req, res) => { try { const users = await CalendarUser.findAll({ where: { isActive: true }, attributes: ['id', 'name', 'nextcloudUrl', 'username', 'calendarName', 'isActive'] }); res.json(users); } catch (error) { console.error('Error fetching calendar users:', error); res.status(500).json({ error: 'Failed to fetch calendar users' }); } }); // Add a new calendar user router.post('/users', async (req, res) => { try { const { name, nextcloudUrl, username, password, calendarName } = req.body; if (!name || !nextcloudUrl || !username || !password) { return res.status(400).json({ error: 'Missing required fields: name, nextcloudUrl, username, password' }); } // Test connection before saving const testConfig = { name, nextcloudUrl, username, password, calendarName }; const connectionTest = await CalendarService.testConnection(testConfig); if (!connectionTest) { return res.status(400).json({ error: 'Failed to connect to Nextcloud instance. Please check your credentials.' }); } const user = await CalendarUser.create({ name, nextcloudUrl, username, password, calendarName }); // Return user without password const { password: _, ...userWithoutPassword } = user.toJSON(); res.status(201).json(userWithoutPassword); } catch (error) { console.error('Error creating calendar user:', error); if (error.name === 'SequelizeUniqueConstraintError') { res.status(400).json({ error: 'User name already exists' }); } else { res.status(500).json({ error: 'Failed to create calendar user' }); } } }); // Update a calendar user router.put('/users/:id', async (req, res) => { try { const { id } = req.params; const { name, nextcloudUrl, username, password, calendarName, isActive } = req.body; const user = await CalendarUser.findByPk(id); if (!user) { return res.status(404).json({ error: 'Calendar user not found' }); } // Test connection if credentials are being updated if (nextcloudUrl || username || password) { const testConfig = { name: name || user.name, nextcloudUrl: nextcloudUrl || user.nextcloudUrl, username: username || user.username, password: password || user.password, calendarName: calendarName || user.calendarName }; const connectionTest = await CalendarService.testConnection(testConfig); if (!connectionTest) { return res.status(400).json({ error: 'Failed to connect to Nextcloud instance. Please check your credentials.' }); } } await user.update({ ...(name && { name }), ...(nextcloudUrl && { nextcloudUrl }), ...(username && { username }), ...(password && { password }), ...(calendarName !== undefined && { calendarName }), ...(isActive !== undefined && { isActive }) }); // Return user without password const { password: _, ...userWithoutPassword } = user.toJSON(); res.json(userWithoutPassword); } catch (error) { console.error('Error updating calendar user:', error); res.status(500).json({ error: 'Failed to update calendar user' }); } }); // Delete a calendar user router.delete('/users/:id', async (req, res) => { try { const { id } = req.params; const user = await CalendarUser.findByPk(id); if (!user) { return res.status(404).json({ error: 'Calendar user not found' }); } await user.destroy(); res.json({ message: 'Calendar user deleted successfully' }); } catch (error) { console.error('Error deleting calendar user:', error); res.status(500).json({ error: 'Failed to delete calendar user' }); } }); // Get calendar events for a specific month // GET /api/calendar/events/:year/:month?user=username&type=family|individual router.get('/events/:year/:month', async (req, res) => { try { const { year, month } = req.params; const { user, type } = req.query; // Validate year and month if (!year || !month || !/^\d{4}$/.test(year) || !/^(0[1-9]|1[0-2])$/.test(month)) { return res.status(400).json({ error: 'Invalid year or month format. Use YYYY for year and MM for month.' }); } if (type === 'family') { // Get events from all active users const users = await CalendarUser.findAll({ where: { isActive: true }, attributes: ['name', 'nextcloudUrl', 'username', 'password', 'calendarName'] }); if (users.length === 0) { return res.json([]); } // Convert Sequelize instances to plain objects const userConfigs = users.map(user => user.toJSON()); const events = await CalendarService.getFamilyEventsForMonth(userConfigs, year, month); res.json(events); } else { // Get events for a specific user if (!user) { return res.status(400).json({ error: 'User parameter required for individual calendar view' }); } const userConfig = await CalendarUser.findOne({ where: { name: user, isActive: true }, attributes: ['name', 'nextcloudUrl', 'username', 'password', 'calendarName'] }); if (!userConfig) { return res.status(404).json({ error: 'User not found or inactive' }); } // Convert Sequelize instance to plain object const userConfigPlain = userConfig.toJSON(); const events = await CalendarService.getEventsForMonth(userConfigPlain, year, month); const birthdayEvents = await CalendarService.getContactBirthdaysForMonth(userConfigPlain, year, month); // Combine regular events and birthday events const allEvents = [...events, ...birthdayEvents]; // Sort events by date string first (YYYY-MM-DD format) - this ensures proper chronological ordering allEvents.sort((a, b) => { // First, compare dates as strings to avoid timezone issues if (a.date !== b.date) { return a.date.localeCompare(b.date); } // If dates are the same, sort by type (birthdays first) if (a.type === 'birthday' && b.type !== 'birthday') return -1; if (a.type !== 'birthday' && b.type === 'birthday') return 1; // Then sort by event text for consistent ordering return (a.text || '').localeCompare(b.text || ''); }); res.json(allEvents); } } catch (error) { console.error('Error retrieving calendar events:', error.message); res.status(500).json({ error: 'Failed to retrieve calendar events' }); } }); // Create a new calendar event // POST /api/calendar/events router.post('/events', async (req, res) => { try { const { user, date, text } = req.body; if (!user || !date || !text) { return res.status(400).json({ error: 'Missing required fields: user, date, text' }); } // Validate date format (YYYY-MM-DD) if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { return res.status(400).json({ error: 'Invalid date format. Use YYYY-MM-DD.' }); } const userConfig = await CalendarUser.findOne({ where: { name: user, isActive: true }, attributes: ['name', 'nextcloudUrl', 'username', 'password', 'calendarName'] }); if (!userConfig) { return res.status(404).json({ error: 'User not found or inactive' }); } const event = await CalendarService.createEvent(userConfig.toJSON(), date, text); res.status(201).json(event); } catch (error) { console.error('Error creating calendar event:', error.message); res.status(500).json({ error: 'Failed to create calendar event' }); } }); // Update a calendar event // PUT /api/calendar/events/:eventId router.put('/events/:eventId', async (req, res) => { try { const { eventId } = req.params; const { user, date, text } = req.body; if (!user || !date || !text) { return res.status(400).json({ error: 'Missing required fields: user, date, text' }); } // Validate date format (YYYY-MM-DD) if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { return res.status(400).json({ error: 'Invalid date format. Use YYYY-MM-DD.' }); } const userConfig = await CalendarUser.findOne({ where: { name: user, isActive: true }, attributes: ['name', 'nextcloudUrl', 'username', 'password', 'calendarName'] }); if (!userConfig) { return res.status(404).json({ error: 'User not found or inactive' }); } const event = await CalendarService.updateEvent(userConfig.toJSON(), eventId, date, text); res.json(event); } catch (error) { console.error('Error updating calendar event:', error); if (error.message === 'Event not found') { res.status(404).json({ error: 'Event not found' }); } else if (error.message.includes('You can only edit events that belong to you')) { res.status(403).json({ error: 'You can only edit events that belong to you' }); } else if (error.message.includes('Permission denied')) { res.status(403).json({ error: error.message }); } else if (error.message.includes('CalDAV authentication failed')) { res.status(401).json({ error: 'Calendar authentication failed' }); } else { res.status(500).json({ error: 'Failed to update calendar event' }); } } }); // Delete a calendar event // DELETE /api/calendar/events/:eventId router.delete('/events/:eventId', async (req, res) => { try { const { eventId } = req.params; const { user } = req.body; if (!user) { return res.status(400).json({ error: 'Missing required field: user' }); } // Check if this is a birthday event (birthday events should not be deletable) if (eventId && eventId.includes('birthday-')) { return res.status(403).json({ error: 'Birthday events cannot be deleted from the calendar interface' }); } const userConfig = await CalendarUser.findOne({ where: { name: user, isActive: true }, attributes: ['name', 'nextcloudUrl', 'username', 'password', 'calendarName'] }); if (!userConfig) { return res.status(404).json({ error: 'User not found or inactive' }); } await CalendarService.deleteEvent(userConfig.toJSON(), eventId); res.json({ message: 'Event deleted successfully' }); } catch (error) { console.error('Error deleting calendar event:', error); if (error.message === 'Event not found') { res.status(404).json({ error: 'Event not found. This event may belong to a different user or have already been deleted.' }); } else if (error.message.includes('You can only delete events that belong to you')) { res.status(403).json({ error: 'You can only delete events that belong to you' }); } else if (error.message === 'Birthday events cannot be deleted from the calendar interface') { res.status(403).json({ error: error.message }); } else if (error.message.includes('Permission denied') || error.message.includes('authentication failed')) { res.status(403).json({ error: 'Permission denied. You may not have access to delete this event.' }); } else { res.status(500).json({ error: 'Failed to delete calendar event' }); } } }); // Test Nextcloud connection for a user router.post('/users/:id/test-connection', async (req, res) => { try { const { id } = req.params; const user = await CalendarUser.findByPk(id); if (!user) { return res.status(404).json({ error: 'Calendar user not found' }); } const connectionTest = await CalendarService.testConnection({ name: user.name, nextcloudUrl: user.nextcloudUrl, username: user.username, password: user.password, calendarName: user.calendarName }); res.json({ success: connectionTest, message: connectionTest ? 'Connection successful' : 'Connection failed' }); } catch (error) { console.error('Error testing connection:', error); res.status(500).json({ error: 'Failed to test connection' }); } }); // Get contact birthdays for a specific month // GET /api/calendar/birthdays/:year/:month?user=username router.get('/birthdays/:year/:month', async (req, res) => { try { const { year, month } = req.params; const { user } = req.query; // Validate year and month if (!year || !month || !/^\d{4}$/.test(year) || !/^(0[1-9]|1[0-2])$/.test(month)) { return res.status(400).json({ error: 'Invalid year or month format. Use YYYY for year and MM for month.' }); } if (!user) { return res.status(400).json({ error: 'User parameter required for birthday calendar access' }); } const userConfig = await CalendarUser.findOne({ where: { name: user, isActive: true }, attributes: ['name', 'nextcloudUrl', 'username', 'password', 'calendarName'] }); if (!userConfig) { return res.status(404).json({ error: 'User not found or inactive' }); } const birthdayEvents = await CalendarService.getContactBirthdaysForMonth(userConfig.toJSON(), year, month); res.json(birthdayEvents); } catch (error) { console.error('Error retrieving contact birthdays:', error.message); res.status(500).json({ error: 'Failed to retrieve contact birthdays' }); } }); module.exports = router;