Compare commits

...

20 Commits

Author SHA1 Message Date
d7902deeb4 pipeline
Some checks failed
continuous-integration/drone/push Build was killed
continuous-integration/drone/tag Build is passing
2026-02-25 20:09:52 +01:00
7fefb9a153 pino shoudn't be a dev dependency; hopefully fixed pipeline
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is passing
2026-02-25 19:08:32 +01:00
565cb0a044 sanitize tag-names for kubernetes
Some checks failed
continuous-integration/drone/push Build was killed
continuous-integration/drone/tag Build is failing
2026-02-25 18:39:00 +01:00
6463100fbd feat: restore CI pipelines and add k3s deployment
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
Re-enable build/test/format pipelines, rename deploy_server to
deploy_latest, add upload_tag (tag-triggered k3s deploy) and
upload_commit (promote-triggered k3s deploy). Update CLAUDE.md.
2026-02-25 17:58:03 +01:00
b088e380a4 typo
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is passing
2026-02-24 18:11:56 +01:00
54936f1b96 added tsconfig.json in Dockerfile
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-24 18:08:59 +01:00
e732305d99 feat: add deploy pipeline and switch Dockerfile to COPY-based build
Some checks failed
continuous-integration/drone/push Build is failing
Add deploy_server Drone pipeline that builds and pushes the Docker image
to Gitea Container Registry, then deploys to VPS via SSH. Switch
Dockerfile from git clone to COPY-based build for CI compatibility and
better layer caching. Change exposed port to 3001.
2026-02-24 17:52:48 +01:00
93a0928928 hopefully final pipeline fix for now
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-24 12:57:35 +01:00
68a49712bc another pipeline fix
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-02-24 12:52:09 +01:00
602e4e1413 added types in pipeline
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-02-24 12:47:14 +01:00
bf8bb3cfb8 feat: add Drone CI pipelines, Jest unit tests, and Prettier check
Some checks failed
continuous-integration/drone/push Build encountered an error
Add Drone CI with server build/test and format check pipelines.
Add unit tests for password utils and recurrenceExpander.
Add check_format script, fix Jest config to ignore dist/,
remove dead CaldavService.test.ts, apply Prettier formatting.
2026-02-24 12:43:31 +01:00
16848bfdf0 refactor: clone repo from Gitea in Dockerfile instead of COPY
Replace local COPY with git clone --depth 1 so the image can be built
without a local source context. Adds BRANCH build arg (default: main).
2026-02-18 20:12:05 +01:00
a3e7f0288e feat: add Docker support and compile shared package to dist
- Add multi-stage Dockerfile for server containerization
- Add .dockerignore to exclude unnecessary files from build context
- Switch shared package from source to compiled CommonJS output (dist/)
- Server dev/build scripts now build shared package first
- Fix deep imports to use @calchat/shared barrel export
- Update CLAUDE.md with Docker and shared package documentation
2026-02-18 19:37:27 +01:00
0c157da817 update README 2026-02-10 01:10:44 +01:00
e5cd64367d feat: add sync and logout toolbar to calendar screen
- Add CalendarToolbar component between header and weekdays in calendar.tsx
- Sync button with CalDAV sync, spinner during sync, green checkmark on success, red X on error (3s feedback)
- Sync button disabled/greyed out when no CalDAV config present
- Logout button with redirect to login screen
- Buttons styled with border and shadow
- Update CLAUDE.md with CalendarToolbar documentation
2026-02-09 23:51:43 +01:00
b9ffc6c908 refactor: reduce CalDAV sync to login and manual sync button only
- Remove auto-login sync in AuthGuard
- Remove 10s interval sync and syncAndReload in calendar tab
- Remove lazy syncOnce pattern in ChatService AI callbacks
- Remove CaldavService dependency from ChatService constructor
2026-02-09 23:32:04 +01:00
5a9485acfc fix: use pino err key for proper Error serialization in controllers
Error objects logged as { error } were serialized as {} because pino
only applies its error serializer to the err key.
2026-02-09 22:41:46 +01:00
189c38dc2b docs: clean up frontend class diagram layout
Comment out service methods for consistency with stores and switch to
left-to-right direction for a more vertical package arrangement.
2026-02-09 22:00:13 +01:00
73e768a0ad refactor: remove all JWT-related code and references
JWT was never used - auth uses X-User-Id header. Removes jwt.ts utility,
jsonwebtoken dependency, stubbed refresh/logout endpoints, and updates
all docs (PUML diagrams, api-routes, tex, CLAUDE.md) accordingly.
2026-02-09 20:02:05 +01:00
cb32bd23ca docs: add .env.example files for client and server 2026-02-09 19:57:55 +01:00
37 changed files with 954 additions and 363 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
node_modules
*/node_modules
*/*/node_modules
**/dist
apps/client
.git
.env
*.md

171
.drone.yml Normal file
View File

@@ -0,0 +1,171 @@
kind: pipeline
type: docker
name: server_build_and_test
trigger:
branch:
- main
event:
- push
steps:
- name: build_server
image: node
commands:
- npm ci -w @calchat/shared
- npm ci -w @calchat/server
- npm run build -w @calchat/server
- name: jest_server
image: node
commands:
- npm run test -w @calchat/server
---
kind: pipeline
type: docker
name: check_for_formatting
trigger:
branch:
- main
event:
- push
steps:
- name: format_check
image: node
commands:
- npm ci
- npm run check_format
---
kind: pipeline
type: docker
name: deploy_latest
trigger:
branch:
- main
event:
- push
steps:
- name: upload_latest
image: plugins/docker
settings:
registry: gitea.gilmour109.de
repo: gitea.gilmour109.de/gilmour109/calchat-server
dockerfile: apps/server/docker/Dockerfile
tags:
- latest
username:
from_secret: gitea_username
password:
from_secret: gitea_password
- name: deploy_to_vps
image: appleboy/drone-ssh
settings:
host:
- 10.0.0.1
username: root
password:
from_secret: vps_ssh_password
envs:
- gitea_username
- gitea_password
port: 22
command_timeout: 2m
script:
- docker login -u $GITEA_USERNAME -p $GITEA_PASSWORD gitea.gilmour109.de
- docker pull gitea.gilmour109.de/gilmour109/calchat-server:latest
- docker compose -f /root/calchat-mongo/docker-compose.yml up -d
depends_on:
- server_build_and_test
- check_for_formatting
---
kind: pipeline
type: docker
name: upload_tag
trigger:
event:
- tag
steps:
- name: upload_tag
image: plugins/docker
settings:
registry: gitea.gilmour109.de
repo: gitea.gilmour109.de/gilmour109/calchat-server
dockerfile: apps/server/docker/Dockerfile
tags:
- ${DRONE_TAG}
username:
from_secret: gitea_username
password:
from_secret: gitea_password
- name: deploy_to_k3s
image: appleboy/drone-ssh
settings:
host:
- 192.168.178.201
username: debian
password:
from_secret: k3s_ssh_password
envs:
- drone_tag
port: 22
command_timeout: 2m
script:
- export TAG=$DRONE_TAG
- export NAME=$(echo $DRONE_TAG | tr -d '.')
- envsubst < /home/debian/manifest.yml | sudo kubectl apply -f -
---
kind: pipeline
type: docker
name: upload_commit
trigger:
event:
- promote
steps:
- name: upload_commit
image: plugins/docker
settings:
registry: gitea.gilmour109.de
repo: gitea.gilmour109.de/gilmour109/calchat-server
dockerfile: apps/server/docker/Dockerfile
tags:
- ${DRONE_COMMIT_SHA:0:8}
username:
from_secret: gitea_username
password:
from_secret: gitea_password
- name: deploy_to_k3s
image: appleboy/drone-ssh
settings:
host:
- 192.168.178.201
username: debian
password:
from_secret: k3s_ssh_password
envs:
- drone_commit_sha
port: 22
command_timeout: 2m
script:
- export TAG=$(echo $DRONE_COMMIT_SHA | cut -c1-8)
- export NAME=$TAG
- envsubst < /home/debian/manifest.yml | sudo kubectl apply -f -

View File

@@ -14,6 +14,7 @@ This is a fullstack TypeScript monorepo with npm workspaces.
```bash ```bash
npm install # Install all dependencies for all workspaces npm install # Install all dependencies for all workspaces
npm run format # Format all TypeScript files with Prettier npm run format # Format all TypeScript files with Prettier
npm run check_format # Check formatting without modifying files (used in CI)
``` ```
### Client (apps/client) - Expo React Native app ### Client (apps/client) - Expo React Native app
@@ -26,11 +27,17 @@ npm run lint -w @calchat/client # Run ESLint
npm run build:apk -w @calchat/client # Build APK locally with EAS npm run build:apk -w @calchat/client # Build APK locally with EAS
``` ```
### Shared (packages/shared)
```bash
npm run build -w @calchat/shared # Compile shared types to dist/
```
### Server (apps/server) - Express.js backend ### Server (apps/server) - Express.js backend
```bash ```bash
npm run dev -w @calchat/server # Start dev server with hot reload (tsx watch) npm run dev -w @calchat/server # Build shared + start dev server with hot reload (tsx watch)
npm run build -w @calchat/server # Compile TypeScript npm run build -w @calchat/server # Build shared + compile TypeScript
npm run start -w @calchat/server # Run compiled server (port 3000) npm run start -w @calchat/server # Run compiled server (port 3000)
npm run test -w @calchat/server # Run Jest unit tests
``` ```
## Technology Stack ## Technology Stack
@@ -48,11 +55,14 @@ npm run start -w @calchat/server # Run compiled server (port 3000)
| | MongoDB | Database | | | MongoDB | Database |
| | Mongoose | ODM | | | Mongoose | ODM |
| | GPT (OpenAI) | AI/LLM for chat | | | GPT (OpenAI) | AI/LLM for chat |
| | X-User-Id Header | Authentication (simple, no JWT yet) | | | X-User-Id Header | Authentication |
| | pino / pino-http | Structured logging | | | pino / pino-http | Structured logging |
| | react-native-logs | Client-side logging | | | react-native-logs | Client-side logging |
| | tsdav | CalDAV client library | | | tsdav | CalDAV client library |
| | ical.js | iCalendar parsing/generation | | | ical.js | iCalendar parsing/generation |
| Testing | Jest / ts-jest | Server unit tests |
| Deployment | Docker | Server containerization (multi-stage build) |
| | Drone CI | CI/CD pipelines (build, test, format check, deploy) |
| Planned | iCalendar | Event export/import | | Planned | iCalendar | Event export/import |
## Architecture ## Architecture
@@ -76,7 +86,7 @@ src/
│ ├── (tabs)/ # Tab navigation group │ ├── (tabs)/ # Tab navigation group
│ │ ├── _layout.tsx # Tab bar configuration (themed) │ │ ├── _layout.tsx # Tab bar configuration (themed)
│ │ ├── chat.tsx # Chat screen (AI conversation) │ │ ├── chat.tsx # Chat screen (AI conversation)
│ │ ├── calendar.tsx # Calendar overview │ │ ├── calendar.tsx # Calendar overview (with CalendarToolbar: sync + logout)
│ │ └── settings.tsx # Settings screen (theme switcher, logout, CalDAV config with feedback) │ │ └── settings.tsx # Settings screen (theme switcher, logout, CalDAV config with feedback)
│ ├── editEvent.tsx # Event edit screen (dual-mode: calendar/chat) │ ├── editEvent.tsx # Event edit screen (dual-mode: calendar/chat)
│ ├── event/ │ ├── event/
@@ -235,7 +245,7 @@ CardBase
src/ src/
├── app.ts # Entry point, DI setup, Express config ├── app.ts # Entry point, DI setup, Express config
├── controllers/ # Request handlers + middleware (per architecture diagram) ├── controllers/ # Request handlers + middleware (per architecture diagram)
│ ├── AuthController.ts # login(), register(), refresh(), logout() │ ├── AuthController.ts # login(), register()
│ ├── ChatController.ts # sendMessage(), confirmEvent() + CalDAV push, rejectEvent(), getConversations(), getConversation(), updateProposalEvent() │ ├── ChatController.ts # sendMessage(), confirmEvent() + CalDAV push, rejectEvent(), getConversations(), getConversation(), updateProposalEvent()
│ ├── EventController.ts # create(), getById(), getAll(), getByDateRange(), update(), delete() - pushes/deletes to CalDAV on mutations │ ├── EventController.ts # create(), getById(), getAll(), getByDateRange(), update(), delete() - pushes/deletes to CalDAV on mutations
│ ├── CaldavController.ts # saveConfig(), loadConfig(), deleteConfig(), pullEvents(), pushEvents(), pushEvent() │ ├── CaldavController.ts # saveConfig(), loadConfig(), deleteConfig(), pullEvents(), pushEvents(), pushEvent()
@@ -285,7 +295,6 @@ src/
│ ├── toolDefinitions.ts # TOOL_DEFINITIONS - provider-agnostic tool specs │ ├── toolDefinitions.ts # TOOL_DEFINITIONS - provider-agnostic tool specs
│ └── toolExecutor.ts # executeToolCall() - handles getDay, proposeCreate/Update/Delete, searchEvents, getEventsInRange │ └── toolExecutor.ts # executeToolCall() - handles getDay, proposeCreate/Update/Delete, searchEvents, getEventsInRange
├── utils/ ├── utils/
│ ├── jwt.ts # signToken(), verifyToken() - NOT USED YET (no JWT)
│ ├── password.ts # hash(), compare() using bcrypt │ ├── password.ts # hash(), compare() using bcrypt
│ ├── eventFormatters.ts # getWeeksOverview(), getMonthOverview() - formatted event listings │ ├── eventFormatters.ts # getWeeksOverview(), getMonthOverview() - formatted event listings
│ └── recurrenceExpander.ts # expandRecurringEvents() - expand recurring events into occurrences │ └── recurrenceExpander.ts # expandRecurringEvents() - expand recurring events into occurrences
@@ -296,8 +305,6 @@ src/
**API Endpoints:** **API Endpoints:**
- `POST /api/auth/login` - User login - `POST /api/auth/login` - User login
- `POST /api/auth/register` - User registration - `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` - Get all events (protected)
- `GET /api/events/range` - Get events by date range (protected) - `GET /api/events/range` - Get events by date range (protected)
- `GET /api/events/:id` - Get single event (protected) - `GET /api/events/:id` - Get single event (protected)
@@ -321,6 +328,8 @@ src/
### Shared Package (packages/shared) ### Shared Package (packages/shared)
The shared package is compiled to `dist/` (CommonJS). All imports must use `@calchat/shared` (NOT `@calchat/shared/src/...`). Server `dev` and `build` scripts automatically build shared first.
``` ```
src/ src/
├── index.ts ├── index.ts
@@ -533,11 +542,17 @@ docker compose up -d # Start Radicale CalDAV server
``` ```
- Radicale: `localhost:5232` - Radicale: `localhost:5232`
### Server Docker Image
```bash
# Build (requires local build context):
docker build -f apps/server/docker/Dockerfile -t calchat-server .
docker run -p 3001:3001 --env-file apps/server/.env calchat-server
```
Multi-stage COPY-based build: copies `package.json` files first for layer caching, then source code. Compiles shared + server, then copies only `dist/` and production dependencies to the runtime stage. Exposes port 3001. In CI, the `plugins/docker` Drone plugin builds and pushes the image automatically.
### Environment Variables ### Environment Variables
Server requires `.env` file in `apps/server/`: Server requires `.env` file in `apps/server/`:
``` ```
JWT_SECRET=your-secret-key
JWT_EXPIRES_IN=1h
MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
OPENAI_API_KEY=sk-proj-... OPENAI_API_KEY=sk-proj-...
USE_TEST_RESPONSES=false # true = static test responses, false = real GPT AI USE_TEST_RESPONSES=false # true = static test responses, false = real GPT AI
@@ -585,10 +600,6 @@ NODE_ENV=development # development = pretty logs, production = JSON
- `EventService`: Extended with searchByTitle(), findByCaldavUUID() - `EventService`: Extended with searchByTitle(), findByCaldavUUID()
- `utils/eventFormatters`: Refactored to use EventService instead of EventRepository - `utils/eventFormatters`: Refactored to use EventService instead of EventRepository
- CORS configured to allow X-User-Id header - CORS configured to allow X-User-Id header
- **Stubbed (TODO):**
- `AuthController`: refresh(), logout()
- `AuthService`: refreshToken()
- JWT authentication (currently using simple X-User-Id header)
**Shared:** **Shared:**
- Types, DTOs, constants (Day, Month with German translations), ExpandedEvent type, CaldavConfig, CaldavSyncStatus defined and exported - Types, DTOs, constants (Day, Month with German translations), ExpandedEvent type, CaldavConfig, CaldavSyncStatus defined and exported
@@ -646,6 +657,7 @@ NODE_ENV=development # development = pretty logs, production = JSON
- `EventCard`: Uses EventCardBase + edit/delete buttons (TouchableOpacity with delayPressIn for scroll-friendly touch handling) - `EventCard`: Uses EventCardBase + edit/delete buttons (TouchableOpacity with delayPressIn for scroll-friendly touch handling)
- `ProposedEventCard`: Uses EventCardBase + confirm/reject/edit buttons for chat proposals, displays green highlighted text for new changes ("Neue Ausnahme: [date]" for single delete, "Neues Ende: [date]" for UNTIL updates), shows yellow conflict warnings when proposed time overlaps with existing events. Edit button allows modifying proposals before confirming. - `ProposedEventCard`: Uses EventCardBase + confirm/reject/edit buttons for chat proposals, displays green highlighted text for new changes ("Neue Ausnahme: [date]" for single delete, "Neues Ende: [date]" for UNTIL updates), shows yellow conflict warnings when proposed time overlaps with existing events. Edit button allows modifying proposals before confirming.
- `DeleteEventModal`: Delete confirmation modal using ModalBase - shows three options for recurring events (single/future/all), simple confirm for non-recurring - `DeleteEventModal`: Delete confirmation modal using ModalBase - shows three options for recurring events (single/future/all), simple confirm for non-recurring
- `CalendarToolbar` (in calendar.tsx): Toolbar between header and weekdays with Sync button (CalDAV sync with spinner/green checkmark/red X feedback, disabled without config) and Logout button
- `EventOverlay` (in calendar.tsx): Day events overlay using ModalBase - shows EventCards for selected day - `EventOverlay` (in calendar.tsx): Day events overlay using ModalBase - shows EventCards for selected day
- `Themes.tsx`: Theme definitions with THEMES object (defaultLight, defaultDark) including all color tokens (textPrimary, borderPrimary, eventIndicator, secondaryBg, shadowColor, etc.) - `Themes.tsx`: Theme definitions with THEMES object (defaultLight, defaultDark) including all color tokens (textPrimary, borderPrimary, eventIndicator, secondaryBg, shadowColor, etc.)
- `EventsStore`: Zustand store with setEvents(), addEvent(), updateEvent(), deleteEvent() - stores ExpandedEvent[], preloaded by AuthGuard - `EventsStore`: Zustand store with setEvents(), addEvent(), updateEvent(), deleteEvent() - stores ExpandedEvent[], preloaded by AuthGuard
@@ -684,6 +696,29 @@ This uses the `preview` profile from `eas.json` which builds an APK with:
- Package name: `com.gilmour109.calchat` - Package name: `com.gilmour109.calchat`
- EAS Project ID: `b722dde6-7d89-48ff-9095-e007e7c7da87` - EAS Project ID: `b722dde6-7d89-48ff-9095-e007e7c7da87`
## CI/CD (Drone)
The project uses Drone CI (`.drone.yml`) with five pipelines:
**On push to main:**
1. **`server_build_and_test`**: Builds the server (`npm ci` + `npm run build`) and runs Jest tests (`npm run test`)
2. **`check_for_formatting`**: Checks Prettier formatting across all workspaces (`npm run check_format`)
3. **`deploy_latest`**: Builds Docker image, pushes to Gitea Container Registry (`gitea.gilmour109.de/gilmour109/calchat-server:latest`), then SSHs into VPS (`10.0.0.1`) to pull and restart via `docker compose`. Depends on both pipelines above passing first.
**On tag:**
4. **`upload_tag`**: Builds Docker image tagged with the git tag (`${DRONE_TAG}`), pushes to registry, then deploys to k3s cluster (`192.168.178.201`) via SSH using `envsubst` with a Kubernetes manifest template.
**On promote:**
5. **`upload_commit`**: Builds Docker image tagged with short commit SHA (first 8 chars), pushes to registry, then deploys to k3s cluster (`192.168.178.201`) via SSH using `envsubst` with a Kubernetes manifest template.
## Testing
Server uses Jest with ts-jest for unit testing. Config in `apps/server/jest.config.js` ignores `/node_modules/` and `/dist/`.
**Existing tests:**
- `src/utils/password.test.ts` - Tests for bcrypt hash() and compare()
- `src/utils/recurrenceExpander.test.ts` - Tests for expandRecurringEvents() (non-recurring, weekly/daily/UNTIL recurrence, EXDATE filtering, RRULE: prefix stripping, invalid RRULE fallback, multi-day events, sorting)
## Documentation ## Documentation
Detailed architecture diagrams are in `docs/`: Detailed architecture diagrams are in `docs/`:

157
README.md
View File

@@ -1,50 +1,141 @@
# Welcome to your Expo app 👋 # CalChat
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). Kalender-App mit KI-Chatbot. Termine lassen sich per Chat in natuerlicher Sprache erstellen, bearbeiten und loeschen.
## Get started ## Tech Stack
1. Install dependencies | Bereich | Technologie |
|---------|-------------|
| Frontend | React Native, Expo, Expo-Router, NativeWind, Zustand |
| Backend | Express.js, MongoDB, Mongoose, OpenAI GPT |
| Shared | TypeScript Monorepo mit npm Workspaces |
| Optional | CalDAV-Sync (z.B. Radicale) |
```bash ## Voraussetzungen
npm install
```
2. Start the app - Node.js (>= 20)
- npm
- Docker & Docker Compose (fuer MongoDB)
- OpenAI API Key (fuer KI-Chat)
- Android SDK + Java (nur fuer APK-Build)
```bash ## Projekt aufsetzen
npx expo start
```
In the output, you'll find options to open the app in a ### 1. Repository klonen
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
## Get a fresh project
When you're ready, run:
```bash ```bash
npm run reset-project git clone <repo-url>
cd calchat
``` ```
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. ### 2. Dependencies installieren
## Learn more ```bash
npm install
```
To learn more about developing your project with Expo, look at the following resources: Installiert alle Dependencies fuer Client, Server und Shared.
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). ### 3. MongoDB starten
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
## Join the community ```bash
cd apps/server/docker/mongo
docker compose up -d
```
Join our community of developers creating universal apps. - MongoDB: `localhost:27017` (root/mongoose)
- Mongo Express UI: `localhost:8083` (admin/admin)
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. ### 4. Server konfigurieren
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
```bash
cp apps/server/.env.example apps/server/.env
```
`apps/server/.env` bearbeiten:
```env
MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
OPENAI_API_KEY=sk-proj-... # Eigenen Key eintragen
USE_TEST_RESPONSES=false # true = statische Testantworten ohne GPT
LOG_LEVEL=debug
NODE_ENV=development
PORT=3000
```
### 5. Client konfigurieren
```bash
cp apps/client/.env.example apps/client/.env
```
`apps/client/.env` bearbeiten:
```env
# Fuer Emulator/Web:
EXPO_PUBLIC_API_URL=http://localhost:3000/api
# Fuer physisches Geraet im gleichen Netzwerk:
EXPO_PUBLIC_API_URL=http://<DEINE-LOKALE-IP>:3000/api
```
### 6. Server starten
```bash
npm run dev -w @calchat/server
```
Startet den Server auf Port 3000 (mit `tsx watch` - startet bei Dateiänderungen automatisch neu (oder sollte es zumindest)).
### 7. Client starten
```bash
npm run start -w @calchat/client
```
Dann im Expo-Menue die gewuenschte Plattform waehlen:
- `a` - Android Emulator
- `i` - iOS Simulator
- `w` - Web Browser
Oder direkt:
```bash
npm run android -w @calchat/client
npm run ios -w @calchat/client
npm run web -w @calchat/client
```
## CalDAV (optional)
Fuer CalDAV-Synchronisation kann ein Radicale-Server gestartet werden:
```bash
cd apps/server/docker/radicale
docker compose up -d
```
Radicale ist dann unter `localhost:5232` erreichbar. Die CalDAV-Verbindung wird in der App unter Einstellungen konfiguriert.
## Weitere Befehle
```bash
npm run format # Prettier auf alle TS/TSX-Dateien
npm run lint -w @calchat/client # ESLint (Client)
npm run build -w @calchat/server # TypeScript kompilieren (Server)
npm run build:apk -w @calchat/client # APK lokal bauen (EAS)
```
## Projektstruktur
```
calchat/
├── apps/
│ ├── client/ # Expo React Native App
│ └── server/ # Express.js Backend
│ └── docker/
│ ├── mongo/ # MongoDB + Mongo Express
│ └── radicale/ # CalDAV Server
└── packages/
└── shared/ # Geteilte Types und Utilities
```

8
apps/client/.env.example Normal file
View File

@@ -0,0 +1,8 @@
# Base URL of the CalChat API server
# Must include the /api path suffix
# Use your local network IP for mobile device testing, or localhost for emulator/web
# Examples:
# http://192.168.178.22:3001/api (local network, for physical device)
# http://localhost:3001/api (emulator or web)
# https://calchat.example.com/api (production)
EXPO_PUBLIC_API_URL=http://localhost:3001/api

View File

@@ -1,4 +1,4 @@
import { Pressable, Text, View } from "react-native"; import { ActivityIndicator, Pressable, Text, View } from "react-native";
import { import {
DAYS, DAYS,
MONTHS, MONTHS,
@@ -16,10 +16,11 @@ import { router, useFocusEffect } from "expo-router";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { useThemeStore } from "../../stores/ThemeStore"; import { useThemeStore } from "../../stores/ThemeStore";
import BaseBackground from "../../components/BaseBackground"; import BaseBackground from "../../components/BaseBackground";
import { EventService } from "../../services"; import { AuthService, EventService } from "../../services";
import { CaldavConfigService } from "../../services/CaldavConfigService";
import { useEventsStore } from "../../stores"; import { useEventsStore } from "../../stores";
import { useDropdownPosition } from "../../hooks/useDropdownPosition"; import { useDropdownPosition } from "../../hooks/useDropdownPosition";
import { CaldavConfigService } from "../../services/CaldavConfigService";
import { useCaldavConfigStore } from "../../stores/CaldavConfigStore";
// MonthSelector types and helpers // MonthSelector types and helpers
type MonthItem = { type MonthItem = {
@@ -108,25 +109,11 @@ const Calendar = () => {
} }
}, [monthIndex, currentYear, setEvents]); }, [monthIndex, currentYear, setEvents]);
// Sync CalDAV in background, then reload events // Load events from DB on focus
const syncAndReload = useCallback(async () => {
try {
await CaldavConfigService.sync();
await loadEvents();
} catch {
// Sync failed — not critical
}
}, [loadEvents]);
// Load events instantly on focus, then sync in background periodically
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
loadEvents(); loadEvents();
syncAndReload(); }, [loadEvents]),
const interval = setInterval(syncAndReload, 10_000);
return () => clearInterval(interval);
}, [loadEvents, syncAndReload]),
); );
// Re-open overlay after back navigation from editEvent // Re-open overlay after back navigation from editEvent
@@ -254,6 +241,7 @@ const Calendar = () => {
setMonthIndex={setMonthIndex} setMonthIndex={setMonthIndex}
setYear={setCurrentYear} setYear={setCurrentYear}
/> />
<CalendarToolbar loadEvents={loadEvents} />
<WeekDaysLine /> <WeekDaysLine />
<CalendarGrid <CalendarGrid
month={MONTHS[monthIndex]} month={MONTHS[monthIndex]}
@@ -558,6 +546,131 @@ const ChangeMonthButton = (props: ChangeMonthButtonProps) => {
); );
}; };
type CalendarToolbarProps = {
loadEvents: () => Promise<void>;
};
const CalendarToolbar = ({ loadEvents }: CalendarToolbarProps) => {
const { theme } = useThemeStore();
const { config } = useCaldavConfigStore();
const [isSyncing, setIsSyncing] = useState(false);
const [syncResult, setSyncResult] = useState<"success" | "error" | null>(
null,
);
const handleSync = async () => {
if (!config || isSyncing) return;
setSyncResult(null);
setIsSyncing(true);
try {
await CaldavConfigService.sync();
await loadEvents();
setSyncResult("success");
} catch (error) {
console.error("CalDAV sync failed:", error);
setSyncResult("error");
} finally {
setIsSyncing(false);
}
};
useEffect(() => {
if (!syncResult) return;
const timer = setTimeout(() => setSyncResult(null), 3000);
return () => clearTimeout(timer);
}, [syncResult]);
const handleLogout = async () => {
await AuthService.logout();
router.replace("/login");
};
const syncIcon = () => {
if (isSyncing) {
return <ActivityIndicator size="small" color={theme.primeFg} />;
}
if (syncResult === "success") {
return (
<Ionicons
name="checkmark-circle"
size={20}
color={theme.confirmButton}
/>
);
}
if (syncResult === "error") {
return (
<Ionicons name="close-circle" size={20} color={theme.rejectButton} />
);
}
return (
<Ionicons
name="sync-outline"
size={20}
color={config ? theme.primeFg : theme.textMuted}
/>
);
};
const buttonStyle = {
backgroundColor: theme.chatBot,
borderColor: theme.borderPrimary,
borderWidth: 1,
shadowColor: theme.shadowColor,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 4,
};
const className = "flex flex-row items-center gap-2 px-3 py-1 rounded-lg";
return (
<View
className="flex flex-row items-center justify-around py-2"
style={{
backgroundColor: theme.primeBg,
borderBottomWidth: 0,
borderBottomColor: theme.borderPrimary,
}}
>
<Pressable
onPress={handleSync}
disabled={!config || isSyncing}
className={className}
style={{
...buttonStyle,
...(config
? {}
: {
backgroundColor: theme.disabledButton,
borderColor: theme.disabledButton,
}),
}}
>
{syncIcon()}
<Text
style={{ color: config ? theme.textPrimary : theme.textMuted }}
className="font-medium"
>
Sync
</Text>
</Pressable>
<Pressable
onPress={handleLogout}
className={className}
style={buttonStyle}
>
<Ionicons name="log-out-outline" size={20} color={theme.primeFg} />
<Text style={{ color: theme.textPrimary }} className="font-medium">
Logout
</Text>
</Pressable>
</View>
);
};
const WeekDaysLine = () => { const WeekDaysLine = () => {
const { theme } = useThemeStore(); const { theme } = useThemeStore();
return ( return (

View File

@@ -45,7 +45,9 @@ const CaldavTextInput = ({
const { theme } = useThemeStore(); const { theme } = useThemeStore();
return ( return (
<View className="flex flex-row items-center py-1"> <View className="flex flex-row items-center py-1">
<Text className="ml-4 w-24" style={{ color: theme.textPrimary }}>{title}:</Text> <Text className="ml-4 w-24" style={{ color: theme.textPrimary }}>
{title}:
</Text>
<CustomTextInput <CustomTextInput
className="flex-1 mr-4 px-3 py-2" className="flex-1 mr-4 px-3 py-2"
text={value} text={value}
@@ -111,7 +113,13 @@ const CaldavSettings = () => {
); );
const saveConfig = async () => { const saveConfig = async () => {
showFeedback(setSaveFeedback, saveTimer, "Speichere Konfiguration...", false, true); showFeedback(
setSaveFeedback,
saveTimer,
"Speichere Konfiguration...",
false,
true,
);
try { try {
const saved = await CaldavConfigService.saveConfig( const saved = await CaldavConfigService.saveConfig(
serverUrl, serverUrl,
@@ -119,9 +127,19 @@ const CaldavSettings = () => {
password, password,
); );
setConfig(saved); setConfig(saved);
showFeedback(setSaveFeedback, saveTimer, "Konfiguration wurde gespeichert", false); showFeedback(
setSaveFeedback,
saveTimer,
"Konfiguration wurde gespeichert",
false,
);
} catch { } catch {
showFeedback(setSaveFeedback, saveTimer, "Fehler beim Speichern der Konfiguration", true); showFeedback(
setSaveFeedback,
saveTimer,
"Fehler beim Speichern der Konfiguration",
true,
);
} }
}; };
@@ -129,9 +147,19 @@ const CaldavSettings = () => {
showFeedback(setSyncFeedback, syncTimer, "Synchronisiere...", false, true); showFeedback(setSyncFeedback, syncTimer, "Synchronisiere...", false, true);
try { try {
await CaldavConfigService.sync(); await CaldavConfigService.sync();
showFeedback(setSyncFeedback, syncTimer, "Synchronisierung erfolgreich", false); showFeedback(
setSyncFeedback,
syncTimer,
"Synchronisierung erfolgreich",
false,
);
} catch { } catch {
showFeedback(setSyncFeedback, syncTimer, "Fehler beim Synchronisieren", true); showFeedback(
setSyncFeedback,
syncTimer,
"Fehler beim Synchronisieren",
true,
);
} }
}; };

View File

@@ -62,11 +62,6 @@ export const AuthGuard = ({ children }: AuthGuardProps) => {
if (!useAuthStore.getState().isAuthenticated) return; if (!useAuthStore.getState().isAuthenticated) return;
await preloadAppData(); await preloadAppData();
setDataReady(true); setDataReady(true);
try {
await CaldavConfigService.sync();
} catch {
// No CalDAV config or sync failed — not critical
}
}; };
init(); init();
}, [loadStoredUser]); }, [loadStoredUser]);

28
apps/server/.env.example Normal file
View File

@@ -0,0 +1,28 @@
# OpenAI API key for GPT-based chat assistant
# Required for AI chat functionality
OPENAI_API_KEY=sk-proj-your-key-here
# Port the server listens on
# Default: 3000
PORT=3000
# MongoDB connection URI
# Default: mongodb://localhost:27017/calchat
# The Docker Compose setup uses root:mongoose credentials with authSource=admin
MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
# Use static test responses instead of real GPT calls
# Values: true | false
# Default: false
USE_TEST_RESPONSES=false
# Log level for pino logger
# Values: debug | info | warn | error | fatal
# Default: debug (development), info (production)
LOG_LEVEL=debug
# Node environment
# Values: development | production
# development = pretty-printed logs, production = JSON logs
# Default: development
NODE_ENV=development

View File

@@ -0,0 +1,31 @@
FROM node:alpine AS build
WORKDIR /app
COPY package.json package-lock.json tsconfig.json ./
COPY packages/shared/package.json ./packages/shared/
COPY apps/server/package.json ./apps/server/
RUN npm ci -w @calchat/server -w @calchat/shared --include-workspace-root
COPY packages/shared/ packages/shared/
COPY apps/server/ apps/server/
RUN npm run build -w @calchat/shared && npm run build -w @calchat/server
FROM node:alpine
WORKDIR /app
COPY --from=build /app/package.json /app/package-lock.json ./
COPY --from=build /app/packages/shared/package.json packages/shared/
COPY --from=build /app/apps/server/package.json apps/server/
RUN npm ci --omit=dev -w @calchat/server -w @calchat/shared
COPY --from=build /app/packages/shared/dist/ packages/shared/dist/
COPY --from=build /app/apps/server/dist/ apps/server/dist/
EXPOSE 3001
CMD ["node", "apps/server/dist/app.js"]

View File

@@ -1,4 +1,5 @@
module.exports = { module.exports = {
preset: "ts-jest", preset: "ts-jest",
testEnvironment: "node", testEnvironment: "node",
testPathIgnorePatterns: ["/node_modules/", "/dist/"],
}; };

View File

@@ -3,8 +3,8 @@
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "tsx watch src/app.ts", "dev": "npm run build --workspace=@calchat/shared && tsx watch src/app.ts",
"build": "tsc", "build": "npm run build --workspace=@calchat/shared && tsc",
"start": "node dist/app.js", "start": "node dist/app.js",
"test": "jest" "test": "jest"
}, },
@@ -14,11 +14,11 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^5.2.1", "express": "^5.2.1",
"ical.js": "^2.2.1", "ical.js": "^2.2.1",
"jsonwebtoken": "^9.0.3",
"mongoose": "^9.1.1", "mongoose": "^9.1.1",
"openai": "^6.15.0", "openai": "^6.15.0",
"pino": "^10.1.1", "pino": "^10.1.1",
"pino-http": "^11.0.0", "pino-http": "^11.0.0",
"pino-pretty": "^13.1.3",
"rrule": "^2.8.1", "rrule": "^2.8.1",
"tsdav": "^2.1.6" "tsdav": "^2.1.6"
}, },
@@ -27,10 +27,8 @@
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/ical": "^0.8.3", "@types/ical": "^0.8.3",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"jest": "^30.2.0", "jest": "^30.2.0",
"pino-pretty": "^13.1.3",
"ts-jest": "^29.4.6", "ts-jest": "^29.4.6",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "^5.9.3" "typescript": "^5.9.3"

View File

@@ -63,12 +63,7 @@ const aiProvider = new GPTAdapter();
const authService = new AuthService(userRepo); const authService = new AuthService(userRepo);
const eventService = new EventService(eventRepo); const eventService = new EventService(eventRepo);
const caldavService = new CaldavService(caldavRepo, eventService); const caldavService = new CaldavService(caldavRepo, eventService);
const chatService = new ChatService( const chatService = new ChatService(chatRepo, eventService, aiProvider);
chatRepo,
eventService,
aiProvider,
caldavService,
);
// Initialize controllers // Initialize controllers
const authController = new AuthController(authService); const authController = new AuthController(authService);

View File

@@ -21,12 +21,4 @@ export class AuthController {
res.status(400).json({ error: (error as Error).message }); res.status(400).json({ error: (error as Error).message });
} }
} }
async refresh(req: Request, res: Response): Promise<void> {
throw new Error("Not implemented");
}
async logout(req: Request, res: Response): Promise<void> {
throw new Error("Not implemented");
}
} }

View File

@@ -15,7 +15,10 @@ export class CaldavController {
const response = await this.caldavService.saveConfig(config); const response = await this.caldavService.saveConfig(config);
res.json(response); res.json(response);
} catch (error) { } catch (error) {
log.error({ error, userId: req.user?.userId }, "Error saving config"); log.error(
{ err: error, userId: req.user?.userId },
"Error saving config",
);
res.status(500).json({ error: "Failed to save config" }); res.status(500).json({ error: "Failed to save config" });
} }
} }
@@ -30,7 +33,10 @@ export class CaldavController {
// Don't expose the password to the client // Don't expose the password to the client
res.json(config); res.json(config);
} catch (error) { } catch (error) {
log.error({ error, userId: req.user?.userId }, "Error loading config"); log.error(
{ err: error, userId: req.user?.userId },
"Error loading config",
);
res.status(500).json({ error: "Failed to load config" }); res.status(500).json({ error: "Failed to load config" });
} }
} }
@@ -40,7 +46,10 @@ export class CaldavController {
await this.caldavService.deleteConfig(req.user!.userId); await this.caldavService.deleteConfig(req.user!.userId);
res.status(204).send(); res.status(204).send();
} catch (error) { } catch (error) {
log.error({ error, userId: req.user?.userId }, "Error deleting config"); log.error(
{ err: error, userId: req.user?.userId },
"Error deleting config",
);
res.status(500).json({ error: "Failed to delete config" }); res.status(500).json({ error: "Failed to delete config" });
} }
} }
@@ -50,7 +59,10 @@ export class CaldavController {
const events = await this.caldavService.pullEvents(req.user!.userId); const events = await this.caldavService.pullEvents(req.user!.userId);
res.json(events); res.json(events);
} catch (error) { } catch (error) {
log.error({ error, userId: req.user?.userId }, "Error pulling events"); log.error(
{ err: error, userId: req.user?.userId },
"Error pulling events",
);
res.status(500).json({ error: "Failed to pull events" }); res.status(500).json({ error: "Failed to pull events" });
} }
} }
@@ -60,7 +72,10 @@ export class CaldavController {
await this.caldavService.pushAll(req.user!.userId); await this.caldavService.pushAll(req.user!.userId);
res.status(204).send(); res.status(204).send();
} catch (error) { } catch (error) {
log.error({ error, userId: req.user?.userId }, "Error pushing events"); log.error(
{ err: error, userId: req.user?.userId },
"Error pushing events",
);
res.status(500).json({ error: "Failed to push events" }); res.status(500).json({ error: "Failed to push events" });
} }
} }
@@ -78,7 +93,10 @@ export class CaldavController {
await this.caldavService.pushEvent(req.user!.userId, event); await this.caldavService.pushEvent(req.user!.userId, event);
res.status(204).send(); res.status(204).send();
} catch (error) { } catch (error) {
log.error({ error, userId: req.user?.userId }, "Error pushing event"); log.error(
{ err: error, userId: req.user?.userId },
"Error pushing event",
);
res.status(500).json({ error: "Failed to push event" }); res.status(500).json({ error: "Failed to push event" });
} }
} }

View File

@@ -28,7 +28,7 @@ export class ChatController {
res.json(response); res.json(response);
} catch (error) { } catch (error) {
log.error( log.error(
{ error, userId: req.user?.userId }, { err: error, userId: req.user?.userId },
"Error processing message", "Error processing message",
); );
res.status(500).json({ error: "Failed to process message" }); res.status(500).json({ error: "Failed to process message" });
@@ -79,13 +79,13 @@ export class ChatController {
await this.caldavService.pushAll(userId); await this.caldavService.pushAll(userId);
} }
} catch (error) { } catch (error) {
log.error({ error, userId }, "CalDAV push after confirm failed"); log.error({ err: error, userId }, "CalDAV push after confirm failed");
} }
res.json(response); res.json(response);
} catch (error) { } catch (error) {
log.error( log.error(
{ error, conversationId: req.params.conversationId }, { err: error, conversationId: req.params.conversationId },
"Error confirming event", "Error confirming event",
); );
res.status(500).json({ error: "Failed to confirm event" }); res.status(500).json({ error: "Failed to confirm event" });
@@ -106,7 +106,7 @@ export class ChatController {
res.json(response); res.json(response);
} catch (error) { } catch (error) {
log.error( log.error(
{ error, conversationId: req.params.conversationId }, { err: error, conversationId: req.params.conversationId },
"Error rejecting event", "Error rejecting event",
); );
res.status(500).json({ error: "Failed to reject event" }); res.status(500).json({ error: "Failed to reject event" });
@@ -123,7 +123,7 @@ export class ChatController {
res.json(conversations); res.json(conversations);
} catch (error) { } catch (error) {
log.error( log.error(
{ error, userId: req.user?.userId }, { err: error, userId: req.user?.userId },
"Error getting conversations", "Error getting conversations",
); );
res.status(500).json({ error: "Failed to get conversations" }); res.status(500).json({ error: "Failed to get conversations" });
@@ -157,7 +157,7 @@ export class ChatController {
res.status(404).json({ error: "Conversation not found" }); res.status(404).json({ error: "Conversation not found" });
} else { } else {
log.error( log.error(
{ error, conversationId: req.params.id }, { err: error, conversationId: req.params.id },
"Error getting conversation", "Error getting conversation",
); );
res.status(500).json({ error: "Failed to get conversation" }); res.status(500).json({ error: "Failed to get conversation" });
@@ -187,7 +187,7 @@ export class ChatController {
} }
} catch (error) { } catch (error) {
log.error( log.error(
{ error, messageId: req.params.messageId }, { err: error, messageId: req.params.messageId },
"Error updating proposal event", "Error updating proposal event",
); );
res.status(500).json({ error: "Failed to update proposal event" }); res.status(500).json({ error: "Failed to update proposal event" });

View File

@@ -18,7 +18,7 @@ export class EventController {
try { try {
await this.caldavService.pushEvent(userId, event); await this.caldavService.pushEvent(userId, event);
} catch (error) { } catch (error) {
log.error({ error, userId }, "Error pushing event to CalDAV"); log.error({ err: error, userId }, "Error pushing event to CalDAV");
} }
} }
} }
@@ -28,7 +28,7 @@ export class EventController {
try { try {
await this.caldavService.deleteEvent(userId, event.caldavUUID); await this.caldavService.deleteEvent(userId, event.caldavUUID);
} catch (error) { } catch (error) {
log.error({ error, userId }, "Error deleting event from CalDAV"); log.error({ err: error, userId }, "Error deleting event from CalDAV");
} }
} }
} }
@@ -40,7 +40,10 @@ export class EventController {
await this.pushToCaldav(userId, event); await this.pushToCaldav(userId, event);
res.status(201).json(event); res.status(201).json(event);
} catch (error) { } catch (error) {
log.error({ error, userId: req.user?.userId }, "Error creating event"); log.error(
{ err: error, userId: req.user?.userId },
"Error creating event",
);
res.status(500).json({ error: "Failed to create event" }); res.status(500).json({ error: "Failed to create event" });
} }
} }
@@ -57,7 +60,7 @@ export class EventController {
} }
res.json(event); res.json(event);
} catch (error) { } catch (error) {
log.error({ error, eventId: req.params.id }, "Error getting event"); log.error({ err: error, eventId: req.params.id }, "Error getting event");
res.status(500).json({ error: "Failed to get event" }); res.status(500).json({ error: "Failed to get event" });
} }
} }
@@ -67,7 +70,10 @@ export class EventController {
const events = await this.eventService.getAll(req.user!.userId); const events = await this.eventService.getAll(req.user!.userId);
res.json(events); res.json(events);
} catch (error) { } catch (error) {
log.error({ error, userId: req.user?.userId }, "Error getting events"); log.error(
{ err: error, userId: req.user?.userId },
"Error getting events",
);
res.status(500).json({ error: "Failed to get events" }); res.status(500).json({ error: "Failed to get events" });
} }
} }
@@ -100,7 +106,7 @@ export class EventController {
res.json(events); res.json(events);
} catch (error) { } catch (error) {
log.error( log.error(
{ error, start: req.query.start, end: req.query.end }, { err: error, start: req.query.start, end: req.query.end },
"Error getting events by range", "Error getting events by range",
); );
res.status(500).json({ error: "Failed to get events" }); res.status(500).json({ error: "Failed to get events" });
@@ -124,7 +130,7 @@ export class EventController {
res.json(event); res.json(event);
} catch (error) { } catch (error) {
log.error({ error, eventId: req.params.id }, "Error updating event"); log.error({ err: error, eventId: req.params.id }, "Error updating event");
res.status(500).json({ error: "Failed to update event" }); res.status(500).json({ error: "Failed to update event" });
} }
} }
@@ -171,7 +177,7 @@ export class EventController {
await this.deleteFromCaldav(userId, event); await this.deleteFromCaldav(userId, event);
res.status(204).send(); res.status(204).send();
} catch (error) { } catch (error) {
log.error({ error, eventId: req.params.id }, "Error deleting event"); log.error({ err: error, eventId: req.params.id }, "Error deleting event");
res.status(500).json({ error: "Failed to delete event" }); res.status(500).json({ error: "Failed to delete event" });
} }
} }

View File

@@ -6,8 +6,6 @@ export function createAuthRoutes(authController: AuthController): Router {
router.post("/login", (req, res) => authController.login(req, res)); router.post("/login", (req, res) => authController.login(req, res));
router.post("/register", (req, res) => authController.register(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; return router;
} }

View File

@@ -1,6 +1,5 @@
import { User, CreateUserDTO, LoginDTO, AuthResponse } from "@calchat/shared"; import { CreateUserDTO, LoginDTO, AuthResponse } from "@calchat/shared";
import { UserRepository } from "./interfaces"; import { UserRepository } from "./interfaces";
import * as jwt from "../utils/jwt";
import * as password from "../utils/password"; import * as password from "../utils/password";
export class AuthService { export class AuthService {
@@ -45,12 +44,4 @@ export class AuthService {
return { user, accessToken: "" }; return { user, accessToken: "" };
} }
async refreshToken(refreshToken: string): Promise<AuthResponse> {
throw new Error("Not implemented");
}
async logout(userId: string): Promise<void> {
throw new Error("Not implemented");
}
} }

View File

@@ -1,11 +0,0 @@
// import { createLogger } from "../logging";
// import { CaldavService } from "./CaldavService";
//
// const logger = createLogger("CaldavService-Test");
//
// const cdService = new CaldavService();
//
// test("print events", async () => {
// const client = await cdService.login();
// await cdService.pullEvents(client);
// });

View File

@@ -6,9 +6,10 @@ import { CaldavRepository } from "./interfaces/CaldavRepository";
import { import {
CalendarEvent, CalendarEvent,
CreateEventDTO, CreateEventDTO,
} from "@calchat/shared/src/models/CalendarEvent"; CaldavConfig,
formatDateKey,
} from "@calchat/shared";
import { EventService } from "./EventService"; import { EventService } from "./EventService";
import { CaldavConfig, formatDateKey } from "@calchat/shared";
const logger = createLogger("CaldavService"); const logger = createLogger("CaldavService");

View File

@@ -14,7 +14,6 @@ import {
} from "@calchat/shared"; } from "@calchat/shared";
import { ChatRepository, AIProvider } from "./interfaces"; import { ChatRepository, AIProvider } from "./interfaces";
import { EventService } from "./EventService"; import { EventService } from "./EventService";
import { CaldavService } from "./CaldavService";
import { getWeeksOverview, getMonthOverview } from "../utils/eventFormatters"; import { getWeeksOverview, getMonthOverview } from "../utils/eventFormatters";
type TestResponse = { type TestResponse = {
@@ -543,7 +542,6 @@ export class ChatService {
private chatRepo: ChatRepository, private chatRepo: ChatRepository,
private eventService: EventService, private eventService: EventService,
private aiProvider: AIProvider, private aiProvider: AIProvider,
private caldavService: CaldavService,
) {} ) {}
async processMessage( async processMessage(
@@ -578,32 +576,17 @@ export class ChatService {
limit: 20, limit: 20,
}); });
// Lazy CalDAV sync: only sync once when the AI first accesses event data
let hasSynced = false;
const syncOnce = async () => {
if (hasSynced) return;
hasSynced = true;
try {
await this.caldavService.sync(userId);
} catch {
// CalDAV sync is not critical for AI responses
}
};
response = await this.aiProvider.processMessage(data.content, { response = await this.aiProvider.processMessage(data.content, {
userId, userId,
conversationHistory: history, conversationHistory: history,
currentDate: new Date(), currentDate: new Date(),
fetchEventsInRange: async (start, end) => { fetchEventsInRange: async (start, end) => {
await syncOnce();
return this.eventService.getByDateRange(userId, start, end); return this.eventService.getByDateRange(userId, start, end);
}, },
searchEvents: async (query) => { searchEvents: async (query) => {
await syncOnce();
return this.eventService.searchByTitle(userId, query); return this.eventService.searchByTitle(userId, query);
}, },
fetchEventById: async (eventId) => { fetchEventById: async (eventId) => {
await syncOnce();
return this.eventService.getById(eventId, userId); return this.eventService.getById(eventId, userId);
}, },
}); });

View File

@@ -1,4 +1,4 @@
import { CaldavConfig } from "@calchat/shared/src/models/CaldavConfig"; import { CaldavConfig } from "@calchat/shared";
export interface CaldavRepository { export interface CaldavRepository {
findByUserId(userId: string): Promise<CaldavConfig | null>; findByUserId(userId: string): Promise<CaldavConfig | null>;

View File

@@ -1,2 +1 @@
export * from "./jwt";
export * from "./password"; export * from "./password";

View File

@@ -1,26 +0,0 @@
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");
}

View File

@@ -0,0 +1,37 @@
import { hash, compare } from "./password";
describe("password", () => {
describe("hash()", () => {
it("returns a valid bcrypt hash", async () => {
const result = await hash("testpassword");
expect(result).toMatch(/^\$2b\$/);
});
it("produces different hashes for the same password (salt)", async () => {
const hash1 = await hash("samepassword");
const hash2 = await hash("samepassword");
expect(hash1).not.toBe(hash2);
});
});
describe("compare()", () => {
it("returns true for the correct password", async () => {
const hashed = await hash("correct");
const result = await compare("correct", hashed);
expect(result).toBe(true);
});
it("returns false for a wrong password", async () => {
const hashed = await hash("correct");
const result = await compare("wrong", hashed);
expect(result).toBe(false);
});
it("handles special characters and unicode", async () => {
const password = "p@$$w0rd!#%& äöü 🔑";
const hashed = await hash(password);
expect(await compare(password, hashed)).toBe(true);
expect(await compare("other", hashed)).toBe(false);
});
});
});

View File

@@ -0,0 +1,210 @@
import { CalendarEvent } from "@calchat/shared";
import { expandRecurringEvents } from "./recurrenceExpander";
// Helper: create a CalendarEvent with sensible defaults
function makeEvent(
overrides: Partial<CalendarEvent> & { startTime: Date; endTime: Date },
): CalendarEvent {
return {
id: "evt-1",
userId: "user-1",
title: "Test Event",
...overrides,
};
}
// Helper: create a date from "YYYY-MM-DD HH:mm" (local time)
function d(dateStr: string): Date {
const [datePart, timePart] = dateStr.split(" ");
const [y, m, day] = datePart.split("-").map(Number);
if (timePart) {
const [h, min] = timePart.split(":").map(Number);
return new Date(y, m - 1, day, h, min);
}
return new Date(y, m - 1, day);
}
describe("expandRecurringEvents", () => {
// Range: 2025-06-01 to 2025-06-30
const rangeStart = d("2025-06-01 00:00");
const rangeEnd = d("2025-06-30 23:59");
describe("non-recurring events", () => {
it("returns event within the range", () => {
const event = makeEvent({
startTime: d("2025-06-10 09:00"),
endTime: d("2025-06-10 10:00"),
});
const result = expandRecurringEvents([event], rangeStart, rangeEnd);
expect(result).toHaveLength(1);
expect(result[0].occurrenceStart).toEqual(d("2025-06-10 09:00"));
expect(result[0].occurrenceEnd).toEqual(d("2025-06-10 10:00"));
});
it("excludes event outside the range", () => {
const event = makeEvent({
startTime: d("2025-07-05 09:00"),
endTime: d("2025-07-05 10:00"),
});
const result = expandRecurringEvents([event], rangeStart, rangeEnd);
expect(result).toHaveLength(0);
});
it("includes event that starts before range and ends within", () => {
const event = makeEvent({
startTime: d("2025-05-31 22:00"),
endTime: d("2025-06-01 02:00"),
});
const result = expandRecurringEvents([event], rangeStart, rangeEnd);
expect(result).toHaveLength(1);
});
it("includes event that spans the entire range", () => {
const event = makeEvent({
startTime: d("2025-05-01 00:00"),
endTime: d("2025-07-31 23:59"),
});
const result = expandRecurringEvents([event], rangeStart, rangeEnd);
expect(result).toHaveLength(1);
});
it("returns empty array for empty input", () => {
const result = expandRecurringEvents([], rangeStart, rangeEnd);
expect(result).toEqual([]);
});
});
describe("recurring events", () => {
it("expands weekly event to all occurrences in range", () => {
// Weekly on Mondays, starting 2025-06-02 (a Monday)
const event = makeEvent({
startTime: d("2025-06-02 10:00"),
endTime: d("2025-06-02 11:00"),
recurrenceRule: "FREQ=WEEKLY;BYDAY=MO",
});
const result = expandRecurringEvents([event], rangeStart, rangeEnd);
// Mondays in June 2025: 2, 9, 16, 23, 30
expect(result).toHaveLength(5);
expect(result[0].occurrenceStart).toEqual(d("2025-06-02 10:00"));
expect(result[1].occurrenceStart).toEqual(d("2025-06-09 10:00"));
expect(result[2].occurrenceStart).toEqual(d("2025-06-16 10:00"));
expect(result[3].occurrenceStart).toEqual(d("2025-06-23 10:00"));
expect(result[4].occurrenceStart).toEqual(d("2025-06-30 10:00"));
});
it("daily event with UNTIL stops at the right date", () => {
const event = makeEvent({
startTime: d("2025-06-01 08:00"),
endTime: d("2025-06-01 09:00"),
recurrenceRule: "FREQ=DAILY;UNTIL=20250605T235959",
});
const result = expandRecurringEvents([event], rangeStart, rangeEnd);
// June 1, 2, 3, 4, 5
expect(result).toHaveLength(5);
expect(result[4].occurrenceStart).toEqual(d("2025-06-05 08:00"));
});
it("skips occurrences on exception dates (EXDATE)", () => {
const event = makeEvent({
startTime: d("2025-06-02 10:00"),
endTime: d("2025-06-02 11:00"),
recurrenceRule: "FREQ=WEEKLY;BYDAY=MO",
exceptionDates: ["2025-06-09", "2025-06-23"],
});
const result = expandRecurringEvents([event], rangeStart, rangeEnd);
// 5 Mondays minus 2 exceptions = 3
expect(result).toHaveLength(3);
const dates = result.map((r) => r.occurrenceStart.getDate());
expect(dates).toEqual([2, 16, 30]);
});
it("handles RRULE: prefix (strips it)", () => {
const event = makeEvent({
startTime: d("2025-06-01 08:00"),
endTime: d("2025-06-01 09:00"),
recurrenceRule: "RRULE:FREQ=DAILY;COUNT=3",
});
const result = expandRecurringEvents([event], rangeStart, rangeEnd);
expect(result).toHaveLength(3);
});
it("falls back to single occurrence on invalid RRULE", () => {
const consoleSpy = jest.spyOn(console, "error").mockImplementation();
const event = makeEvent({
startTime: d("2025-06-10 09:00"),
endTime: d("2025-06-10 10:00"),
recurrenceRule: "COMPLETELY_INVALID_RULE",
});
const result = expandRecurringEvents([event], rangeStart, rangeEnd);
expect(result).toHaveLength(1);
expect(result[0].occurrenceStart).toEqual(d("2025-06-10 09:00"));
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
});
describe("multi-day events", () => {
it("finds event starting before range that ends within range", () => {
// 3-day recurring event starting May 15, weekly
const event = makeEvent({
startTime: d("2025-05-15 08:00"),
endTime: d("2025-05-18 08:00"),
recurrenceRule: "FREQ=WEEKLY",
});
// The occurrence starting May 29 ends June 1 → overlaps with range
const result = expandRecurringEvents([event], rangeStart, rangeEnd);
const starts = result.map((r) => r.occurrenceStart.getDate());
// May 29 (ends June 1), June 5, 12, 19, 26
expect(starts).toContain(29); // May 29
});
});
describe("sorting", () => {
it("returns events sorted by occurrenceStart", () => {
const laterEvent = makeEvent({
id: "evt-later",
startTime: d("2025-06-20 14:00"),
endTime: d("2025-06-20 15:00"),
});
const earlierEvent = makeEvent({
id: "evt-earlier",
startTime: d("2025-06-05 09:00"),
endTime: d("2025-06-05 10:00"),
});
// Pass in reverse order
const result = expandRecurringEvents(
[laterEvent, earlierEvent],
rangeStart,
rangeEnd,
);
expect(result).toHaveLength(2);
expect(result[0].id).toBe("evt-earlier");
expect(result[1].id).toBe("evt-later");
});
});
});

View File

@@ -11,15 +11,13 @@ Base URL: `/api`
|--------|----------|--------------| |--------|----------|--------------|
| POST | `/auth/login` | User Login | | POST | `/auth/login` | User Login |
| POST | `/auth/register` | User Registrierung | | POST | `/auth/register` | User Registrierung |
| POST | `/auth/refresh` | JWT Token erneuern |
| POST | `/auth/logout` | User Logout |
--- ---
## Events ## Events
### Event Endpoints (`/api/events`) ### Event Endpoints (`/api/events`)
Alle Endpoints erfordern JWT-Authentifizierung. Alle Endpoints erfordern Authentifizierung (X-User-Id Header).
| Method | Endpoint | Beschreibung | | Method | Endpoint | Beschreibung |
|--------|----------|--------------| |--------|----------|--------------|
@@ -35,7 +33,7 @@ Alle Endpoints erfordern JWT-Authentifizierung.
## Chat ## Chat
### Chat Endpoints (`/api/chat`) ### Chat Endpoints (`/api/chat`)
Alle Endpoints erfordern JWT-Authentifizierung. Alle Endpoints erfordern Authentifizierung (X-User-Id Header).
| Method | Endpoint | Beschreibung | | Method | Endpoint | Beschreibung |
|--------|----------|--------------| |--------|----------|--------------|

View File

@@ -188,11 +188,6 @@ package "Models" #D3D3D3 {
} }
package "Utils" #DDA0DD { package "Utils" #DDA0DD {
class JWT {
' +signToken()
' +verifyToken()
}
class Password { class Password {
' +hash() ' +hash()
' +compare() ' +compare()
@@ -215,8 +210,6 @@ ChatController --> CaldavService
EventController --> EventService EventController --> EventService
EventController --> CaldavService EventController --> CaldavService
CaldavController --> CaldavService CaldavController --> CaldavService
AuthMiddleware --> JWT
' Service -> Interfaces (intern) ' Service -> Interfaces (intern)
AuthService --> UserRepository AuthService --> UserRepository
ChatService --> ChatRepository ChatService --> ChatRepository
@@ -227,7 +220,6 @@ CaldavService --> CaldavRepository
CaldavService --> EventService CaldavService --> EventService
' Auth uses Utils ' Auth uses Utils
AuthService --> JWT
AuthService --> Password AuthService --> Password
' Event/Chat uses Utils ' Event/Chat uses Utils

View File

@@ -77,7 +77,6 @@ package "apps/server (Express.js)" as ServerPkg #98FB98 {
} }
package "Utils" { package "Utils" {
[JWT] as JWTUtil
[Password] as PwdUtil [Password] as PwdUtil
[RecurrenceExpander] as RecExpander [RecurrenceExpander] as RecExpander
[EventFormatters] as EvtFormatters [EventFormatters] as EvtFormatters
@@ -154,9 +153,7 @@ GPT ..|> Interfaces
Repos ..|> Interfaces Repos ..|> Interfaces
' Backend: Service -> Utils ' Backend: Service -> Utils
AuthSvc --> JWTUtil
AuthSvc --> PwdUtil AuthSvc --> PwdUtil
Middleware --> JWTUtil
EventSvc --> RecExpander EventSvc --> RecExpander
ChatSvc --> EvtFormatters ChatSvc --> EvtFormatters

View File

@@ -12,22 +12,10 @@ skinparam wrapWidth 100
skinparam nodesep 30 skinparam nodesep 30
skinparam ranksep 30 skinparam ranksep 30
top to bottom direction left to right direction
title Frontend (Expo React Native) title Frontend (Expo React Native)
' ===== SCREENS =====
package "Screens" #87CEEB {
class LoginScreen
class RegisterScreen
class CalendarScreen
class ChatScreen
class SettingsScreen
class EditEventScreen
class EventDetailScreen
class NoteScreen
}
' ===== COMPONENTS ===== ' ===== COMPONENTS =====
package "Components" #FFA07A { package "Components" #FFA07A {
class AuthGuard class AuthGuard
@@ -44,41 +32,53 @@ package "Components" #FFA07A {
class TypingIndicator class TypingIndicator
} }
' ===== SCREENS =====
package "Screens" #87CEEB {
class LoginScreen
class RegisterScreen
class CalendarScreen
class ChatScreen
class SettingsScreen
class EditEventScreen
class EventDetailScreen
class NoteScreen
}
' ===== SERVICES ===== ' ===== SERVICES =====
package "Services" #90EE90 { package "Services" #90EE90 {
class ApiClient { class ApiClient {
+get() ' +get()
+post() ' +post()
+put() ' +put()
+delete() ' +delete()
} }
class AuthService { class AuthService {
+login() ' +login()
+register() ' +register()
+logout() ' +logout()
+refresh() ' +refresh()
} }
class EventService { class EventService {
+getAll() ' +getAll()
+getById() ' +getById()
+getByDateRange() ' +getByDateRange()
+create() ' +create()
+update() ' +update()
+delete() ' +delete()
} }
class ChatService { class ChatService {
+sendMessage() ' +sendMessage()
+confirmEvent() ' +confirmEvent()
+rejectEvent() ' +rejectEvent()
+getConversations() ' +getConversations()
+getConversation() ' +getConversation()
+updateProposalEvent() ' +updateProposalEvent()
} }
class CaldavConfigService { class CaldavConfigService {
+getConfig() ' +getConfig()
+saveConfig() ' +saveConfig()
+deleteConfig() ' +deleteConfig()
+sync() ' +sync()
} }
} }
@@ -122,16 +122,6 @@ package "Models (shared)" #D3D3D3 {
' ===== RELATIONSHIPS ===== ' ===== RELATIONSHIPS =====
' Screens -> Services
LoginScreen --> AuthService
CalendarScreen --> EventService
CalendarScreen --> CaldavConfigService
ChatScreen --> ChatService
NoteScreen --> EventService
EditEventScreen --> EventService
EditEventScreen --> ChatService
SettingsScreen --> CaldavConfigService
' Screens -> Components ' Screens -> Components
CalendarScreen --> EventCard CalendarScreen --> EventCard
ChatScreen --> ProposedEventCard ChatScreen --> ProposedEventCard
@@ -143,6 +133,16 @@ EventCardBase --> CardBase
ModalBase --> CardBase ModalBase --> CardBase
DeleteEventModal --> ModalBase DeleteEventModal --> ModalBase
' Screens -> Services
LoginScreen --> AuthService
CalendarScreen --> EventService
CalendarScreen --> CaldavConfigService
ChatScreen --> ChatService
NoteScreen --> EventService
EditEventScreen --> EventService
EditEventScreen --> ChatService
SettingsScreen --> CaldavConfigService
' Auth ' Auth
AuthGuard --> AuthStore AuthGuard --> AuthStore
AuthGuard --> CaldavConfigService AuthGuard --> CaldavConfigService

View File

@@ -66,7 +66,7 @@ Backend & Express.js & Web-App Framework \\
& MongoDB & Datenbank \\ & MongoDB & Datenbank \\
& Mongoose & ODM \\ & Mongoose & ODM \\
& Claude (Anthropic) & KI / LLM \\ & Claude (Anthropic) & KI / LLM \\
& JWT & Authentifizierung \\ & X-User-Id Header & Authentifizierung \\
\hline \hline
Geplant & iCalendar & Event-Export \\ Geplant & iCalendar & Event-Export \\
\hline \hline
@@ -112,8 +112,9 @@ Der wichtigste Teil der App ist die KI-Integration über \textbf{Claude}
(Anthropic). Dieses LLM verarbeitet natürlichsprachliche Eingaben der Nutzer und (Anthropic). Dieses LLM verarbeitet natürlichsprachliche Eingaben der Nutzer und
generiert daraus strukturierte Event-Vorschläge. generiert daraus strukturierte Event-Vorschläge.
Die Authentifizierung läuft über \textbf{JSON Web Tokens} (JWT). Der Vorteil: Die Authentifizierung erfolgt über einen \textbf{X-User-Id Header}, der bei
zustandslose Sessions, bei denen der Server keine Session-Daten speichern muss. jedem Request die User-ID mitschickt. Diese einfache Lösung reicht für den
aktuellen Entwicklungsstand aus.
Geplant ist außerdem die Unterstützung des \textbf{iCalendar}-Formats (ICAL) Geplant ist außerdem die Unterstützung des \textbf{iCalendar}-Formats (ICAL)
für den Export von Kalender-Events. für den Export von Kalender-Events.
@@ -157,7 +158,7 @@ Notiz-Feld) und ChatMessage.
Der Controller Layer bildet die Schnittstelle zwischen Frontend und Der Controller Layer bildet die Schnittstelle zwischen Frontend und
Backend-Logik. Die Routes definieren die API-Endpunkte, die Controller Backend-Logik. Die Routes definieren die API-Endpunkte, die Controller
verarbeiten die eingehenden Requests und reichen diese an die Services weiter. verarbeiten die eingehenden Requests und reichen diese an die Services weiter.
Eine Auth Middleware prüft bei geschützten Routen den JWT-Token. Eine Auth Middleware prüft bei geschützten Routen den X-User-Id Header.
\subsubsection{Service Layer} \subsubsection{Service Layer}

102
package-lock.json generated
View File

@@ -72,7 +72,6 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^5.2.1", "express": "^5.2.1",
"ical.js": "^2.2.1", "ical.js": "^2.2.1",
"jsonwebtoken": "^9.0.3",
"mongoose": "^9.1.1", "mongoose": "^9.1.1",
"openai": "^6.15.0", "openai": "^6.15.0",
"pino": "^10.1.1", "pino": "^10.1.1",
@@ -85,7 +84,6 @@
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/ical": "^0.8.3", "@types/ical": "^0.8.3",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"jest": "^30.2.0", "jest": "^30.2.0",
"pino-pretty": "^13.1.3", "pino-pretty": "^13.1.3",
@@ -4936,20 +4934,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
"node_modules/@types/ms": {
"version": "2.1.0",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.10.1", "version": "24.10.1",
"license": "MIT", "license": "MIT",
@@ -6076,10 +6060,6 @@
"ieee754": "^1.1.13" "ieee754": "^1.1.13"
} }
}, },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"license": "MIT" "license": "MIT"
@@ -6844,13 +6824,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"license": "MIT" "license": "MIT"
@@ -13280,36 +13253,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"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",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/jsx-ast-utils": { "node_modules/jsx-ast-utils": {
"version": "3.3.5", "version": "3.3.5",
"dev": true, "dev": true,
@@ -13324,23 +13267,6 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/jwa": {
"version": "2.0.1",
"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",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/kareem": { "node_modules/kareem": {
"version": "3.0.0", "version": "3.0.0",
"license": "Apache-2.0", "license": "Apache-2.0",
@@ -13502,30 +13428,6 @@
"version": "4.0.8", "version": "4.0.8",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.includes": {
"version": "4.3.0",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"license": "MIT"
},
"node_modules/lodash.memoize": { "node_modules/lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -13538,10 +13440,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.once": {
"version": "4.1.1",
"license": "MIT"
},
"node_modules/lodash.throttle": { "node_modules/lodash.throttle": {
"version": "4.1.1", "version": "4.1.1",
"license": "MIT" "license": "MIT"

View File

@@ -7,7 +7,8 @@
"packages/*" "packages/*"
], ],
"scripts": { "scripts": {
"format": "prettier --write \"apps/*/src/**/*.{ts,tsx}\" \"packages/*/src/**/*.ts\"" "format": "prettier --write \"apps/*/src/**/*.{ts,tsx}\" \"packages/*/src/**/*.ts\"",
"check_format": "prettier --check \"apps/*/src/**/*.{ts,tsx}\" \"packages/*/src/**/*.ts\""
}, },
"devDependencies": { "devDependencies": {
"eslint": "^9.25.0", "eslint": "^9.25.0",

1
packages/shared/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist/

View File

@@ -2,11 +2,14 @@
"name": "@calchat/shared", "name": "@calchat/shared",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"main": "./src/index.ts", "main": "./dist/index.js",
"types": "./src/index.ts", "types": "./dist/index.d.ts",
"exports": { "exports": {
".": "./src/index.ts", ".": "./dist/index.js",
"./*": "./src/*" "./*": "./dist/*"
},
"scripts": {
"build": "tsc"
}, },
"dependencies": { "dependencies": {
"rrule": "^2.8.1" "rrule": "^2.8.1"

View File

@@ -1,10 +1,10 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"composite": true,
"declaration": true, "declaration": true,
"declarationMap": true, "outDir": "dist",
"module": "ESNext", "rootDir": "src",
"module": "CommonJS",
"target": "ES2020", "target": "ES2020",
"moduleResolution": "Node", "moduleResolution": "Node",
"esModuleInterop": true, "esModuleInterop": true,