diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..78c34df --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,187 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**CalChat** is a calendar mobile app with AI support. The core concept is creating calendar events through a chat interface with an AI chatbot (Claude). Users can add, edit, and delete events via natural language conversation. + +This is a fullstack TypeScript monorepo with npm workspaces. + +## Commands + +### Root (monorepo) +```bash +npm install # Install all dependencies for all workspaces +``` + +### Client (apps/client) - Expo React Native app +```bash +npm run start -w @caldav/client # Start Expo dev server +npm run android -w @caldav/client # Start on Android +npm run ios -w @caldav/client # Start on iOS +npm run web -w @caldav/client # Start web version +npm run lint -w @caldav/client # Run ESLint +``` + +### Server (apps/server) - Express.js backend +```bash +npm run dev -w @caldav/server # Start dev server with hot reload (tsx watch) +npm run build -w @caldav/server # Compile TypeScript +npm run start -w @caldav/server # Run compiled server (port 3000) +``` + +## Technology Stack + +| Area | Technology | Purpose | +|------|------------|---------| +| Frontend | React Native | Mobile UI Framework | +| | Expo | Development platform | +| | Expo-Router | File-based routing | +| | NativeWind | Tailwind CSS for React Native | +| Backend | Express.js | Web framework | +| | MongoDB | Database | +| | Mongoose | ODM | +| | Claude (Anthropic) | AI/LLM for chat | +| | JWT | Authentication | +| Planned | iCalendar | Event export/import | + +## Architecture + +### Workspace Structure +``` +apps/client - @caldav/client - Expo React Native app +apps/server - @caldav/server - Express.js backend +packages/shared - @caldav/shared - Shared TypeScript types and models +``` + +### Frontend Architecture (apps/client) + +**Screens** (`src/app/`): Login, Register, Calendar, Chat, Event Detail, Notes - file-based routing via Expo-Router + +**Services**: API Client for HTTP requests, plus AuthService, EventService, ChatService for domain logic + +**Components**: Reusable UI (EventCard, ChatBubble, EventConfirmDialog, MonthSelector) + +**Stores**: AuthStore (user + token), EventsStore (calendar events) + +### Backend Architecture (apps/server) + +``` +src/ +├── app.ts # Entry point, DI setup, Express config +├── controllers/ # Request handlers +│ ├── AuthController.ts # login(), register(), refresh(), logout() +│ ├── ChatController.ts # sendMessage(), confirmEvent(), rejectEvent(), getConversations(), getConversation() +│ └── EventController.ts # create(), getById(), getAll(), getByDateRange(), update(), delete() +├── middleware/ +│ └── AuthMiddleware.ts # authenticate() - JWT validation +├── routes/ # API endpoint definitions +│ ├── index.ts # Combines all routes under /api +│ ├── auth.routes.ts # /api/auth/* +│ ├── chat.routes.ts # /api/chat/* (protected) +│ └── event.routes.ts # /api/events/* (protected) +├── services/ # Business logic +│ ├── interfaces/ # DB-agnostic interfaces (for dependency injection) +│ │ ├── AIProvider.ts # processMessage() +│ │ ├── UserRepository.ts # + CreateUserData (server-internal DTO) +│ │ ├── EventRepository.ts +│ │ └── ChatRepository.ts +│ ├── AuthService.ts +│ ├── ChatService.ts +│ └── EventService.ts +├── repositories/ # Data access (DB-specific implementations) +│ ├── index.ts # Re-exports from ./mongo +│ └── mongo/ # MongoDB implementation +│ ├── models/ # Mongoose schemas +│ │ ├── UserModel.ts +│ │ ├── EventModel.ts +│ │ └── ChatModel.ts +│ ├── MongoUserRepository.ts +│ ├── MongoEventRepository.ts +│ └── MongoChatRepository.ts +├── ai/ +│ └── ClaudeAdapter.ts # Implements AIProvider +└── utils/ + ├── jwt.ts # signToken(), verifyToken() + └── password.ts # hash(), compare() +``` + +**API Endpoints:** +- `POST /api/auth/login` - User login +- `POST /api/auth/register` - User registration +- `POST /api/auth/refresh` - Refresh JWT token +- `POST /api/auth/logout` - User logout +- `GET /api/events` - Get all events (protected) +- `GET /api/events/range` - Get events by date range (protected) +- `GET /api/events/:id` - Get single event (protected) +- `POST /api/events` - Create event (protected) +- `PUT /api/events/:id` - Update event (protected) +- `DELETE /api/events/:id` - Delete event (protected) +- `POST /api/chat/message` - Send message to AI (protected) +- `POST /api/chat/confirm/:conversationId/:messageId` - Confirm proposed event (protected) +- `POST /api/chat/reject/:conversationId/:messageId` - Reject proposed event (protected) +- `GET /api/chat/conversations` - Get all conversations (protected) +- `GET /api/chat/conversations/:id` - Get messages of a conversation with cursor-based pagination (protected) +- `GET /health` - Health check + +### Shared Package (packages/shared) + +``` +src/ +├── index.ts +└── models/ + ├── index.ts + ├── User.ts # User, CreateUserDTO, LoginDTO, AuthResponse + ├── CalendarEvent.ts # CalendarEvent, CreateEventDTO, UpdateEventDTO + └── ChatMessage.ts # ChatMessage, Conversation, SendMessageDTO, CreateMessageDTO, + # GetMessagesOptions, ChatResponse, ConversationSummary +``` + +**Key Types:** +- `User`: id, email, displayName, passwordHash?, createdAt?, updatedAt? +- `CalendarEvent`: id, userId, title, description?, startTime, endTime, note?, isRecurring?, recurrenceRule? +- `ChatMessage`: id, conversationId, sender ('user' | 'assistant'), content, proposedEvent? +- `Conversation`: id, userId, createdAt?, updatedAt? (messages loaded separately via lazy loading) +- `CreateEventDTO`: Used for creating events AND for AI-proposed events +- `GetMessagesOptions`: Cursor-based pagination with `before?: string` and `limit?: number` +- `ConversationSummary`: id, lastMessage?, createdAt? (for conversation list) + +### Database Abstraction + +The repository pattern allows swapping databases: +- **Interfaces** (`services/interfaces/`) are DB-agnostic +- **Implementations** (`repositories/mongo/`) are DB-specific +- To add MySQL: create `repositories/mysql/` with TypeORM entities + +## MVP Feature Scope + +### Must-Have +- Chat interface with AI assistant (text input) for event management +- Calendar overview +- Manual event CRUD (without AI) +- View completed events +- Simple reminders +- One note per event +- Recurring events + +### Nice-to-Have +- iCalendar import/export +- Multiple calendars +- CalDAV synchronization with external services + +## Current Implementation Status + +**Backend:** Skeleton complete - all files exist with `throw new Error('Not implemented')` placeholders. Ready for step-by-step implementation. + +**Shared:** Types and DTOs defined and exported. + +**Frontend:** Calendar screen partially implemented, Chat screen exists but not connected to backend. + +## Documentation + +Detailed architecture diagrams are in `docs/`: +- `technisches_brainstorm.tex` - Technical concept document (German) +- `architecture-class-diagram.puml` - Backend class diagram +- `frontend-class-diagram.puml` - Frontend class diagram +- `component-diagram.puml` - System component overview diff --git a/apps/server/package.json b/apps/server/package.json index 9efc39e..0cc6599 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -8,12 +8,19 @@ "start": "node dist/app.js" }, "dependencies": { + "@anthropic-ai/sdk": "^0.71.2", "@caldav/shared": "*", - "express": "^5.2.1" + "bcrypt": "^6.0.0", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "mongoose": "^9.1.1" }, "devDependencies": { + "@types/bcrypt": "^6.0.0", "@types/express": "^5.0.6", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.10.1", - "tsx": "^4.21.0" + "tsx": "^4.21.0", + "typescript": "^5.9.3" } } diff --git a/apps/server/src/ai/ClaudeAdapter.ts b/apps/server/src/ai/ClaudeAdapter.ts new file mode 100644 index 0000000..030a3c9 --- /dev/null +++ b/apps/server/src/ai/ClaudeAdapter.ts @@ -0,0 +1,18 @@ +import Anthropic from '@anthropic-ai/sdk'; +import { AIProvider, AIContext, AIResponse } from '../services/interfaces'; + +export class ClaudeAdapter implements AIProvider { + private client: Anthropic; + private model: string; + + constructor(apiKey?: string, model: string = 'claude-3-haiku-20240307') { + this.client = new Anthropic({ + apiKey: apiKey || process.env.ANTHROPIC_API_KEY, + }); + this.model = model; + } + + async processMessage(message: string, context: AIContext): Promise { + throw new Error('Not implemented'); + } +} diff --git a/apps/server/src/ai/index.ts b/apps/server/src/ai/index.ts new file mode 100644 index 0000000..bc63f1a --- /dev/null +++ b/apps/server/src/ai/index.ts @@ -0,0 +1 @@ +export * from './ClaudeAdapter'; diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 1ced917..f3d2823 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -1,12 +1,64 @@ -import express, { Request, Response } from 'express'; +import express from 'express'; +import mongoose from 'mongoose'; + +import { createRoutes } from './routes'; +import { AuthController, ChatController, EventController } from './controllers'; +import { AuthService, ChatService, EventService } from './services'; +import { MongoUserRepository, MongoEventRepository, MongoChatRepository } from './repositories'; +import { ClaudeAdapter } from './ai'; const app = express(); -const port = 3000; +const port = process.env.PORT || 3000; +const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/caldav'; -app.get('/', (req: Request, res: Response) => { - res.send('Hello World!'); +// Middleware +app.use(express.json()); + +// Initialize repositories +const userRepo = new MongoUserRepository(); +const eventRepo = new MongoEventRepository(); +const chatRepo = new MongoChatRepository(); + +// Initialize AI provider +const aiProvider = new ClaudeAdapter(); + +// Initialize services +const authService = new AuthService(userRepo); +const chatService = new ChatService(chatRepo, eventRepo, aiProvider); +const eventService = new EventService(eventRepo); + +// Initialize controllers +const authController = new AuthController(authService); +const chatController = new ChatController(chatService); +const eventController = new EventController(eventService); + +// Setup routes +app.use('/api', createRoutes({ + authController, + chatController, + eventController, +})); + +// Health check +app.get('/health', (_, res) => { + res.json({ status: 'ok' }); }); -app.listen(port, () => { - console.log(`Example app listening on port ${port}`); -}); +// Start server +async function start() { + try { + await mongoose.connect(mongoUri); + console.log('Connected to MongoDB'); + + app.listen(port, () => { + console.log(`Server running on port ${port}`); + }); + } catch (error) { + console.error('Failed to start server:', error); + process.exit(1); + } +} + +start(); + +export default app; diff --git a/apps/server/src/controllers/AuthController.ts b/apps/server/src/controllers/AuthController.ts new file mode 100644 index 0000000..b1f6a1d --- /dev/null +++ b/apps/server/src/controllers/AuthController.ts @@ -0,0 +1,22 @@ +import { Request, Response } from 'express'; +import { AuthService } from '../services'; + +export class AuthController { + constructor(private authService: AuthService) {} + + async login(req: Request, res: Response): Promise { + throw new Error('Not implemented'); + } + + async register(req: Request, res: Response): Promise { + throw new Error('Not implemented'); + } + + async refresh(req: Request, res: Response): Promise { + throw new Error('Not implemented'); + } + + async logout(req: Request, res: Response): Promise { + throw new Error('Not implemented'); + } +} diff --git a/apps/server/src/controllers/ChatController.ts b/apps/server/src/controllers/ChatController.ts new file mode 100644 index 0000000..aac7d78 --- /dev/null +++ b/apps/server/src/controllers/ChatController.ts @@ -0,0 +1,27 @@ +import { Response } from 'express'; +import { ChatService } from '../services'; +import { AuthenticatedRequest } from '../middleware'; + +export class ChatController { + constructor(private chatService: ChatService) {} + + async sendMessage(req: AuthenticatedRequest, res: Response): Promise { + throw new Error('Not implemented'); + } + + async confirmEvent(req: AuthenticatedRequest, res: Response): Promise { + throw new Error('Not implemented'); + } + + async rejectEvent(req: AuthenticatedRequest, res: Response): Promise { + throw new Error('Not implemented'); + } + + async getConversations(req: AuthenticatedRequest, res: Response): Promise { + throw new Error('Not implemented'); + } + + async getConversation(req: AuthenticatedRequest, res: Response): Promise { + throw new Error('Not implemented'); + } +} diff --git a/apps/server/src/controllers/EventController.ts b/apps/server/src/controllers/EventController.ts new file mode 100644 index 0000000..d6ba38f --- /dev/null +++ b/apps/server/src/controllers/EventController.ts @@ -0,0 +1,31 @@ +import { Response } from 'express'; +import { EventService } from '../services'; +import { AuthenticatedRequest } from '../middleware'; + +export class EventController { + constructor(private eventService: EventService) {} + + async create(req: AuthenticatedRequest, res: Response): Promise { + throw new Error('Not implemented'); + } + + async getById(req: AuthenticatedRequest, res: Response): Promise { + throw new Error('Not implemented'); + } + + async getAll(req: AuthenticatedRequest, res: Response): Promise { + throw new Error('Not implemented'); + } + + async getByDateRange(req: AuthenticatedRequest, res: Response): Promise { + throw new Error('Not implemented'); + } + + async update(req: AuthenticatedRequest, res: Response): Promise { + throw new Error('Not implemented'); + } + + async delete(req: AuthenticatedRequest, res: Response): Promise { + throw new Error('Not implemented'); + } +} diff --git a/apps/server/src/controllers/index.ts b/apps/server/src/controllers/index.ts new file mode 100644 index 0000000..409a32e --- /dev/null +++ b/apps/server/src/controllers/index.ts @@ -0,0 +1,3 @@ +export * from './AuthController'; +export * from './ChatController'; +export * from './EventController'; diff --git a/apps/server/src/middleware/AuthMiddleware.ts b/apps/server/src/middleware/AuthMiddleware.ts new file mode 100644 index 0000000..7fda4dd --- /dev/null +++ b/apps/server/src/middleware/AuthMiddleware.ts @@ -0,0 +1,10 @@ +import { Request, Response, NextFunction } from 'express'; +import { verifyToken, TokenPayload } from '../utils/jwt'; + +export interface AuthenticatedRequest extends Request { + user?: TokenPayload; +} + +export function authenticate(req: AuthenticatedRequest, res: Response, next: NextFunction): void { + throw new Error('Not implemented'); +} diff --git a/apps/server/src/middleware/index.ts b/apps/server/src/middleware/index.ts new file mode 100644 index 0000000..f504f2f --- /dev/null +++ b/apps/server/src/middleware/index.ts @@ -0,0 +1 @@ +export * from './AuthMiddleware'; diff --git a/apps/server/src/repositories/index.ts b/apps/server/src/repositories/index.ts new file mode 100644 index 0000000..b9fe678 --- /dev/null +++ b/apps/server/src/repositories/index.ts @@ -0,0 +1 @@ +export * from './mongo'; diff --git a/apps/server/src/repositories/mongo/MongoChatRepository.ts b/apps/server/src/repositories/mongo/MongoChatRepository.ts new file mode 100644 index 0000000..250ca05 --- /dev/null +++ b/apps/server/src/repositories/mongo/MongoChatRepository.ts @@ -0,0 +1,23 @@ +import { ChatMessage, Conversation, CreateMessageDTO, GetMessagesOptions } from '@caldav/shared'; +import { ChatRepository } from '../../services/interfaces'; +import { ChatMessageModel, ConversationModel } from './models'; + +export class MongoChatRepository implements ChatRepository { + // Conversations + async getConversationsByUser(userId: string): Promise { + throw new Error('Not implemented'); + } + + async createConversation(userId: string): Promise { + throw new Error('Not implemented'); + } + + // Messages (cursor-based pagination) + async getMessages(conversationId: string, options?: GetMessagesOptions): Promise { + throw new Error('Not implemented'); + } + + async createMessage(conversationId: string, message: CreateMessageDTO): Promise { + throw new Error('Not implemented'); + } +} diff --git a/apps/server/src/repositories/mongo/MongoEventRepository.ts b/apps/server/src/repositories/mongo/MongoEventRepository.ts new file mode 100644 index 0000000..ff4f442 --- /dev/null +++ b/apps/server/src/repositories/mongo/MongoEventRepository.ts @@ -0,0 +1,29 @@ +import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from '@caldav/shared'; +import { EventRepository } from '../../services/interfaces'; +import { EventModel } from './models'; + +export class MongoEventRepository implements EventRepository { + async findById(id: string): Promise { + throw new Error('Not implemented'); + } + + async findByUserId(userId: string): Promise { + throw new Error('Not implemented'); + } + + async findByDateRange(userId: string, startDate: Date, endDate: Date): Promise { + throw new Error('Not implemented'); + } + + async create(userId: string, data: CreateEventDTO): Promise { + throw new Error('Not implemented'); + } + + async update(id: string, data: UpdateEventDTO): Promise { + throw new Error('Not implemented'); + } + + async delete(id: string): Promise { + throw new Error('Not implemented'); + } +} diff --git a/apps/server/src/repositories/mongo/MongoUserRepository.ts b/apps/server/src/repositories/mongo/MongoUserRepository.ts new file mode 100644 index 0000000..72e64ca --- /dev/null +++ b/apps/server/src/repositories/mongo/MongoUserRepository.ts @@ -0,0 +1,17 @@ +import { User } from '@caldav/shared'; +import { UserRepository, CreateUserData } from '../../services/interfaces'; +import { UserModel } from './models'; + +export class MongoUserRepository implements UserRepository { + async findById(id: string): Promise { + throw new Error('Not implemented'); + } + + async findByEmail(email: string): Promise { + throw new Error('Not implemented'); + } + + async create(data: CreateUserData): Promise { + throw new Error('Not implemented'); + } +} diff --git a/apps/server/src/repositories/mongo/index.ts b/apps/server/src/repositories/mongo/index.ts new file mode 100644 index 0000000..224258a --- /dev/null +++ b/apps/server/src/repositories/mongo/index.ts @@ -0,0 +1,3 @@ +export * from './MongoUserRepository'; +export * from './MongoEventRepository'; +export * from './MongoChatRepository'; diff --git a/apps/server/src/repositories/mongo/models/ChatModel.ts b/apps/server/src/repositories/mongo/models/ChatModel.ts new file mode 100644 index 0000000..5e9841d --- /dev/null +++ b/apps/server/src/repositories/mongo/models/ChatModel.ts @@ -0,0 +1,74 @@ +import mongoose, { Schema, Document } from 'mongoose'; +import { ChatMessage, Conversation, CreateEventDTO } from '@caldav/shared'; + +export interface ChatMessageDocument extends Omit, Document {} +export interface ConversationDocument extends Omit, Document {} + +const ProposedEventSchema = new Schema( + { + title: { type: String, required: true }, + description: { type: String }, + startTime: { type: Date, required: true }, + endTime: { type: Date, required: true }, + note: { type: String }, + isRecurring: { type: Boolean }, + recurrenceRule: { type: String }, + }, + { _id: false } +); + +const ChatMessageSchema = new Schema( + { + conversationId: { + type: String, + required: true, + }, + sender: { + type: String, + enum: ['user', 'assistant'], + required: true, + }, + content: { + type: String, + required: true, + }, + proposedEvent: { + type: ProposedEventSchema, + }, + }, + { + timestamps: true, + toJSON: { + transform: (_, ret: Record) => { + ret.id = String(ret._id); + delete ret._id; + delete ret.__v; + return ret; + }, + }, + } +); + +const ConversationSchema = new Schema( + { + userId: { + type: String, + required: true, + index: true, + }, + }, + { + timestamps: true, + toJSON: { + transform: (_, ret: Record) => { + ret.id = String(ret._id); + delete ret._id; + delete ret.__v; + return ret; + }, + }, + } +); + +export const ChatMessageModel = mongoose.model('ChatMessage', ChatMessageSchema); +export const ConversationModel = mongoose.model('Conversation', ConversationSchema); diff --git a/apps/server/src/repositories/mongo/models/EventModel.ts b/apps/server/src/repositories/mongo/models/EventModel.ts new file mode 100644 index 0000000..6544a2e --- /dev/null +++ b/apps/server/src/repositories/mongo/models/EventModel.ts @@ -0,0 +1,56 @@ +import mongoose, { Schema, Document } from 'mongoose'; +import { CalendarEvent } from '@caldav/shared'; + +export interface EventDocument extends Omit, Document {} + +const EventSchema = new Schema( + { + userId: { + type: String, + required: true, + index: true, + }, + title: { + type: String, + required: true, + trim: true, + }, + description: { + type: String, + trim: true, + }, + startTime: { + type: Date, + required: true, + }, + endTime: { + type: Date, + required: true, + }, + note: { + type: String, + }, + isRecurring: { + type: Boolean, + default: false, + }, + recurrenceRule: { + type: String, + }, + }, + { + timestamps: true, + toJSON: { + transform: (_, ret: Record) => { + ret.id = String(ret._id); + delete ret._id; + delete ret.__v; + return ret; + }, + }, + } +); + +EventSchema.index({ userId: 1, startTime: 1, endTime: 1 }); + +export const EventModel = mongoose.model('Event', EventSchema); diff --git a/apps/server/src/repositories/mongo/models/UserModel.ts b/apps/server/src/repositories/mongo/models/UserModel.ts new file mode 100644 index 0000000..c256ebc --- /dev/null +++ b/apps/server/src/repositories/mongo/models/UserModel.ts @@ -0,0 +1,39 @@ +import mongoose, { Schema, Document } from 'mongoose'; +import { User } from '@caldav/shared'; + +export interface UserDocument extends Omit, Document {} + +const UserSchema = new Schema( + { + email: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true, + }, + displayName: { + type: String, + required: true, + trim: true, + }, + passwordHash: { + type: String, + required: true, + }, + }, + { + timestamps: true, + toJSON: { + transform: (_, ret: Record) => { + ret.id = String(ret._id); + delete ret._id; + delete ret.__v; + delete ret.passwordHash; + return ret; + }, + }, + } +); + +export const UserModel = mongoose.model('User', UserSchema); diff --git a/apps/server/src/repositories/mongo/models/index.ts b/apps/server/src/repositories/mongo/models/index.ts new file mode 100644 index 0000000..6ef5ca1 --- /dev/null +++ b/apps/server/src/repositories/mongo/models/index.ts @@ -0,0 +1,3 @@ +export * from './UserModel'; +export * from './EventModel'; +export * from './ChatModel'; diff --git a/apps/server/src/routes/auth.routes.ts b/apps/server/src/routes/auth.routes.ts new file mode 100644 index 0000000..a3ac2bc --- /dev/null +++ b/apps/server/src/routes/auth.routes.ts @@ -0,0 +1,13 @@ +import { Router } from 'express'; +import { AuthController } from '../controllers'; + +export function createAuthRoutes(authController: AuthController): Router { + const router = Router(); + + router.post('/login', (req, res) => authController.login(req, res)); + router.post('/register', (req, res) => authController.register(req, res)); + router.post('/refresh', (req, res) => authController.refresh(req, res)); + router.post('/logout', (req, res) => authController.logout(req, res)); + + return router; +} diff --git a/apps/server/src/routes/chat.routes.ts b/apps/server/src/routes/chat.routes.ts new file mode 100644 index 0000000..ba48824 --- /dev/null +++ b/apps/server/src/routes/chat.routes.ts @@ -0,0 +1,17 @@ +import { Router } from 'express'; +import { ChatController } from '../controllers'; +import { authenticate } from '../middleware'; + +export function createChatRoutes(chatController: ChatController): Router { + const router = Router(); + + router.use(authenticate); + + router.post('/message', (req, res) => chatController.sendMessage(req, res)); + router.post('/confirm/:conversationId/:messageId', (req, res) => chatController.confirmEvent(req, res)); + router.post('/reject/:conversationId/:messageId', (req, res) => chatController.rejectEvent(req, res)); + router.get('/conversations', (req, res) => chatController.getConversations(req, res)); + router.get('/conversations/:id', (req, res) => chatController.getConversation(req, res)); + + return router; +} diff --git a/apps/server/src/routes/event.routes.ts b/apps/server/src/routes/event.routes.ts new file mode 100644 index 0000000..a7deb5f --- /dev/null +++ b/apps/server/src/routes/event.routes.ts @@ -0,0 +1,18 @@ +import { Router } from 'express'; +import { EventController } from '../controllers'; +import { authenticate } from '../middleware'; + +export function createEventRoutes(eventController: EventController): Router { + const router = Router(); + + router.use(authenticate); + + router.post('/', (req, res) => eventController.create(req, res)); + router.get('/', (req, res) => eventController.getAll(req, res)); + router.get('/range', (req, res) => eventController.getByDateRange(req, res)); + router.get('/:id', (req, res) => eventController.getById(req, res)); + router.put('/:id', (req, res) => eventController.update(req, res)); + router.delete('/:id', (req, res) => eventController.delete(req, res)); + + return router; +} diff --git a/apps/server/src/routes/index.ts b/apps/server/src/routes/index.ts new file mode 100644 index 0000000..86bcaac --- /dev/null +++ b/apps/server/src/routes/index.ts @@ -0,0 +1,25 @@ +import { Router } from 'express'; +import { createAuthRoutes } from './auth.routes'; +import { createChatRoutes } from './chat.routes'; +import { createEventRoutes } from './event.routes'; +import { AuthController, ChatController, EventController } from '../controllers'; + +export interface Controllers { + authController: AuthController; + chatController: ChatController; + eventController: EventController; +} + +export function createRoutes(controllers: Controllers): Router { + const router = Router(); + + router.use('/auth', createAuthRoutes(controllers.authController)); + router.use('/chat', createChatRoutes(controllers.chatController)); + router.use('/events', createEventRoutes(controllers.eventController)); + + return router; +} + +export * from './auth.routes'; +export * from './chat.routes'; +export * from './event.routes'; diff --git a/apps/server/src/services/AuthService.ts b/apps/server/src/services/AuthService.ts new file mode 100644 index 0000000..1e3da85 --- /dev/null +++ b/apps/server/src/services/AuthService.ts @@ -0,0 +1,24 @@ +import { User, CreateUserDTO, LoginDTO, AuthResponse } from '@caldav/shared'; +import { UserRepository } from './interfaces'; +import * as jwt from '../utils/jwt'; +import * as password from '../utils/password'; + +export class AuthService { + constructor(private userRepo: UserRepository) {} + + async login(data: LoginDTO): Promise { + throw new Error('Not implemented'); + } + + async register(data: CreateUserDTO): Promise { + throw new Error('Not implemented'); + } + + async refreshToken(refreshToken: string): Promise { + throw new Error('Not implemented'); + } + + async logout(userId: string): Promise { + throw new Error('Not implemented'); + } +} diff --git a/apps/server/src/services/ChatService.ts b/apps/server/src/services/ChatService.ts new file mode 100644 index 0000000..5adc9bd --- /dev/null +++ b/apps/server/src/services/ChatService.ts @@ -0,0 +1,30 @@ +import { ChatMessage, ChatResponse, SendMessageDTO, CalendarEvent, ConversationSummary, GetMessagesOptions } from '@caldav/shared'; +import { ChatRepository, EventRepository, AIProvider } from './interfaces'; + +export class ChatService { + constructor( + private chatRepo: ChatRepository, + private eventRepo: EventRepository, + private aiProvider: AIProvider + ) {} + + async processMessage(userId: string, data: SendMessageDTO): Promise { + throw new Error('Not implemented'); + } + + async confirmEvent(userId: string, conversationId: string, messageId: string): Promise { + throw new Error('Not implemented'); + } + + async rejectEvent(conversationId: string, messageId: string): Promise { + throw new Error('Not implemented'); + } + + async getConversations(userId: string): Promise { + throw new Error('Not implemented'); + } + + async getConversation(userId: string, conversationId: string, options?: GetMessagesOptions): Promise { + throw new Error('Not implemented'); + } +} diff --git a/apps/server/src/services/EventService.ts b/apps/server/src/services/EventService.ts new file mode 100644 index 0000000..acbace8 --- /dev/null +++ b/apps/server/src/services/EventService.ts @@ -0,0 +1,30 @@ +import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from '@caldav/shared'; +import { EventRepository } from './interfaces'; + +export class EventService { + constructor(private eventRepo: EventRepository) {} + + async create(userId: string, data: CreateEventDTO): Promise { + throw new Error('Not implemented'); + } + + async getById(id: string, userId: string): Promise { + throw new Error('Not implemented'); + } + + async getAll(userId: string): Promise { + throw new Error('Not implemented'); + } + + async getByDateRange(userId: string, startDate: Date, endDate: Date): Promise { + throw new Error('Not implemented'); + } + + async update(id: string, userId: string, data: UpdateEventDTO): Promise { + throw new Error('Not implemented'); + } + + async delete(id: string, userId: string): Promise { + throw new Error('Not implemented'); + } +} diff --git a/apps/server/src/services/index.ts b/apps/server/src/services/index.ts new file mode 100644 index 0000000..463f4c5 --- /dev/null +++ b/apps/server/src/services/index.ts @@ -0,0 +1,4 @@ +export * from './AuthService'; +export * from './ChatService'; +export * from './EventService'; +export * from './interfaces'; diff --git a/apps/server/src/services/interfaces/AIProvider.ts b/apps/server/src/services/interfaces/AIProvider.ts new file mode 100644 index 0000000..f3a8d0d --- /dev/null +++ b/apps/server/src/services/interfaces/AIProvider.ts @@ -0,0 +1,16 @@ +import { CalendarEvent, ChatMessage, CreateEventDTO } from '@caldav/shared'; + +export interface AIContext { + userId: string; + conversationHistory: ChatMessage[]; + existingEvents: CalendarEvent[]; +} + +export interface AIResponse { + content: string; + proposedEvent?: CreateEventDTO; +} + +export interface AIProvider { + processMessage(message: string, context: AIContext): Promise; +} diff --git a/apps/server/src/services/interfaces/ChatRepository.ts b/apps/server/src/services/interfaces/ChatRepository.ts new file mode 100644 index 0000000..ca95e5a --- /dev/null +++ b/apps/server/src/services/interfaces/ChatRepository.ts @@ -0,0 +1,11 @@ +import { ChatMessage, Conversation, CreateMessageDTO, GetMessagesOptions } from '@caldav/shared'; + +export interface ChatRepository { + // Conversations + getConversationsByUser(userId: string): Promise; + createConversation(userId: string): Promise; + + // Messages (cursor-based pagination) + getMessages(conversationId: string, options?: GetMessagesOptions): Promise; + createMessage(conversationId: string, message: CreateMessageDTO): Promise; +} diff --git a/apps/server/src/services/interfaces/EventRepository.ts b/apps/server/src/services/interfaces/EventRepository.ts new file mode 100644 index 0000000..5c4342d --- /dev/null +++ b/apps/server/src/services/interfaces/EventRepository.ts @@ -0,0 +1,10 @@ +import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from '@caldav/shared'; + +export interface EventRepository { + findById(id: string): Promise; + findByUserId(userId: string): Promise; + findByDateRange(userId: string, startDate: Date, endDate: Date): Promise; + create(userId: string, data: CreateEventDTO): Promise; + update(id: string, data: UpdateEventDTO): Promise; + delete(id: string): Promise; +} diff --git a/apps/server/src/services/interfaces/UserRepository.ts b/apps/server/src/services/interfaces/UserRepository.ts new file mode 100644 index 0000000..7103bfe --- /dev/null +++ b/apps/server/src/services/interfaces/UserRepository.ts @@ -0,0 +1,13 @@ +import { User } from '@caldav/shared'; + +export interface CreateUserData { + email: string; + displayName: string; + passwordHash: string; +} + +export interface UserRepository { + findById(id: string): Promise; + findByEmail(email: string): Promise; + create(data: CreateUserData): Promise; +} diff --git a/apps/server/src/services/interfaces/index.ts b/apps/server/src/services/interfaces/index.ts new file mode 100644 index 0000000..d568496 --- /dev/null +++ b/apps/server/src/services/interfaces/index.ts @@ -0,0 +1,4 @@ +export * from './AIProvider'; +export * from './UserRepository'; +export * from './EventRepository'; +export * from './ChatRepository'; diff --git a/apps/server/src/utils/index.ts b/apps/server/src/utils/index.ts new file mode 100644 index 0000000..41ed166 --- /dev/null +++ b/apps/server/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './jwt'; +export * from './password'; diff --git a/apps/server/src/utils/jwt.ts b/apps/server/src/utils/jwt.ts new file mode 100644 index 0000000..c1e571f --- /dev/null +++ b/apps/server/src/utils/jwt.ts @@ -0,0 +1,26 @@ +import jwt from 'jsonwebtoken'; + +export interface TokenPayload { + userId: string; + email: string; +} + +export interface JWTConfig { + secret: string; + expiresIn: string; +} + +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; +const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1h'; + +export function signToken(payload: TokenPayload): string { + throw new Error('Not implemented'); +} + +export function verifyToken(token: string): TokenPayload { + throw new Error('Not implemented'); +} + +export function decodeToken(token: string): TokenPayload | null { + throw new Error('Not implemented'); +} diff --git a/apps/server/src/utils/password.ts b/apps/server/src/utils/password.ts new file mode 100644 index 0000000..825c3d3 --- /dev/null +++ b/apps/server/src/utils/password.ts @@ -0,0 +1,11 @@ +import bcrypt from 'bcrypt'; + +const SALT_ROUNDS = 10; + +export async function hash(password: string): Promise { + throw new Error('Not implemented'); +} + +export async function compare(password: string, hash: string): Promise { + throw new Error('Not implemented'); +} diff --git a/docs/Backend Klassendiagramm.png b/docs/Backend Klassendiagramm.png new file mode 100644 index 0000000..2fa437d Binary files /dev/null and b/docs/Backend Klassendiagramm.png differ diff --git a/docs/Frontend Klassendiagramm.png b/docs/Frontend Klassendiagramm.png new file mode 100644 index 0000000..4259039 Binary files /dev/null and b/docs/Frontend Klassendiagramm.png differ diff --git a/docs/System Komponenten.png b/docs/System Komponenten.png new file mode 100644 index 0000000..068df39 Binary files /dev/null and b/docs/System Komponenten.png differ diff --git a/docs/architecture-class-diagram.puml b/docs/architecture-class-diagram.puml index 19fa1b9..27aadcb 100644 --- a/docs/architecture-class-diagram.puml +++ b/docs/architecture-class-diagram.puml @@ -38,7 +38,7 @@ package "Controller Layer" #ADD8E6 { } package "Service Layer" #90EE90 { - package "Interfaces" { + package "Data Access Interfaces" { interface AIProvider { ' +processMessage() } @@ -94,7 +94,7 @@ package "AI Layer" #FFA07A { } } -package "Data Access Layer" #FFD700 { +package "Data Access Implementations" #FFD700 { class MongoUserRepository implements UserRepository { ' -model: UserModel } diff --git a/package-lock.json b/package-lock.json index b51015d..cdfa599 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,13 +60,20 @@ "name": "@caldav/server", "version": "1.0.0", "dependencies": { + "@anthropic-ai/sdk": "^0.71.2", "@caldav/shared": "*", - "express": "^5.2.1" + "bcrypt": "^6.0.0", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "mongoose": "^9.1.1" }, "devDependencies": { + "@types/bcrypt": "^6.0.0", "@types/express": "^5.0.6", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.10.1", - "tsx": "^4.21.0" + "tsx": "^4.21.0", + "typescript": "^5.9.3" } }, "node_modules/@0no-co/graphql.web": { @@ -95,6 +102,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.71.2", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.71.2.tgz", + "integrity": "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -3239,6 +3266,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.4.tgz", + "integrity": "sha512-p7X/ytJDIdwUfFL/CLOhKgdfJe1Fa8uw9seJYvdOmnP9JBWGWHW69HkOixXS6Wy9yvGf1MbhcS6lVmrhy4jm2g==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -3935,6 +3971,16 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -4048,6 +4094,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.10.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", @@ -4108,6 +4172,21 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "license": "MIT" }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -5342,6 +5421,20 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/better-opn": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", @@ -5513,6 +5606,15 @@ "node-int64": "^0.4.0" } }, + "node_modules/bson": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.0.0.tgz", + "integrity": "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -5537,6 +5639,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6365,6 +6473,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -9449,6 +9566,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -9475,6 +9605,40 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -9491,6 +9655,36 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.0.0.tgz", + "integrity": "sha512-RKhaOBSPN8L7y4yAgNhDT2602G5FD6QbOIISbjN9D6mjHPeqeg7K+EB5IGSU5o81/X2Gzm3ICnAvQW3x3OP8HA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9855,6 +10049,42 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9862,6 +10092,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", @@ -10011,6 +10247,12 @@ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -10494,6 +10736,138 @@ "node": ">=10" } }, + "node_modules/mongodb": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", + "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.0.0", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.0.tgz", + "integrity": "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mongoose": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.1.1.tgz", + "integrity": "sha512-/CgKSAmjzgIj4o1FyWZIpQ2rmUQlhalwWd4l/Ht3XUVWscHRHev1TIwKz1ADNC5tHHyOQEaUDkEh0jI91c4Ydw==", + "license": "MIT", + "dependencies": { + "kareem": "3.0.0", + "mongodb": "~7.0", + "mpath": "0.9.0", + "mquery": "6.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz", + "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10584,6 +10958,15 @@ "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==", "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -10613,6 +10996,17 @@ "node": ">= 6.13.0" } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -13224,6 +13618,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -13323,6 +13723,15 @@ "node": ">=0.10.0" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/split-on-first": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", @@ -13907,6 +14316,12 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", diff --git a/packages/shared/package.json b/packages/shared/package.json index ddb0b4b..1ac2404 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -2,7 +2,10 @@ "name": "@caldav/shared", "version": "1.0.0", "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts", "exports": { + ".": "./src/index.ts", "./*": "./src/*" } } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..e9644da --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1 @@ +export * from './models'; diff --git a/packages/shared/src/models/CalendarEvent.ts b/packages/shared/src/models/CalendarEvent.ts new file mode 100644 index 0000000..4e59e3d --- /dev/null +++ b/packages/shared/src/models/CalendarEvent.ts @@ -0,0 +1,33 @@ +export interface CalendarEvent { + id: string; + userId: string; + title: string; + description?: string; + startTime: Date; + endTime: Date; + note?: string; + isRecurring?: boolean; + recurrenceRule?: string; + createdAt?: Date; + updatedAt?: Date; +} + +export interface CreateEventDTO { + title: string; + description?: string; + startTime: Date; + endTime: Date; + note?: string; + isRecurring?: boolean; + recurrenceRule?: string; +} + +export interface UpdateEventDTO { + title?: string; + description?: string; + startTime?: Date; + endTime?: Date; + note?: string; + isRecurring?: boolean; + recurrenceRule?: string; +} diff --git a/packages/shared/src/models/ChatMessage.ts b/packages/shared/src/models/ChatMessage.ts new file mode 100644 index 0000000..2ddec96 --- /dev/null +++ b/packages/shared/src/models/ChatMessage.ts @@ -0,0 +1,46 @@ +import { CreateEventDTO } from './CalendarEvent'; + +export type MessageSender = 'user' | 'assistant'; + +export interface ChatMessage { + id: string; + conversationId: string; + sender: MessageSender; + content: string; + proposedEvent?: CreateEventDTO; + createdAt?: Date; +} + +export interface Conversation { + id: string; + userId: string; + createdAt?: Date; + updatedAt?: Date; +} + +export interface SendMessageDTO { + conversationId?: string; + content: string; +} + +export interface CreateMessageDTO { + sender: MessageSender; + content: string; + proposedEvent?: CreateEventDTO; +} + +export interface GetMessagesOptions { + before?: string; // Message ID - load messages before this one + limit?: number; // Default: 20 +} + +export interface ChatResponse { + message: ChatMessage; + conversationId: string; +} + +export interface ConversationSummary { + id: string; + lastMessage?: ChatMessage; + createdAt?: Date; +} diff --git a/packages/shared/src/models/User.ts b/packages/shared/src/models/User.ts new file mode 100644 index 0000000..30485c0 --- /dev/null +++ b/packages/shared/src/models/User.ts @@ -0,0 +1,25 @@ +export interface User { + id: string; + email: string; + displayName: string; + passwordHash?: string; + createdAt?: Date; + updatedAt?: Date; +} + +export interface CreateUserDTO { + email: string; + displayName: string; + password: string; +} + +export interface LoginDTO { + email: string; + password: string; +} + +export interface AuthResponse { + user: Omit; + accessToken: string; + refreshToken?: string; +} diff --git a/packages/shared/src/models/index.ts b/packages/shared/src/models/index.ts new file mode 100644 index 0000000..f00dc4c --- /dev/null +++ b/packages/shared/src/models/index.ts @@ -0,0 +1,3 @@ +export * from './User'; +export * from './CalendarEvent'; +export * from './ChatMessage';