feat: add CalDAV synchronization with automatic sync
- Add CaldavService with tsdav/ical.js for CalDAV server communication - Add CaldavController, CaldavRepository, and caldav routes - Add client-side CaldavConfigService with sync(), config CRUD - Add CalDAV settings UI with config load/save in settings screen - Sync on login, auto-login (AuthGuard), periodic timer (calendar), and sync button - Push single events to CalDAV on server-side create/update/delete - Push all events to CalDAV after chat event confirmation - Refactor ChatService to use EventService instead of direct EventRepository - Rename CalDav/calDav to Caldav/caldav for consistent naming - Add Radicale Docker setup for local CalDAV testing - Update PlantUML diagrams and CLAUDE.md with CalDAV architecture
This commit is contained in:
31
apps/server/src/repositories/mongo/MongoCaldavRepository.ts
Normal file
31
apps/server/src/repositories/mongo/MongoCaldavRepository.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { CaldavConfig } from "@calchat/shared";
|
||||
import { Logged } from "../../logging/Logged";
|
||||
import { CaldavRepository } from "../../services/interfaces/CaldavRepository";
|
||||
import { CaldavConfigModel } from "./models/CaldavConfigModel";
|
||||
|
||||
@Logged("MongoCaldavRepository")
|
||||
export class MongoCaldavRepository implements CaldavRepository {
|
||||
async findByUserId(userId: string): Promise<CaldavConfig | null> {
|
||||
const config = await CaldavConfigModel.findOne({ userId });
|
||||
if (!config) return null;
|
||||
return config.toJSON() as unknown as CaldavConfig;
|
||||
}
|
||||
|
||||
async createOrUpdate(config: CaldavConfig): Promise<CaldavConfig> {
|
||||
const caldavConfig = await CaldavConfigModel.findOneAndUpdate(
|
||||
{ userId: config.userId },
|
||||
config,
|
||||
{
|
||||
upsert: true,
|
||||
new: true,
|
||||
},
|
||||
);
|
||||
// NOTE: Casting required because Mongoose's toJSON() type doesn't reflect our virtual 'id' field
|
||||
return caldavConfig.toJSON() as unknown as CaldavConfig;
|
||||
}
|
||||
|
||||
async deleteByUserId(userId: string): Promise<boolean> {
|
||||
const result = await CaldavConfigModel.findOneAndDelete({userId});
|
||||
return result !== null;
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,12 @@ export class MongoEventRepository implements EventRepository {
|
||||
return events.map((e) => e.toJSON() as unknown as CalendarEvent);
|
||||
}
|
||||
|
||||
async findByCaldavUUID(userId: string, caldavUUID: string): Promise<CalendarEvent | null> {
|
||||
const event = await EventModel.findOne({ userId, caldavUUID });
|
||||
if (!event) return null;
|
||||
return event.toJSON() as unknown as CalendarEvent;
|
||||
}
|
||||
|
||||
async searchByTitle(userId: string, query: string): Promise<CalendarEvent[]> {
|
||||
const events = await EventModel.find({
|
||||
userId,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { CaldavConfig } from "@calchat/shared";
|
||||
import mongoose, { Document, Schema } from "mongoose";
|
||||
|
||||
export interface CaldavConfigDocument extends CaldavConfig, Document {
|
||||
toJSON(): CaldavConfig;
|
||||
}
|
||||
|
||||
const CaldavConfigSchema = new Schema<CaldavConfigDocument>(
|
||||
{
|
||||
userId: { type: String, required: true, index: true },
|
||||
serverUrl: { type: String, required: true },
|
||||
username: { type: String, required: true },
|
||||
password: { type: String, required: true },
|
||||
syncIntervalSeconds: { type: Number },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
export const CaldavConfigModel = mongoose.model<CaldavConfigDocument>(
|
||||
"CaldavConfig",
|
||||
CaldavConfigSchema,
|
||||
);
|
||||
@@ -21,6 +21,12 @@ const EventSchema = new Schema<
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
caldavUUID: {
|
||||
type: String,
|
||||
},
|
||||
etag: {
|
||||
type: String,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
||||
Reference in New Issue
Block a user