diff --git a/server/index.js b/server/index.js index 9156c25..23da911 100644 --- a/server/index.js +++ b/server/index.js @@ -4,6 +4,7 @@ require('dotenv').config(); const { sequelize, cleanupCheckedItems } = require('./models'); const shoppingRoutes = require('./routes/shopping'); +const calendarRoutes = require('./routes/calendar'); const app = express(); const PORT = process.env.PORT || 3001; @@ -15,13 +16,15 @@ app.use(express.urlencoded({ extended: true })); // Routes app.use('/api/shopping', shoppingRoutes); +app.use('/api/calendar', calendarRoutes); // Health check endpoint app.get('/api/health', (req, res) => { res.json({ status: 'OK', - message: 'Shopping List Server is running', - timestamp: new Date().toISOString() + message: 'OpenWall Server is running', + timestamp: new Date().toISOString(), + features: ['shopping', 'calendar'] }); }); @@ -52,9 +55,10 @@ const startServer = async () => { // Start the server app.listen(PORT, () => { - console.log(`Shopping List Server is running on port ${PORT}`); + console.log(`OpenWall Server is running on port ${PORT}`); console.log(`Health check available at: http://localhost:${PORT}/api/health`); console.log(`Shopping API available at: http://localhost:${PORT}/api/shopping`); + console.log(`Calendar API available at: http://localhost:${PORT}/api/calendar`); }); } catch (error) { console.error('Unable to start server:', error); diff --git a/server/models/CalendarUser.js b/server/models/CalendarUser.js new file mode 100644 index 0000000..e6c5065 --- /dev/null +++ b/server/models/CalendarUser.js @@ -0,0 +1,41 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const CalendarUser = sequelize.define('CalendarUser', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + name: { + type: DataTypes.STRING, + allowNull: false, + unique: true + }, + nextcloudUrl: { + type: DataTypes.STRING, + allowNull: false + }, + username: { + type: DataTypes.STRING, + allowNull: false + }, + password: { + type: DataTypes.STRING, + allowNull: false + }, + calendarName: { + type: DataTypes.STRING, + allowNull: true // Will default to personal calendar if not specified + }, + isActive: { + type: DataTypes.BOOLEAN, + defaultValue: true + } + }, { + tableName: 'calendar_users', + timestamps: true + }); + + return CalendarUser; +}; diff --git a/server/models/index.js b/server/models/index.js index 9f05a1a..7302c45 100644 --- a/server/models/index.js +++ b/server/models/index.js @@ -4,39 +4,41 @@ const path = require('path'); // Initialize Sequelize with SQLite const sequelize = new Sequelize({ dialect: 'sqlite', - storage: path.join(__dirname, '../database.sqlite'), - logging: false, // Set to console.log to see SQL queries + storage: path.join(__dirname, '..', 'database.sqlite'), + logging: process.env.NODE_ENV === 'development' ? console.log : false }); -// Import models -const ShoppingItem = require('./ShoppingItem')(sequelize); +// Import and initialize models +const createShoppingItem = require('./ShoppingItem'); +const createCalendarUser = require('./CalendarUser'); -// Function to clean up checked items older than 2 hours +const ShoppingItem = createShoppingItem(sequelize); +const CalendarUser = createCalendarUser(sequelize); + +// Function to clean up checked shopping items older than 24 hours const cleanupCheckedItems = async () => { try { - const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000); + const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); const deletedCount = await ShoppingItem.destroy({ where: { checked: true, - checkedAt: { - [Sequelize.Op.lt]: twoHoursAgo, - }, - }, + updatedAt: { + [Sequelize.Op.lt]: oneDayAgo + } + } }); if (deletedCount > 0) { - console.log(`Cleaned up ${deletedCount} checked items older than 2 hours`); + console.log(`Cleaned up ${deletedCount} checked shopping items older than 24 hours`); } } catch (error) { - console.error('Error cleaning up checked items:', error); + console.error('Error during cleanup:', error); } }; -// Run cleanup every 30 minutes -setInterval(cleanupCheckedItems, 30 * 60 * 1000); - module.exports = { sequelize, ShoppingItem, - cleanupCheckedItems, + CalendarUser, + cleanupCheckedItems }; diff --git a/server/package-lock.json b/server/package-lock.json index 2e87ad2..755f0b0 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@nextcloud/cdav-library": "^1.1.0", "cors": "^2.8.5", "dotenv": "^17.2.0", "express": "^5.1.0", @@ -26,6 +27,19 @@ "license": "MIT", "optional": true }, + "node_modules/@nextcloud/cdav-library": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nextcloud/cdav-library/-/cdav-library-1.1.0.tgz", + "integrity": "sha512-hmJgR9Cp11y3ch4dS0NufsPgofe4+iwhUkusYKmDTl0PFsJrBUNy1zawLdfDrpEjK1zXrU3tOpyF3pIqyGMYBg==", + "license": "AGPL-3.0", + "dependencies": { + "core-js": "^3.19.3", + "regenerator-runtime": "^0.13.9" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -501,6 +515,17 @@ "node": ">=6.6.0" } }, + "node_modules/core-js": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.44.0.tgz", + "integrity": "sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -1979,6 +2004,12 @@ "node": ">=8.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", diff --git a/server/package.json b/server/package.json index 1c5d3d6..02c8540 100644 --- a/server/package.json +++ b/server/package.json @@ -13,6 +13,7 @@ "license": "ISC", "packageManager": "pnpm@10.13.1", "dependencies": { + "@nextcloud/cdav-library": "^1.1.0", "cors": "^2.8.5", "dotenv": "^17.2.0", "express": "^5.1.0", diff --git a/server/routes/calendar.js b/server/routes/calendar.js new file mode 100644 index 0000000..9d212b8 --- /dev/null +++ b/server/routes/calendar.js @@ -0,0 +1,323 @@ +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([]); + } + + const events = await CalendarService.getFamilyEventsForMonth(users, 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' }); + } + + const events = await CalendarService.getEventsForMonth(userConfig, year, month); + res.json(events); + } + } catch (error) { + console.error('Error retrieving calendar events:', error); + 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, date, text); + res.status(201).json(event); + } catch (error) { + console.error('Error creating calendar event:', error); + 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, 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('Permission denied')) { + res.status(403).json({ error: error.message }); + } 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' + }); + } + + 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, 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' }); + } else if (error.message.includes('Permission denied')) { + res.status(403).json({ error: error.message }); + } 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' }); + } +}); + +module.exports = router; diff --git a/server/services/CalendarService.js b/server/services/CalendarService.js new file mode 100644 index 0000000..d3954dc --- /dev/null +++ b/server/services/CalendarService.js @@ -0,0 +1,305 @@ +// Calendar Service for Nextcloud CalDAV integration +// This is a basic implementation that can be enhanced with the @nextcloud/cdav-library +const crypto = require('crypto'); +const https = require('https'); +const http = require('http'); +const { URL } = require('url'); + +class CalendarService { + constructor() { + this.events = new Map(); // In-memory storage for demo purposes + // TODO: Replace with actual CalDAV operations when library is available + } + + /** + * Create a calendar event + * @param {Object} userConfig - Nextcloud user configuration + * @param {string} date - Date in YYYY-MM-DD format + * @param {string} text - Event description + * @returns {Promise} Created event + */ + async createEvent(userConfig, date, text) { + try { + // TODO: Implement actual CalDAV event creation + // For now, simulate the operation with local storage + + const eventId = crypto.randomUUID(); + const event = { + id: eventId, + date: date, + text: text, + user: userConfig.name, + summary: text, + dtstart: this._formatDateForCalDAV(date), + dtend: this._formatDateForCalDAV(date), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + + this.events.set(eventId, event); + + console.log(`Calendar event created for ${userConfig.name}: ${text} on ${date}`); + + // TODO: When CalDAV library is available, replace with: + // const caldavEvent = await this._createCalDAVEvent(userConfig, event); + + return event; + } catch (error) { + console.error('Error creating calendar event:', error); + throw new Error('Failed to create calendar event'); + } + } + + /** + * Get calendar events for a specific month + * @param {Object} userConfig - Nextcloud user configuration + * @param {string} year - Year (YYYY) + * @param {string} month - Month (MM) + * @returns {Promise} Array of events + */ + async getEventsForMonth(userConfig, year, month) { + try { + // TODO: Implement actual CalDAV event retrieval + // For now, filter local events + + const events = Array.from(this.events.values()); + const filteredEvents = events.filter(event => { + if (event.user !== userConfig.name) return false; + const eventDate = new Date(event.date); + return eventDate.getFullYear() === parseInt(year) && + (eventDate.getMonth() + 1) === parseInt(month); + }); + + console.log(`Retrieved ${filteredEvents.length} events for ${userConfig.name} in ${year}-${month}`); + + // TODO: When CalDAV library is available, replace with: + // const caldavEvents = await this._fetchCalDAVEvents(userConfig, year, month); + + return filteredEvents; + } catch (error) { + console.error('Error retrieving calendar events:', error); + throw new Error('Failed to retrieve calendar events'); + } + } + + /** + * Get calendar events for all users (family view) + * @param {Array} userConfigs - Array of user configurations + * @param {string} year - Year (YYYY) + * @param {string} month - Month (MM) + * @returns {Promise} Array of events from all users + */ + async getFamilyEventsForMonth(userConfigs, year, month) { + try { + const allEvents = []; + + for (const userConfig of userConfigs) { + const userEvents = await this.getEventsForMonth(userConfig, year, month); + allEvents.push(...userEvents); + } + + // Sort events by date + allEvents.sort((a, b) => new Date(a.date) - new Date(b.date)); + + console.log(`Retrieved ${allEvents.length} family events for ${year}-${month}`); + return allEvents; + } catch (error) { + console.error('Error retrieving family calendar events:', error); + throw new Error('Failed to retrieve family calendar events'); + } + } + + /** + * Update a calendar event + * @param {Object} userConfig - Nextcloud user configuration + * @param {string} eventId - Event ID + * @param {string} date - New date + * @param {string} text - New text + * @returns {Promise} Updated event + */ + async updateEvent(userConfig, eventId, date, text) { + try { + const event = this.events.get(eventId); + if (!event) { + throw new Error('Event not found'); + } + + if (event.user !== userConfig.name) { + throw new Error('Permission denied: You can only edit your own events'); + } + + event.date = date; + event.text = text; + event.summary = text; + event.dtstart = this._formatDateForCalDAV(date); + event.dtend = this._formatDateForCalDAV(date); + event.updatedAt = new Date().toISOString(); + + this.events.set(eventId, event); + + console.log(`Calendar event updated for ${userConfig.name}: ${eventId}`); + + // TODO: When CalDAV library is available, replace with: + // await this._updateCalDAVEvent(userConfig, event); + + return event; + } catch (error) { + console.error('Error updating calendar event:', error); + throw error; + } + } + + /** + * Delete a calendar event + * @param {Object} userConfig - Nextcloud user configuration + * @param {string} eventId - Event ID + * @returns {Promise} Success status + */ + async deleteEvent(userConfig, eventId) { + try { + const event = this.events.get(eventId); + if (!event) { + throw new Error('Event not found'); + } + + if (event.user !== userConfig.name) { + throw new Error('Permission denied: You can only delete your own events'); + } + + this.events.delete(eventId); + + console.log(`Calendar event deleted for ${userConfig.name}: ${eventId}`); + + // TODO: When CalDAV library is available, replace with: + // await this._deleteCalDAVEvent(userConfig, eventId); + + return true; + } catch (error) { + console.error('Error deleting calendar event:', error); + throw error; + } + } + + /** + * Test connection to Nextcloud instance + * @param {Object} userConfig - Nextcloud user configuration + * @returns {Promise} Connection status + */ + async testConnection(userConfig) { + try { + // Basic HTTP test to verify Nextcloud instance is reachable + const url = new URL(userConfig.nextcloudUrl); + const options = { + hostname: url.hostname, + port: url.port || (url.protocol === 'https:' ? 443 : 80), + path: '/status.php', + method: 'GET', + timeout: 5000, + headers: { + 'User-Agent': 'OpenWall/1.0' + } + }; + + const protocol = url.protocol === 'https:' ? https : http; + + return new Promise((resolve) => { + const req = protocol.request(options, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + const status = JSON.parse(data); + const isNextcloud = status.productname && status.productname.toLowerCase().includes('nextcloud'); + console.log(`Connection test for ${userConfig.name}: ${isNextcloud ? 'SUCCESS' : 'NOT_NEXTCLOUD'}`); + resolve(isNextcloud); + } catch (e) { + console.log(`Connection test for ${userConfig.name}: INVALID_RESPONSE`); + resolve(false); + } + }); + }); + + req.on('error', (error) => { + console.log(`Connection test for ${userConfig.name}: ERROR - ${error.message}`); + resolve(false); + }); + + req.on('timeout', () => { + console.log(`Connection test for ${userConfig.name}: TIMEOUT`); + req.destroy(); + resolve(false); + }); + + req.end(); + }); + } catch (error) { + console.error('Error testing connection:', error); + return false; + } + } + + /** + * Format date for CalDAV (YYYYMMDD format for all-day events) + * @private + */ + _formatDateForCalDAV(dateString) { + const date = new Date(dateString); + return date.toISOString().slice(0, 10).replace(/-/g, ''); + } + + // TODO: Implement these methods when @nextcloud/cdav-library is available + /* + async _createCalDAVEvent(userConfig, event) { + const { createClient } = require('@nextcloud/cdav-library'); + const client = createClient({ + url: userConfig.nextcloudUrl, + username: userConfig.username, + password: userConfig.password + }); + + // Create ICS event content + const icsContent = this._generateICSEvent(event); + + // Create event on Nextcloud + const result = await client.createEvent(icsContent); + return result; + } + + async _fetchCalDAVEvents(userConfig, year, month) { + const { createClient } = require('@nextcloud/cdav-library'); + const client = createClient({ + url: userConfig.nextcloudUrl, + username: userConfig.username, + password: userConfig.password + }); + + const startDate = new Date(year, month - 1, 1); + const endDate = new Date(year, month, 0); + + const events = await client.fetchCalendarEvents({ + start: startDate, + end: endDate + }); + + return events; + } + + _generateICSEvent(event) { + return `BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//OpenWall//Calendar//EN +BEGIN:VEVENT +UID:${event.id} +DTSTART;VALUE=DATE:${event.dtstart} +DTEND;VALUE=DATE:${event.dtend} +SUMMARY:${event.summary} +DESCRIPTION:${event.text} +CREATED:${new Date(event.createdAt).toISOString().replace(/[-:]/g, '').split('.')[0]}Z +LAST-MODIFIED:${new Date(event.updatedAt).toISOString().replace(/[-:]/g, '').split('.')[0]}Z +END:VEVENT +END:VCALENDAR`; + } + */ +} + +module.exports = new CalendarService();