diff --git a/CLAUDE.md b/CLAUDE.md index 1b8c4b0..d9be7c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,8 @@ npm run start -w @caldav/server # Run compiled server (port 3000) | | Mongoose | ODM | | | GPT (OpenAI) | AI/LLM for chat | | | JWT | Authentication | +| | pino / pino-http | Structured logging | +| | react-native-logs | Client-side logging | | Planned | iCalendar | Event export/import | ## Architecture @@ -83,9 +85,12 @@ src/ │ ├── EventConfirmDialog.tsx # AI-proposed event confirmation modal │ └── ProposedEventCard.tsx # Chat event proposal (uses EventCardBase + confirm/reject buttons) ├── Themes.tsx # Centralized color/theme definitions +├── logging/ +│ ├── index.ts # Re-exports +│ └── logger.ts # react-native-logs config (apiLogger, storeLogger) ├── services/ │ ├── index.ts # Re-exports all services -│ ├── ApiClient.ts # HTTP client (get, post, put, delete) +│ ├── ApiClient.ts # HTTP client with request logging (get, post, put, delete) │ ├── AuthService.ts # login(), register(), logout(), refresh() │ ├── EventService.ts # getAll(), getById(), getByDateRange(), create(), update(), delete() │ └── ChatService.ts # sendMessage(), confirmEvent(), rejectEvent(), getConversations(), getConversation() @@ -103,12 +108,16 @@ src/ ``` src/ ├── app.ts # Entry point, DI setup, Express config -├── controllers/ # Request handlers +├── controllers/ # Request handlers + middleware (per architecture diagram) │ ├── 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 +│ ├── EventController.ts # create(), getById(), getAll(), getByDateRange(), update(), delete() +│ ├── AuthMiddleware.ts # authenticate() - JWT validation +│ └── LoggingMiddleware.ts # httpLogger - pino-http request logging +├── logging/ +│ ├── index.ts # Re-exports +│ ├── logger.ts # pino config with redact for sensitive data +│ └── Logged.ts # @Logged() class decorator for automatic method logging ├── routes/ # API endpoint definitions │ ├── index.ts # Combines all routes under /api │ ├── auth.routes.ts # /api/auth/* @@ -239,6 +248,32 @@ const Schema = new Schema, {}, {}, IdVirtual> Repositories use `doc.toJSON() as unknown as Type` casting (required because Mongoose's TypeScript types don't reflect virtual fields in toJSON output). +### Logging + +Structured logging with pino (server) and react-native-logs (client). + +**Server Logging:** +- `pino` with `pino-pretty` for development, JSON in production +- `pino-http` middleware logs all HTTP requests (method, path, status, duration) +- `@Logged()` class decorator for automatic method logging on repositories and services +- Sensitive data (password, token, etc.) automatically redacted via pino's `redact` config + +**@Logged Decorator Pattern:** +```typescript +@Logged("MongoEventRepository") +export class MongoEventRepository implements EventRepository { ... } + +@Logged("GPTAdapter") +export class GPTAdapter implements AIProvider { ... } +``` + +The decorator uses a Proxy to intercept method calls lazily, preserves sync/async nature, and logs start/completion/failure with duration. + +**Client Logging:** +- `react-native-logs` with namespaced loggers (apiLogger, storeLogger) +- ApiClient logs all requests with method, endpoint, status, duration +- Log level: debug in __DEV__, warn in production + ## MVP Feature Scope ### Must-Have @@ -274,6 +309,8 @@ JWT_EXPIRES_IN=1h MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin OPENAI_API_KEY=sk-proj-... USE_TEST_RESPONSES=false # true = static test responses, false = real GPT AI +LOG_LEVEL=debug # debug | info | warn | error | fatal +NODE_ENV=development # development = pretty logs, production = JSON ``` ## Current Implementation Status @@ -299,6 +336,8 @@ USE_TEST_RESPONSES=false # true = static test responses, false = real GPT AI - `ChatRepository` interface: updateMessage() added for respondedAction tracking - `GPTAdapter`: Full implementation with OpenAI GPT (gpt-5-mini model), function calling for calendar operations - `ai/utils/`: Provider-agnostic shared utilities (systemPrompt, toolDefinitions, toolExecutor, eventFormatter) + - `logging/`: Structured logging with pino, pino-http middleware, @Logged decorator + - All repositories and GPTAdapter decorated with @Logged for automatic method logging - **Stubbed (TODO):** - `AuthMiddleware.authenticate()`: Currently uses fake user for testing - `AuthController`: refresh(), logout() @@ -323,7 +362,7 @@ USE_TEST_RESPONSES=false # true = static test responses, false = real GPT AI - KeyboardAvoidingView for proper keyboard handling (iOS padding, Android height) - Auto-scroll to end on new messages and keyboard show - keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled" -- `ApiClient`: get(), post(), put(), delete() implemented +- `ApiClient`: get(), post(), put(), delete() implemented with request/response logging - `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete() - fully implemented - `ChatService`: sendMessage(), confirmEvent(), rejectEvent(), getConversations(), getConversation() - fully implemented with cursor pagination - `EventCardBase`: Shared base component with event layout (header, date/time/recurring icons, description) - used by both EventCard and ProposedEventCard diff --git a/apps/client/package.json b/apps/client/package.json index b87bb6e..e3b2f86 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -34,6 +34,7 @@ "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", + "react-native-logs": "^5.5.0", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "5.6.0", "react-native-screens": "~4.16.0", diff --git a/apps/client/src/logging/index.ts b/apps/client/src/logging/index.ts new file mode 100644 index 0000000..509c10e --- /dev/null +++ b/apps/client/src/logging/index.ts @@ -0,0 +1 @@ +export { log, apiLogger, storeLogger } from "./logger"; diff --git a/apps/client/src/logging/logger.ts b/apps/client/src/logging/logger.ts new file mode 100644 index 0000000..de097a1 --- /dev/null +++ b/apps/client/src/logging/logger.ts @@ -0,0 +1,30 @@ +import { logger, consoleTransport } from "react-native-logs"; + +const log = logger.createLogger({ + levels: { + debug: 0, + info: 1, + warn: 2, + error: 3, + }, + severity: __DEV__ ? "debug" : "warn", + transport: consoleTransport, + transportOptions: { + colors: { + debug: "white", + info: "blueBright", + warn: "yellowBright", + error: "redBright", + }, + }, + async: true, + dateFormat: "time", + printLevel: true, + printDate: true, + enabled: true, +}); + +export const apiLogger = log.extend("API"); +export const storeLogger = log.extend("Store"); + +export { log }; diff --git a/apps/client/src/services/ApiClient.ts b/apps/client/src/services/ApiClient.ts index 26c8b01..fc19d80 100644 --- a/apps/client/src/services/ApiClient.ts +++ b/apps/client/src/services/ApiClient.ts @@ -1,4 +1,5 @@ import { Platform } from "react-native"; +import { apiLogger } from "../logging"; const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || @@ -19,20 +20,33 @@ async function request( endpoint: string, options?: RequestOptions, ): Promise { - const response = await fetch(`${API_BASE_URL}${endpoint}`, { - method, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - body: options?.body ? JSON.stringify(options.body) : undefined, - }); + const start = performance.now(); + apiLogger.debug(`${method} ${endpoint}`); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); + try { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + method, + headers: { + "Content-Type": "application/json", + ...options?.headers, + }, + body: options?.body ? JSON.stringify(options.body) : undefined, + }); + + const duration = Math.round(performance.now() - start); + + if (!response.ok) { + apiLogger.error(`${method} ${endpoint} - ${response.status} (${duration}ms)`); + throw new Error(`HTTP ${response.status}`); + } + + apiLogger.debug(`${method} ${endpoint} - ${response.status} (${duration}ms)`); + return response.json(); + } catch (error) { + const duration = Math.round(performance.now() - start); + apiLogger.error(`${method} ${endpoint} failed (${duration}ms): ${error}`); + throw error; } - - return response.json(); } export const ApiClient = { diff --git a/apps/server/package.json b/apps/server/package.json index f3e9422..9ba5040 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -15,6 +15,8 @@ "jsonwebtoken": "^9.0.3", "mongoose": "^9.1.1", "openai": "^6.15.0", + "pino": "^10.1.1", + "pino-http": "^11.0.0", "rrule": "^2.8.1" }, "devDependencies": { @@ -22,6 +24,7 @@ "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.10.1", + "pino-pretty": "^13.1.3", "tsx": "^4.21.0", "typescript": "^5.9.3" } diff --git a/apps/server/src/ai/GPTAdapter.ts b/apps/server/src/ai/GPTAdapter.ts index e8aeb5f..5f8e50c 100644 --- a/apps/server/src/ai/GPTAdapter.ts +++ b/apps/server/src/ai/GPTAdapter.ts @@ -7,6 +7,7 @@ import { executeToolCall, ToolDefinition, } from "./utils"; +import { Logged } from "../logging"; /** * Convert tool definitions to OpenAI format. @@ -24,6 +25,7 @@ function toOpenAITools( })); } +@Logged("GPTAdapter") export class GPTAdapter implements AIProvider { private client: OpenAI; private model: string; diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 3bb4d04..4c71c27 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -3,7 +3,12 @@ import mongoose from "mongoose"; import "dotenv/config"; import { createRoutes } from "./routes"; -import { AuthController, ChatController, EventController } from "./controllers"; +import { + AuthController, + ChatController, + EventController, + httpLogger, +} from "./controllers"; import { AuthService, ChatService, EventService } from "./services"; import { MongoUserRepository, @@ -11,6 +16,7 @@ import { MongoChatRepository, } from "./repositories"; import { GPTAdapter } from "./ai"; +import { logger } from "./logging"; const app = express(); const port = process.env.PORT || 3000; @@ -18,6 +24,7 @@ const mongoUri = process.env.MONGODB_URI || "mongodb://localhost:27017/caldav"; // Middleware app.use(express.json()); +app.use(httpLogger); // CORS - only needed for web browser development // Native mobile apps don't send Origin headers and aren't affected by CORS @@ -86,7 +93,7 @@ app.post("/api/ai/test", async (req, res) => { }); res.json(result); } catch (error) { - console.error("AI test error:", error); + logger.error({ error }, "AI test error"); res.status(500).json({ error: String(error) }); } }); @@ -95,13 +102,13 @@ app.post("/api/ai/test", async (req, res) => { async function start() { try { await mongoose.connect(mongoUri); - console.log("Connected to MongoDB"); + logger.info("Connected to MongoDB"); app.listen(port, () => { - console.log(`Server running on port ${port}`); + logger.info({ port }, "Server started"); }); } catch (error) { - console.error("Failed to start server:", error); + logger.fatal({ error }, "Failed to start server"); process.exit(1); } } diff --git a/apps/server/src/middleware/AuthMiddleware.ts b/apps/server/src/controllers/AuthMiddleware.ts similarity index 100% rename from apps/server/src/middleware/AuthMiddleware.ts rename to apps/server/src/controllers/AuthMiddleware.ts diff --git a/apps/server/src/controllers/ChatController.ts b/apps/server/src/controllers/ChatController.ts index a4b0703..6bc9d29 100644 --- a/apps/server/src/controllers/ChatController.ts +++ b/apps/server/src/controllers/ChatController.ts @@ -7,7 +7,10 @@ import { GetMessagesOptions, } from "@caldav/shared"; import { ChatService } from "../services"; -import { AuthenticatedRequest } from "../middleware"; +import { createLogger } from "../logging"; +import { AuthenticatedRequest } from "./AuthMiddleware"; + +const log = createLogger("ChatController"); export class ChatController { constructor(private chatService: ChatService) {} @@ -19,6 +22,7 @@ export class ChatController { const response = await this.chatService.processMessage(userId, data); res.json(response); } catch (error) { + log.error({ error, userId: req.user?.userId }, "Error processing message"); res.status(500).json({ error: "Failed to process message" }); } } @@ -44,6 +48,7 @@ export class ChatController { ); res.json(response); } catch (error) { + log.error({ error, conversationId: req.params.conversationId }, "Error confirming event"); res.status(500).json({ error: "Failed to confirm event" }); } } @@ -59,6 +64,7 @@ export class ChatController { ); res.json(response); } catch (error) { + log.error({ error, conversationId: req.params.conversationId }, "Error rejecting event"); res.status(500).json({ error: "Failed to reject event" }); } } @@ -72,6 +78,7 @@ export class ChatController { const conversations = await this.chatService.getConversations(userId); res.json(conversations); } catch (error) { + log.error({ error, userId: req.user?.userId }, "Error getting conversations"); res.status(500).json({ error: "Failed to get conversations" }); } } @@ -102,6 +109,7 @@ export class ChatController { if ((error as Error).message === "Conversation not found") { res.status(404).json({ error: "Conversation not found" }); } else { + log.error({ error, conversationId: req.params.id }, "Error getting conversation"); res.status(500).json({ error: "Failed to get conversation" }); } } diff --git a/apps/server/src/controllers/EventController.ts b/apps/server/src/controllers/EventController.ts index 7e231f0..8bce569 100644 --- a/apps/server/src/controllers/EventController.ts +++ b/apps/server/src/controllers/EventController.ts @@ -1,6 +1,9 @@ import { Response } from "express"; import { EventService } from "../services"; -import { AuthenticatedRequest } from "../middleware"; +import { createLogger } from "../logging"; +import { AuthenticatedRequest } from "./AuthMiddleware"; + +const log = createLogger("EventController"); export class EventController { constructor(private eventService: EventService) {} @@ -10,7 +13,7 @@ export class EventController { const event = await this.eventService.create(req.user!.userId, req.body); res.status(201).json(event); } catch (error) { - console.error("Error creating event:", error); + log.error({ error, userId: req.user?.userId }, "Error creating event"); res.status(500).json({ error: "Failed to create event" }); } } @@ -27,7 +30,7 @@ export class EventController { } res.json(event); } catch (error) { - console.error("Error getting event:", error); + log.error({ error, eventId: req.params.id }, "Error getting event"); res.status(500).json({ error: "Failed to get event" }); } } @@ -37,7 +40,7 @@ export class EventController { const events = await this.eventService.getAll(req.user!.userId); res.json(events); } catch (error) { - console.error("Error getting events:", error); + log.error({ error, userId: req.user?.userId }, "Error getting events"); res.status(500).json({ error: "Failed to get events" }); } } @@ -69,7 +72,7 @@ export class EventController { ); res.json(events); } catch (error) { - console.error("Error getting events by range:", error); + log.error({ error, start: req.query.start, end: req.query.end }, "Error getting events by range"); res.status(500).json({ error: "Failed to get events" }); } } @@ -87,7 +90,7 @@ export class EventController { } res.json(event); } catch (error) { - console.error("Error updating event:", error); + log.error({ error, eventId: req.params.id }, "Error updating event"); res.status(500).json({ error: "Failed to update event" }); } } @@ -104,7 +107,7 @@ export class EventController { } res.status(204).send(); } catch (error) { - console.error("Error deleting event:", error); + log.error({ error, eventId: req.params.id }, "Error deleting event"); res.status(500).json({ error: "Failed to delete event" }); } } diff --git a/apps/server/src/controllers/LoggingMiddleware.ts b/apps/server/src/controllers/LoggingMiddleware.ts new file mode 100644 index 0000000..66c1ac1 --- /dev/null +++ b/apps/server/src/controllers/LoggingMiddleware.ts @@ -0,0 +1,27 @@ +import pinoHttp from "pino-http"; +import { logger } from "../logging"; + +export const httpLogger = pinoHttp({ + logger, + customLogLevel: (_req, res, err) => { + if (res.statusCode >= 500 || err) return "error"; + if (res.statusCode >= 400) return "warn"; + return "info"; + }, + customSuccessMessage: (req, res) => { + return `${req.method} ${req.url} ${res.statusCode}`; + }, + customErrorMessage: (req, _res, err) => { + return `${req.method} ${req.url} failed: ${err.message}`; + }, + redact: ["req.headers.authorization"], + serializers: { + req: (req) => ({ + method: req.method, + url: req.url, + }), + res: (res) => ({ + statusCode: res.statusCode, + }), + }, +}); diff --git a/apps/server/src/controllers/index.ts b/apps/server/src/controllers/index.ts index 74b22f3..168e23a 100644 --- a/apps/server/src/controllers/index.ts +++ b/apps/server/src/controllers/index.ts @@ -1,3 +1,5 @@ export * from "./AuthController"; export * from "./ChatController"; export * from "./EventController"; +export * from "./AuthMiddleware"; +export * from "./LoggingMiddleware"; diff --git a/apps/server/src/logging/Logged.ts b/apps/server/src/logging/Logged.ts new file mode 100644 index 0000000..91703e9 --- /dev/null +++ b/apps/server/src/logging/Logged.ts @@ -0,0 +1,76 @@ +import { createLogger } from "./logger"; + +export function Logged(name: string) { + const log = createLogger(name); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function (Constructor: T) { + return class extends Constructor { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(...args: any[]) { + super(...args); + + // Return a Proxy that intercepts method calls lazily + return new Proxy(this, { + get(target, propKey, receiver) { + const original = Reflect.get(target, propKey, receiver); + + if (typeof original !== "function" || propKey === "constructor") { + return original; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const originalFn = original as (...args: any[]) => any; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function (this: any, ...methodArgs: any[]) { + const start = performance.now(); + const method = String(propKey); + + // Pino's redact handles sanitization - just pass args directly + log.debug({ method, args: methodArgs }, `${method} started`); + + const logCompletion = (err?: unknown) => { + const duration = Math.round(performance.now() - start); + if (err) { + const message = + err instanceof Error ? err.message : String(err); + log.error( + { method, duration, error: message }, + `${method} failed`, + ); + } else { + log.info({ method, duration }, `${method} completed`); + } + }; + + try { + const result = originalFn.apply(this, methodArgs); + + // Check if async - preserves sync/async nature of method + if (result instanceof Promise) { + return result + .then((val) => { + logCompletion(); + return val; + }) + .catch((err) => { + logCompletion(err); + throw err; + }); + } + + // Synchronous completion + logCompletion(); + return result; + } catch (err) { + logCompletion(err); + throw err; + } + }; + }, + }); + } + }; + }; +} diff --git a/apps/server/src/logging/index.ts b/apps/server/src/logging/index.ts new file mode 100644 index 0000000..af3416b --- /dev/null +++ b/apps/server/src/logging/index.ts @@ -0,0 +1,2 @@ +export { logger, createLogger, type Logger } from "./logger"; +export { Logged } from "./Logged"; diff --git a/apps/server/src/logging/logger.ts b/apps/server/src/logging/logger.ts new file mode 100644 index 0000000..eee73c5 --- /dev/null +++ b/apps/server/src/logging/logger.ts @@ -0,0 +1,43 @@ +import pino from "pino"; + +const isDevelopment = process.env.NODE_ENV !== "production"; + +export const logger = pino({ + level: process.env.LOG_LEVEL || (isDevelopment ? "debug" : "info"), + redact: { + paths: [ + // Root level + "password", + "passwordHash", + "token", + // One level deep (e.g. user.password) + "*.password", + "*.passwordHash", + "*.token", + // In arrays (for 'args' in decorator) + "args[*].password", + "args[*].passwordHash", + "args[*].token", + ], + censor: "[REDACTED]", + }, + transport: isDevelopment + ? { + target: "pino-pretty", + options: { + colorize: true, + translateTime: "SYS:HH:MM:ss", + ignore: "pid,hostname", + }, + } + : undefined, + base: { + service: "caldav-server", + }, +}); + +export function createLogger(module: string) { + return logger.child({ module }); +} + +export type Logger = pino.Logger; diff --git a/apps/server/src/middleware/index.ts b/apps/server/src/middleware/index.ts deleted file mode 100644 index 260e539..0000000 --- a/apps/server/src/middleware/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./AuthMiddleware"; diff --git a/apps/server/src/repositories/mongo/MongoChatRepository.ts b/apps/server/src/repositories/mongo/MongoChatRepository.ts index d4f9b1c..ea432ba 100644 --- a/apps/server/src/repositories/mongo/MongoChatRepository.ts +++ b/apps/server/src/repositories/mongo/MongoChatRepository.ts @@ -6,8 +6,10 @@ import { UpdateMessageDTO, } from "@caldav/shared"; import { ChatRepository } from "../../services/interfaces"; +import { Logged } from "../../logging"; import { ChatMessageModel, ConversationModel } from "./models"; +@Logged("MongoChatRepository") export class MongoChatRepository implements ChatRepository { // Conversations async getConversationsByUser(userId: string): Promise { diff --git a/apps/server/src/repositories/mongo/MongoEventRepository.ts b/apps/server/src/repositories/mongo/MongoEventRepository.ts index d2d304e..9b2fc03 100644 --- a/apps/server/src/repositories/mongo/MongoEventRepository.ts +++ b/apps/server/src/repositories/mongo/MongoEventRepository.ts @@ -1,7 +1,9 @@ import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from "@caldav/shared"; import { EventRepository } from "../../services/interfaces"; +import { Logged } from "../../logging"; import { EventModel } from "./models"; +@Logged("MongoEventRepository") export class MongoEventRepository implements EventRepository { async findById(id: string): Promise { const event = await EventModel.findById(id); diff --git a/apps/server/src/repositories/mongo/MongoUserRepository.ts b/apps/server/src/repositories/mongo/MongoUserRepository.ts index b669aa6..c8b44e8 100644 --- a/apps/server/src/repositories/mongo/MongoUserRepository.ts +++ b/apps/server/src/repositories/mongo/MongoUserRepository.ts @@ -1,7 +1,9 @@ import { User } from "@caldav/shared"; import { UserRepository, CreateUserData } from "../../services/interfaces"; +import { Logged } from "../../logging"; import { UserModel } from "./models"; +@Logged("MongoUserRepository") export class MongoUserRepository implements UserRepository { async findById(id: string): Promise { throw new Error("Not implemented"); diff --git a/apps/server/src/routes/chat.routes.ts b/apps/server/src/routes/chat.routes.ts index b273e50..14f64d2 100644 --- a/apps/server/src/routes/chat.routes.ts +++ b/apps/server/src/routes/chat.routes.ts @@ -1,6 +1,5 @@ import { Router } from "express"; -import { ChatController } from "../controllers"; -import { authenticate } from "../middleware"; +import { ChatController, authenticate } from "../controllers"; export function createChatRoutes(chatController: ChatController): Router { const router = Router(); diff --git a/apps/server/src/routes/event.routes.ts b/apps/server/src/routes/event.routes.ts index c68e47a..1584e3c 100644 --- a/apps/server/src/routes/event.routes.ts +++ b/apps/server/src/routes/event.routes.ts @@ -1,6 +1,5 @@ import { Router } from "express"; -import { EventController } from "../controllers"; -import { authenticate } from "../middleware"; +import { EventController, authenticate } from "../controllers"; export function createEventRoutes(eventController: EventController): Router { const router = Router(); diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index b1cceaa..9a7189e 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -7,7 +7,8 @@ "strict": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, - "skipLibCheck": true + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": ["src"] } diff --git a/package-lock.json b/package-lock.json index f820ba7..047c12e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", + "react-native-logs": "^5.5.0", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "5.6.0", "react-native-screens": "~4.16.0", @@ -69,6 +70,8 @@ "jsonwebtoken": "^9.0.3", "mongoose": "^9.1.1", "openai": "^6.15.0", + "pino": "^10.1.1", + "pino-http": "^11.0.0", "rrule": "^2.8.1" }, "devDependencies": { @@ -76,6 +79,7 @@ "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.10.1", + "pino-pretty": "^13.1.3", "tsx": "^4.21.0", "typescript": "^5.9.3" } @@ -3317,6 +3321,12 @@ "node": ">=12.4.0" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", @@ -5136,6 +5146,15 @@ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "license": "MIT" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -5957,6 +5976,13 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -6249,6 +6275,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -6493,6 +6529,16 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/env-editor": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", @@ -7896,6 +7942,13 @@ "node": ">= 0.8" } }, + "node_modules/fast-copy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", + "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7943,6 +7996,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -8552,6 +8612,13 @@ "node": ">= 0.4" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true, + "license": "MIT" + }, "node_modules/hermes-estree": { "version": "0.29.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz", @@ -9507,6 +9574,16 @@ "jiti": "bin/jiti.js" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -11174,6 +11251,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -11545,6 +11631,93 @@ "node": ">=0.10.0" } }, + "node_modules/pino": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.1.tgz", + "integrity": "sha512-3qqVfpJtRQUCAOs4rTOEwLH6mwJJ/CSAlbis8fKOiMzTtXh0HN/VLsn3UWVTJ7U8DsWmxeNon2IpGb+wORXH4g==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-http": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-11.0.0.tgz", + "integrity": "sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==", + "license": "MIT", + "dependencies": { + "get-caller-file": "^2.0.5", + "pino": "^10.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -11897,6 +12070,22 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -11960,6 +12149,17 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -12039,6 +12239,12 @@ ], "license": "MIT" }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -12557,6 +12763,12 @@ "react-native": "*" } }, + "node_modules/react-native-logs": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-native-logs/-/react-native-logs-5.5.0.tgz", + "integrity": "sha512-H3Jc1pNTzNhYb9yHuk1drHdyGHwRvt4IERSz3EUul8vVTey6999fzGRFLK6ugrxYnmw7P+5fo/mRzDXeByhA8g==", + "license": "MIT" + }, "node_modules/react-native-reanimated": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz", @@ -12840,6 +13052,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -13233,6 +13454,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -13251,6 +13481,23 @@ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -13686,6 +13933,15 @@ "node": ">=8.0.0" } }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -13741,6 +13997,15 @@ "node": ">=6" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -14249,6 +14514,18 @@ "node": ">=0.8" } }, + "node_modules/thread-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", + "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz",