Add calender support in server
This commit is contained in:
@@ -4,6 +4,7 @@ require('dotenv').config();
|
|||||||
|
|
||||||
const { sequelize, cleanupCheckedItems } = require('./models');
|
const { sequelize, cleanupCheckedItems } = require('./models');
|
||||||
const shoppingRoutes = require('./routes/shopping');
|
const shoppingRoutes = require('./routes/shopping');
|
||||||
|
const calendarRoutes = require('./routes/calendar');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
@@ -15,13 +16,15 @@ app.use(express.urlencoded({ extended: true }));
|
|||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
app.use('/api/shopping', shoppingRoutes);
|
app.use('/api/shopping', shoppingRoutes);
|
||||||
|
app.use('/api/calendar', calendarRoutes);
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
app.get('/api/health', (req, res) => {
|
app.get('/api/health', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
message: 'Shopping List Server is running',
|
message: 'OpenWall Server is running',
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
|
features: ['shopping', 'calendar']
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,9 +55,10 @@ const startServer = async () => {
|
|||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
app.listen(PORT, () => {
|
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(`Health check available at: http://localhost:${PORT}/api/health`);
|
||||||
console.log(`Shopping API available at: http://localhost:${PORT}/api/shopping`);
|
console.log(`Shopping API available at: http://localhost:${PORT}/api/shopping`);
|
||||||
|
console.log(`Calendar API available at: http://localhost:${PORT}/api/calendar`);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Unable to start server:', error);
|
console.error('Unable to start server:', error);
|
||||||
|
41
server/models/CalendarUser.js
Normal file
41
server/models/CalendarUser.js
Normal file
@@ -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;
|
||||||
|
};
|
@@ -4,39 +4,41 @@ const path = require('path');
|
|||||||
// Initialize Sequelize with SQLite
|
// Initialize Sequelize with SQLite
|
||||||
const sequelize = new Sequelize({
|
const sequelize = new Sequelize({
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
storage: path.join(__dirname, '../database.sqlite'),
|
storage: path.join(__dirname, '..', 'database.sqlite'),
|
||||||
logging: false, // Set to console.log to see SQL queries
|
logging: process.env.NODE_ENV === 'development' ? console.log : false
|
||||||
});
|
});
|
||||||
|
|
||||||
// Import models
|
// Import and initialize models
|
||||||
const ShoppingItem = require('./ShoppingItem')(sequelize);
|
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 () => {
|
const cleanupCheckedItems = async () => {
|
||||||
try {
|
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({
|
const deletedCount = await ShoppingItem.destroy({
|
||||||
where: {
|
where: {
|
||||||
checked: true,
|
checked: true,
|
||||||
checkedAt: {
|
updatedAt: {
|
||||||
[Sequelize.Op.lt]: twoHoursAgo,
|
[Sequelize.Op.lt]: oneDayAgo
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deletedCount > 0) {
|
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) {
|
} 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 = {
|
module.exports = {
|
||||||
sequelize,
|
sequelize,
|
||||||
ShoppingItem,
|
ShoppingItem,
|
||||||
cleanupCheckedItems,
|
CalendarUser,
|
||||||
|
cleanupCheckedItems
|
||||||
};
|
};
|
||||||
|
31
server/package-lock.json
generated
31
server/package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nextcloud/cdav-library": "^1.1.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.0",
|
"dotenv": "^17.2.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
@@ -26,6 +27,19 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"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": {
|
"node_modules/@npmcli/fs": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz",
|
||||||
@@ -501,6 +515,17 @@
|
|||||||
"node": ">=6.6.0"
|
"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": {
|
"node_modules/cors": {
|
||||||
"version": "2.8.5",
|
"version": "2.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||||
@@ -1979,6 +2004,12 @@
|
|||||||
"node": ">=8.10.0"
|
"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": {
|
"node_modules/retry": {
|
||||||
"version": "0.12.0",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||||
|
@@ -13,6 +13,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"packageManager": "pnpm@10.13.1",
|
"packageManager": "pnpm@10.13.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nextcloud/cdav-library": "^1.1.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.0",
|
"dotenv": "^17.2.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
323
server/routes/calendar.js
Normal file
323
server/routes/calendar.js
Normal file
@@ -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;
|
305
server/services/CalendarService.js
Normal file
305
server/services/CalendarService.js
Normal file
@@ -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<Object>} 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>} 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>} 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<Object>} 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<boolean>} 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<boolean>} 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();
|
Reference in New Issue
Block a user