diff --git a/apps/bridge-frontend/README.md b/apps/bridge-frontend/README.md index c403366..ebec248 100644 --- a/apps/bridge-frontend/README.md +++ b/apps/bridge-frontend/README.md @@ -1,36 +1,133 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +# Bridge Frontend -## Getting Started +Frontend application for managing communication bridges between various messaging platforms and the CDR Link system. -First, run the development server: +## Overview + +Bridge Frontend provides a web interface for configuring and managing communication channels including Signal, WhatsApp, Facebook, and Voice integrations. It handles bot registration, webhook configuration, and channel settings. + +## Features + +- **Channel Management**: Configure Signal, WhatsApp, Facebook, and Voice channels +- **Bot Registration**: Register and manage bots for each communication platform +- **Webhook Configuration**: Set up webhooks for message routing +- **Settings Management**: Configure channel-specific settings and behaviors +- **User Authentication**: Secure access with NextAuth.js + +## Development + +### Prerequisites + +- Node.js >= 20 +- npm >= 10 +- PostgreSQL database +- Running bridge-worker service + +### Setup ```bash +# Install dependencies +npm install + +# Run database migrations +npm run migrate:latest + +# Run development server npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev + +# Build for production +npm run build + +# Start production server +npm run start ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +### Environment Variables -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +Required environment variables: -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. +- `DATABASE_URL` - PostgreSQL connection string +- `DATABASE_HOST` - Database host +- `DATABASE_NAME` - Database name +- `DATABASE_USER` - Database username +- `DATABASE_PASSWORD` - Database password +- `NEXTAUTH_URL` - Application URL +- `NEXTAUTH_SECRET` - NextAuth.js secret +- `GOOGLE_CLIENT_ID` - Google OAuth client ID +- `GOOGLE_CLIENT_SECRET` - Google OAuth client secret -## Learn More +### Available Scripts -To learn more about Next.js, take a look at the following resources: +- `npm run dev` - Start development server +- `npm run build` - Build for production +- `npm run start` - Start production server +- `npm run lint` - Run ESLint +- `npm run migrate:latest` - Run all pending migrations +- `npm run migrate:down` - Rollback last migration +- `npm run migrate:up` - Run next migration +- `npm run migrate:make` - Create new migration -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +## Architecture -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! +### Database Schema -## Deploy on Vercel +The application manages the following main entities: -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +- **Bots**: Communication channel bot configurations +- **Webhooks**: Webhook endpoints for external integrations +- **Settings**: Channel-specific configuration settings +- **Users**: User accounts with role-based permissions -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +### API Routes + +- `/api/auth` - Authentication endpoints +- `/api/[service]/bots` - Bot management for each service +- `/api/[service]/webhooks` - Webhook configuration + +### Page Structure + +- `/` - Dashboard/home page +- `/login` - Authentication page +- `/[...segment]` - Dynamic routing for CRUD operations + - `@create` - Create new entities + - `@detail` - View entity details + - `@edit` - Edit existing entities + +## Integration + +### Database Access + +Uses Kysely ORM for type-safe database queries: + +```typescript +import { db } from '@link-stack/database' + +const bots = await db + .selectFrom('bots') + .selectAll() + .execute() +``` + +### Authentication + +Integrated with NextAuth.js using database adapter: + +```typescript +import { authOptions } from '@link-stack/auth' +``` + +## Docker Support + +```bash +# Build image +docker build -t link-stack/bridge-frontend . + +# Run with docker-compose +docker-compose -f docker/compose/bridge.yml up +``` + +## Related Services + +- **bridge-worker**: Processes messages from configured channels +- **bridge-whatsapp**: WhatsApp-specific integration service +- **bridge-migrations**: Database schema management \ No newline at end of file diff --git a/apps/bridge-frontend/app/(main)/[...segment]/@create/page.tsx b/apps/bridge-frontend/app/(main)/[...segment]/@create/page.tsx index 3973926..bc5b6b8 100644 --- a/apps/bridge-frontend/app/(main)/[...segment]/@create/page.tsx +++ b/apps/bridge-frontend/app/(main)/[...segment]/@create/page.tsx @@ -1,10 +1,11 @@ import { Create } from "@link-stack/bridge-ui"; type PageProps = { - params: { segment: string[] }; + params: Promise<{ segment: string[] }>; }; -export default function Page({ params: { segment } }: PageProps) { +export default async function Page({ params }: PageProps) { + const { segment } = await params; const service = segment[0]; return ; diff --git a/apps/bridge-frontend/app/(main)/[...segment]/@detail/page.tsx b/apps/bridge-frontend/app/(main)/[...segment]/@detail/page.tsx index 0b1f49c..e857b7d 100644 --- a/apps/bridge-frontend/app/(main)/[...segment]/@detail/page.tsx +++ b/apps/bridge-frontend/app/(main)/[...segment]/@detail/page.tsx @@ -1,11 +1,12 @@ import { db } from "@link-stack/bridge-common"; import { serviceConfig, Detail } from "@link-stack/bridge-ui"; -type Props = { - params: { segment: string[] }; +type PageProps = { + params: Promise<{ segment: string[] }>; }; -export default async function Page({ params: { segment } }: Props) { +export default async function Page({ params }: PageProps) { + const { segment } = await params; const service = segment[0]; const id = segment?.[1]; diff --git a/apps/bridge-frontend/app/(main)/[...segment]/@edit/page.tsx b/apps/bridge-frontend/app/(main)/[...segment]/@edit/page.tsx index 59977eb..82c8052 100644 --- a/apps/bridge-frontend/app/(main)/[...segment]/@edit/page.tsx +++ b/apps/bridge-frontend/app/(main)/[...segment]/@edit/page.tsx @@ -2,10 +2,11 @@ import { db } from "@link-stack/bridge-common"; import { serviceConfig, Edit } from "@link-stack/bridge-ui"; type PageProps = { - params: { segment: string[] }; + params: Promise<{ segment: string[] }>; }; -export default async function Page({ params: { segment } }: PageProps) { +export default async function Page({ params }: PageProps) { + const { segment } = await params; const service = segment[0]; const id = segment?.[1]; diff --git a/apps/bridge-frontend/app/(main)/[...segment]/page.tsx b/apps/bridge-frontend/app/(main)/[...segment]/page.tsx index e248a86..7b4fc03 100644 --- a/apps/bridge-frontend/app/(main)/[...segment]/page.tsx +++ b/apps/bridge-frontend/app/(main)/[...segment]/page.tsx @@ -2,12 +2,13 @@ import { db } from "@link-stack/bridge-common"; import { serviceConfig, List } from "@link-stack/bridge-ui"; type PageProps = { - params: { + params: Promise<{ segment: string[]; - }; + }>; }; -export default async function Page({ params: { segment } }: PageProps) { +export default async function Page({ params }: PageProps) { + const { segment } = await params; const service = segment[0]; if (!service) return null; diff --git a/apps/bridge-frontend/package.json b/apps/bridge-frontend/package.json index b5797a6..c955522 100644 --- a/apps/bridge-frontend/package.json +++ b/apps/bridge-frontend/package.json @@ -1,6 +1,6 @@ { "name": "@link-stack/bridge-frontend", - "version": "2.2.0", + "version": "3.1.0", "type": "module", "scripts": { "dev": "next dev", @@ -13,28 +13,30 @@ "migrate:down:one": "tsx database/migrate.ts down:one" }, "dependencies": { - "@auth/kysely-adapter": "^1.5.2", + "@auth/kysely-adapter": "^1.8.0", + "@mui/icons-material": "^6", + "@mui/material": "^6", + "@mui/material-nextjs": "^6", + "@mui/x-license": "^7.28.0", "@link-stack/bridge-common": "*", "@link-stack/bridge-ui": "*", - "@link-stack/ui": "*", - "@mui/icons-material": "^5", - "@mui/material": "^5", - "@mui/material-nextjs": "^5", - "@mui/x-license": "^7.18.0", - "next": "^14.2.23", - "next-auth": "^4.24.8", - "react": "18.3.1", - "react-dom": "18.3.1", + "next": "15.2.3", + "next-auth": "^4.24.11", + "react": "19.0.0", + "react-dom": "19.0.0", "sharp": "^0.33.5", - "tsx": "^4.19.1" + "tsx": "^4.19.3", + "@link-stack/ui": "*" }, "devDependencies": { "@link-stack/eslint-config": "*", "@link-stack/typescript-config": "*", "@types/node": "^22", - "@types/pg": "^8.11.10", - "@types/react": "^18", - "@types/react-dom": "^18", + "@types/pg": "^8.11.11", + "@types/react": "^19", + "@types/react-dom": "^19", + "@link-stack/eslint-config": "*", + "@link-stack/typescript-config": "*", "typescript": "^5" } } diff --git a/apps/bridge-frontend/public/robots.txt b/apps/bridge-frontend/public/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/apps/bridge-frontend/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/apps/bridge-frontend/tsconfig.json b/apps/bridge-frontend/tsconfig.json index e700859..22be23b 100644 --- a/apps/bridge-frontend/tsconfig.json +++ b/apps/bridge-frontend/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -14,14 +18,24 @@ "jsx": "preserve", "incremental": true, "paths": { - "@/*": ["./*"] + "@/*": [ + "./*" + ] }, "plugins": [ { "name": "next" } - ] + ], + "target": "ES2017" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } diff --git a/apps/bridge-migrations/README.md b/apps/bridge-migrations/README.md new file mode 100644 index 0000000..a9e45e3 --- /dev/null +++ b/apps/bridge-migrations/README.md @@ -0,0 +1,158 @@ +# Bridge Migrations + +Database migration management for the CDR Link bridge system. + +## Overview + +Bridge Migrations handles database schema versioning and migrations for all bridge-related tables using Kysely migration framework. It manages the database structure for authentication, messaging channels, webhooks, and settings. + +## Features + +- **Schema Versioning**: Track and apply database schema changes +- **Up/Down Migrations**: Support for rolling forward and backward +- **Type-Safe Migrations**: TypeScript-based migration files +- **Migration History**: Track applied migrations in the database +- **Multiple Migration Strategies**: Run all, run one, or rollback migrations + +## Migration Files + +Current migrations in order: + +1. **0001-add-next-auth.ts** - NextAuth.js authentication tables +2. **0002-add-signal.ts** - Signal messenger integration +3. **0003-add-whatsapp.ts** - WhatsApp integration +4. **0004-add-voice.ts** - Voice/Twilio integration +5. **0005-add-facebook.ts** - Facebook Messenger integration +6. **0006-add-webhooks.ts** - Webhook configuration +7. **0007-add-settings.ts** - Application settings +8. **0008-add-user-role.ts** - User role management + +## Development + +### Prerequisites + +- Node.js >= 20 +- npm >= 10 +- PostgreSQL database +- Database connection credentials + +### Setup + +```bash +# Install dependencies +npm install + +# Run all pending migrations +npm run migrate:latest + +# Check migration status +npm run migrate:list +``` + +### Environment Variables + +Required environment variables: + +- `DATABASE_URL` - PostgreSQL connection string +- `DATABASE_HOST` - Database host +- `DATABASE_NAME` - Database name +- `DATABASE_USER` - Database username +- `DATABASE_PASSWORD` - Database password + +### Available Scripts + +- `npm run migrate:latest` - Run all pending migrations +- `npm run migrate:up` - Run next pending migration +- `npm run migrate:down` - Rollback last migration +- `npm run migrate:up:all` - Run all migrations (alias) +- `npm run migrate:up:one` - Run one migration +- `npm run migrate:down:all` - Rollback all migrations +- `npm run migrate:down:one` - Rollback one migration +- `npm run migrate:list` - List migration status +- `npm run migrate:make ` - Create new migration file + +## Creating New Migrations + +To create a new migration: + +```bash +npm run migrate:make add-new-feature +``` + +This creates a new timestamped migration file in the `migrations/` directory. + +Example migration structure: + +```typescript +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('new_table') + .addColumn('id', 'serial', (col) => col.primaryKey()) + .addColumn('name', 'varchar', (col) => col.notNull()) + .addColumn('created_at', 'timestamp', (col) => + col.defaultTo('now()').notNull() + ) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('new_table').execute() +} +``` + +## Database Schema + +### Core Tables + +- **users** - User accounts with roles +- **accounts** - OAuth account connections +- **sessions** - User sessions +- **verification_tokens** - Email verification + +### Communication Tables + +- **bots** - Bot configurations for each service +- **signal_messages** - Signal message history +- **whatsapp_messages** - WhatsApp message history +- **voice_messages** - Voice/call records +- **facebook_messages** - Facebook message history + +### Configuration Tables + +- **webhooks** - External webhook endpoints +- **settings** - Application settings + +## Best Practices + +1. **Test Migrations**: Always test migrations in development first +2. **Backup Database**: Create backups before running migrations in production +3. **Review Changes**: Review migration files before applying +4. **Atomic Operations**: Keep migrations focused and atomic +5. **Rollback Plan**: Ensure down() methods properly reverse changes + +## Troubleshooting + +### Common Issues + +1. **Migration Failed**: Check error logs and database permissions +2. **Locked Migrations**: Check for concurrent migration processes +3. **Missing Tables**: Ensure all previous migrations have run +4. **Connection Issues**: Verify DATABASE_URL and network access + +### Recovery + +If migrations fail: + +1. Check migration history table +2. Manually verify database state +3. Run specific migrations as needed +4. Use rollback if necessary + +## Integration + +Migrations are used by: +- **bridge-frontend** - Requires migrated schema +- **bridge-worker** - Depends on message tables +- **bridge-whatsapp** - Uses bot configuration tables \ No newline at end of file diff --git a/apps/bridge-migrations/migrate.ts b/apps/bridge-migrations/migrate.ts index 2e6281b..d06b606 100644 --- a/apps/bridge-migrations/migrate.ts +++ b/apps/bridge-migrations/migrate.ts @@ -72,7 +72,7 @@ export const migrate = async (arg: string) => { results?.forEach((it) => { if (it.status === "Success") { - console.log( + console.info( `Migration "${it.migrationName} ${it.direction.toLowerCase()}" was executed successfully`, ); } else if (it.status === "Error") { diff --git a/apps/bridge-migrations/package.json b/apps/bridge-migrations/package.json index c3a10b6..4254de8 100644 --- a/apps/bridge-migrations/package.json +++ b/apps/bridge-migrations/package.json @@ -1,6 +1,6 @@ { "name": "@link-stack/bridge-migrations", - "version": "2.2.0", + "version": "3.1.0", "type": "module", "scripts": { "migrate:up:all": "tsx migrate.ts up:all", @@ -9,14 +9,14 @@ "migrate:down:one": "tsx migrate.ts down:one" }, "dependencies": { - "dotenv": "^16.4.5", - "kysely": "0.26.1", - "pg": "^8.13.0", - "tsx": "^4.19.1" + "dotenv": "^16.4.7", + "kysely": "0.27.6", + "pg": "^8.14.1", + "tsx": "^4.19.3" }, "devDependencies": { "@types/node": "^22", - "@types/pg": "^8.11.10", + "@types/pg": "^8.11.11", "@link-stack/eslint-config": "*", "@link-stack/typescript-config": "*", "typescript": "^5" diff --git a/apps/bridge-whatsapp/README.md b/apps/bridge-whatsapp/README.md new file mode 100644 index 0000000..705ced2 --- /dev/null +++ b/apps/bridge-whatsapp/README.md @@ -0,0 +1,172 @@ +# Bridge WhatsApp + +WhatsApp integration service for the CDR Link communication bridge system. + +## Overview + +Bridge WhatsApp provides a REST API for sending and receiving WhatsApp messages using the Baileys library (WhatsApp Web API). It handles bot session management, message routing, and media processing for WhatsApp communication channels. + +## Features + +- **Bot Management**: Register and manage multiple WhatsApp bot sessions +- **Message Handling**: Send and receive text messages with formatting +- **Media Support**: Handle images, documents, audio, and video files +- **QR Code Authentication**: Web-based WhatsApp authentication +- **REST API**: Simple HTTP endpoints for integration + +## Development + +### Prerequisites + +- Node.js >= 20 +- npm >= 10 +- PostgreSQL database (for bot configuration) + +### Setup + +```bash +# Install dependencies +npm install + +# Build TypeScript +npm run build + +# Run development server +npm run dev + +# Start production server +npm run start +``` + +### Environment Variables + +- `PORT` - Server port (default: 5000) +- `DATABASE_URL` - PostgreSQL connection string (optional) +- Additional WhatsApp-specific configuration as needed + +### Available Scripts + +- `npm run build` - Compile TypeScript +- `npm run dev` - Development mode with auto-reload +- `npm run start` - Start production server + +## API Endpoints + +### Bot Management + +- `POST /api/bots/:token` - Register/initialize a bot +- `GET /api/bots/:token` - Get bot status and QR code + +### Messaging + +- `POST /api/bots/:token/send` - Send a message +- `POST /api/bots/:token/receive` - Webhook for incoming messages + +### Request/Response Format + +#### Send Message + +```json +{ + "to": "1234567890@s.whatsapp.net", + "message": "Hello World", + "media": { + "url": "https://example.com/image.jpg", + "type": "image" + } +} +``` + +#### Receive Message Webhook + +```json +{ + "from": "1234567890@s.whatsapp.net", + "message": "Hello", + "timestamp": "2024-01-01T00:00:00Z", + "media": { + "url": "https://...", + "type": "image", + "mimetype": "image/jpeg" + } +} +``` + +## Architecture + +### Server Framework + +Built with Hapi.js for: + +- Route validation +- Plugin architecture +- Error handling +- Request lifecycle + +### WhatsApp Integration + +Uses @whiskeysockets/baileys: + +- WhatsApp Web protocol +- Multi-device support +- Message encryption +- Media handling + +### Session Management + +- File-based session storage +- Automatic reconnection +- QR code regeneration +- Session cleanup + +## Media Handling + +Supported media types: + +- **Images**: JPEG, PNG, GIF +- **Documents**: PDF, DOC, DOCX +- **Audio**: MP3, OGG, WAV +- **Video**: MP4, AVI + +Media is processed and uploaded before sending. + +## Error Handling + +- Connection errors trigger reconnection +- Invalid sessions regenerate QR codes +- API errors return appropriate HTTP status codes +- Comprehensive logging for debugging + +## Security + +- Token-based bot authentication +- Message validation +- Rate limiting (configurable) +- Secure session storage + +## Integration + +Designed to work with: + +- **bridge-worker**: Processes WhatsApp message jobs +- **bridge-frontend**: Manages bot configuration +- External webhooks for message routing + +## Docker Support + +```bash +# Build image +docker build -t link-stack/bridge-whatsapp . + +# Run container +docker run -p 5000:5000 link-stack/bridge-whatsapp +``` + +## Testing + +While test configuration exists (jest.config.json), tests should be implemented for: + +- API endpoint validation +- Message processing logic +- Session management +- Error scenarios diff --git a/apps/bridge-whatsapp/package.json b/apps/bridge-whatsapp/package.json index a998164..02fb713 100644 --- a/apps/bridge-whatsapp/package.json +++ b/apps/bridge-whatsapp/package.json @@ -1,26 +1,26 @@ { "name": "@link-stack/bridge-whatsapp", - "version": "2.2.0", + "version": "3.1.0", "main": "build/main/index.js", "author": "Darren Clarke ", "license": "AGPL-3.0-or-later", "dependencies": { "@adiwajshing/keyed-db": "0.2.4", - "@hapi/hapi": "^21.3.10", + "@hapi/hapi": "^21.4.0", "@hapipal/schmervice": "^3.0.0", "@hapipal/toys": "^4.0.0", - "@whiskeysockets/baileys": "^6.7.8", + "@whiskeysockets/baileys": "^6.7.16", "hapi-pino": "^12.1.0", - "link-preview-js": "^3.0.5" + "link-preview-js": "^3.0.14" }, "devDependencies": { "@link-stack/eslint-config": "*", "@link-stack/jest-config": "*", "@link-stack/typescript-config": "*", "@types/node": "*", - "dotenv-cli": "^7.4.2", - "tsx": "^4.19.1", - "typescript": "^5.6.2" + "dotenv-cli": "^8.0.0", + "tsx": "^4.19.3", + "typescript": "^5.8.2" }, "scripts": { "build": "tsc -p tsconfig.json", diff --git a/apps/bridge-whatsapp/src/routes.ts b/apps/bridge-whatsapp/src/routes.ts index 10c9757..9219bf0 100644 --- a/apps/bridge-whatsapp/src/routes.ts +++ b/apps/bridge-whatsapp/src/routes.ts @@ -26,7 +26,6 @@ export const SendMessageRoute = withDefaults({ description: "Send a message", async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const { id } = request.params; - console.log({ payload: request.payload }); const { phoneNumber, message } = request.payload as MessageRequest; const whatsappService = getService(request); await whatsappService.send(id, phoneNumber, message as string); diff --git a/apps/bridge-whatsapp/src/service.ts b/apps/bridge-whatsapp/src/service.ts index 4f6eafa..1fcc881 100644 --- a/apps/bridge-whatsapp/src/service.ts +++ b/apps/bridge-whatsapp/src/service.ts @@ -57,7 +57,7 @@ export default class WhatsappService extends Service { try { connection.end(null); } catch (error) { - console.log(error); + console.error(error); } } this.connections = {}; @@ -92,27 +92,27 @@ export default class WhatsappService extends Service { isNewLogin, } = update; if (qr) { - console.log("got qr code"); + console.info("got qr code"); const botDirectory = this.getBotDirectory(botID); const qrPath = `${botDirectory}/qr.txt`; fs.writeFileSync(qrPath, qr, "utf8"); } else if (isNewLogin) { - console.log("got new login"); + console.info("got new login"); const botDirectory = this.getBotDirectory(botID); const verifiedFile = `${botDirectory}/verified`; fs.writeFileSync(verifiedFile, ""); } else if (connectionState === "open") { - console.log("opened connection"); + console.info("opened connection"); } else if (connectionState === "close") { - console.log("connection closed due to ", lastDisconnect?.error); + console.info("connection closed due to ", lastDisconnect?.error); const disconnectStatusCode = (lastDisconnect?.error as any)?.output ?.statusCode; if (disconnectStatusCode === DisconnectReason.restartRequired) { - console.log("reconnecting after got new login"); + console.info("reconnecting after got new login"); await this.createConnection(botID, server, options); authCompleteCallback?.(); } else if (disconnectStatusCode !== DisconnectReason.loggedOut) { - console.log("reconnecting"); + console.info("reconnecting"); await this.sleep(pause); pause *= 2; this.createConnection(botID, server, options); @@ -121,12 +121,12 @@ export default class WhatsappService extends Service { } if (events["creds.update"]) { - console.log("creds update"); + console.info("creds update"); await saveCreds(); } if (events["messages.upsert"]) { - console.log("messages upsert"); + console.info("messages upsert"); const upsert = events["messages.upsert"]; const { messages } = upsert; if (messages) { @@ -143,13 +143,13 @@ export default class WhatsappService extends Service { const baseDirectory = this.getBaseDirectory(); const botIDs = fs.readdirSync(baseDirectory); - console.log({ botIDs }); + for await (const botID of botIDs) { const directory = this.getBotDirectory(botID); const verifiedFile = `${directory}/verified`; if (fs.existsSync(verifiedFile)) { const { version, isLatest } = await fetchLatestBaileysVersion(); - console.log(`using WA v${version.join(".")}, isLatest: ${isLatest}`); + console.info(`using WA v${version.join(".")}, isLatest: ${isLatest}`); await this.createConnection(botID, this.server, { browser: WhatsappService.browserDescription, @@ -169,7 +169,10 @@ export default class WhatsappService extends Service { message, messageTimestamp, } = webMessageInfo; - console.log(webMessageInfo); + console.info("Message type debug"); + for (const key in message) { + console.info(key, !!message[key as keyof proto.IMessage]); + } const isValidMessage = message && remoteJid !== "status@broadcast" && !fromMe; if (isValidMessage) { diff --git a/apps/bridge-worker/README.md b/apps/bridge-worker/README.md new file mode 100644 index 0000000..8e5638a --- /dev/null +++ b/apps/bridge-worker/README.md @@ -0,0 +1,147 @@ +# Bridge Worker + +Background job processor for handling asynchronous tasks in the CDR Link communication bridge system. + +## Overview + +Bridge Worker uses Graphile Worker to process queued jobs for message handling, media conversion, webhook notifications, and scheduled tasks. It manages the flow of messages between various communication channels (Signal, WhatsApp, Facebook, Voice) and the Zammad ticketing system. + +## Features + +- **Message Processing**: Handle incoming/outgoing messages for all supported channels +- **Media Conversion**: Convert audio/video files between formats +- **Webhook Notifications**: Notify external systems of events +- **Scheduled Tasks**: Cron-based job scheduling +- **Job Queue Management**: Reliable job processing with retries +- **Multi-Channel Support**: Signal, WhatsApp, Facebook, Voice (Twilio) + +## Development + +### Prerequisites + +- Node.js >= 20 +- npm >= 10 +- PostgreSQL database +- Redis (for caching) +- FFmpeg (for media conversion) + +### Setup + +```bash +# Install dependencies +npm install + +# Build TypeScript +npm run build + +# Run development server with auto-reload +npm run dev + +# Start production worker +npm run start +``` + +### Environment Variables + +Required environment variables: + +- `DATABASE_URL` - PostgreSQL connection string +- `GRAPHILE_WORKER_CONCURRENCY` - Number of concurrent jobs (default: 10) +- `GRAPHILE_WORKER_POLL_INTERVAL` - Job poll interval in ms (default: 1000) +- `ZAMMAD_URL` - Zammad instance URL +- `ZAMMAD_API_TOKEN` - Zammad API token +- `TWILIO_ACCOUNT_SID` - Twilio account SID +- `TWILIO_AUTH_TOKEN` - Twilio auth token +- `SIGNAL_CLI_URL` - Signal CLI REST API URL +- `WHATSAPP_SERVICE_URL` - WhatsApp bridge service URL +- `FACEBOOK_APP_SECRET` - Facebook app secret +- `FACEBOOK_PAGE_ACCESS_TOKEN` - Facebook page token + +### Available Scripts + +- `npm run build` - Compile TypeScript +- `npm run dev` - Development mode with watch +- `npm run start` - Start production worker + +## Task Types + +### Signal Tasks +- `receive-signal-message` - Process incoming Signal messages +- `send-signal-message` - Send outgoing Signal messages +- `fetch-signal-messages` - Fetch messages from Signal CLI + +### WhatsApp Tasks +- `receive-whatsapp-message` - Process incoming WhatsApp messages +- `send-whatsapp-message` - Send outgoing WhatsApp messages + +### Facebook Tasks +- `receive-facebook-message` - Process incoming Facebook messages +- `send-facebook-message` - Send outgoing Facebook messages + +### Voice Tasks +- `receive-voice-message` - Process incoming voice calls/messages +- `send-voice-message` - Send voice messages via Twilio +- `twilio-recording` - Handle Twilio call recordings +- `voice-line-audio-update` - Update voice line audio +- `voice-line-delete` - Delete voice line +- `voice-line-provider-update` - Update voice provider settings + +### Common Tasks +- `notify-webhooks` - Send webhook notifications + +### Leafcutter Tasks +- `import-leafcutter` - Import data to Leafcutter +- `import-label-studio` - Import Label Studio annotations + +## Architecture + +### Job Processing + +Jobs are queued in PostgreSQL using Graphile Worker: + +```typescript +await addJob('send-signal-message', { + to: '+1234567890', + message: 'Hello world' +}) +``` + +### Cron Schedule + +Scheduled tasks are configured in `crontab`: +- Periodic message fetching +- Cleanup tasks +- Health checks + +### Error Handling + +- Automatic retries with exponential backoff +- Dead letter queue for failed jobs +- Comprehensive logging with winston + +## Media Handling + +Supports conversion between formats: +- Audio: MP3, OGG, WAV, M4A +- Uses fluent-ffmpeg for processing +- Automatic format detection + +## Integration Points + +- **Zammad**: Creates/updates tickets via API +- **Signal CLI**: REST API for Signal messaging +- **WhatsApp Bridge**: HTTP API for WhatsApp +- **Twilio**: Voice and SMS capabilities +- **Facebook**: Graph API for Messenger + +## Docker Support + +```bash +# Build image +docker build -t link-stack/bridge-worker . + +# Run with docker-compose +docker-compose -f docker/compose/bridge.yml up +``` + +The worker includes cron support via built-in crontab. \ No newline at end of file diff --git a/apps/bridge-worker/graphile.config.ts b/apps/bridge-worker/graphile.config.ts index 8b550c7..9367a65 100644 --- a/apps/bridge-worker/graphile.config.ts +++ b/apps/bridge-worker/graphile.config.ts @@ -4,8 +4,12 @@ import type {} from "graphile-worker"; const preset: GraphileConfig.Preset = { worker: { connectionString: process.env.DATABASE_URL, - maxPoolSize: 10, - pollInterval: 2000, + maxPoolSize: process.env.BRIDGE_WORKER_POOL_SIZE + ? parseInt(process.env.BRIDGE_WORKER_POOL_SIZE, 10) + : 10, + pollInterval: process.env.BRIDGE_WORKER_POLL_INTERVAL + ? parseInt(process.env.BRIDGE_WORKER_POLL_INTERVAL, 10) + : 2000, fileExtensions: [".ts"], }, }; diff --git a/apps/bridge-worker/index.ts b/apps/bridge-worker/index.ts index 5a7dbd5..a0cb1d8 100644 --- a/apps/bridge-worker/index.ts +++ b/apps/bridge-worker/index.ts @@ -6,13 +6,20 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const startWorker = async () => { - console.log("Starting worker..."); - console.log(process.env); + console.info("Starting worker..."); + await run({ connectionString: process.env.DATABASE_URL, - concurrency: 10, noHandleSignals: false, - pollInterval: 1000, + concurrency: process.env.BRIDGE_WORKER_CONCURRENCY + ? parseInt(process.env.BRIDGE_WORKER_CONCURRENCY, 10) + : 10, + maxPoolSize: process.env.BRIDGE_WORKER_POOL_SIZE + ? parseInt(process.env.BRIDGE_WORKER_POOL_SIZE, 10) + : 10, + pollInterval: process.env.BRIDGE_WORKER_POLL_INTERVAL + ? parseInt(process.env.BRIDGE_WORKER_POLL_INTERVAL, 10) + : 1000, taskDirectory: `${__dirname}/tasks`, crontabFile: `${__dirname}/crontab`, }); diff --git a/apps/bridge-worker/lib/common.ts b/apps/bridge-worker/lib/common.ts index 26e4aeb..ff167b3 100644 --- a/apps/bridge-worker/lib/common.ts +++ b/apps/bridge-worker/lib/common.ts @@ -62,9 +62,8 @@ export const createZammadTicket = async ( }, }); } catch (error: any) { - console.log(Object.keys(error)); if (error.isBoom) { - console.log(error.output); + console.error(error.output); throw new Error("Failed to create zamamd ticket"); } } diff --git a/apps/bridge-worker/lib/media-convert.ts b/apps/bridge-worker/lib/media-convert.ts index bc6a22a..587e893 100644 --- a/apps/bridge-worker/lib/media-convert.ts +++ b/apps/bridge-worker/lib/media-convert.ts @@ -25,7 +25,7 @@ const defaultAudioConvertOpts = { **/ export const convert = ( input: Buffer, - opts?: AudioConvertOpts + opts?: AudioConvertOpts, ): Promise => { const settings = { ...defaultAudioConvertOpts, ...opts }; return new Promise((resolve, reject) => { @@ -35,12 +35,8 @@ export const convert = ( .audioCodec(settings.audioCodec) .audioBitrate(settings.bitrate) .toFormat(settings.format) - .on("error", (err, stdout, stderr) => { + .on("error", (err, _stdout, _stderr) => { console.error(err.message); - console.log("FFMPEG OUTPUT"); - console.log(stdout); - console.log("FFMPEG ERROR"); - console.log(stderr); reject(err); }) .on("end", () => { @@ -66,8 +62,12 @@ export const selfCheck = (): Promise => { resolve(false); } - const preds = R.map(requiredCodecs, (codec) => (available: any) => - available[codec] && available[codec].canDemux && available[codec].canMux + const preds = R.map( + requiredCodecs, + (codec) => (available: any) => + available[codec] && + available[codec].canDemux && + available[codec].canMux, ); resolve(R.allPass(codecs, preds)); @@ -79,6 +79,6 @@ export const assertFfmpegAvailable = async (): Promise => { const r = await selfCheck(); if (!r) throw new Error( - `ffmpeg is not installed, could not be located, or does not support the required codecs: ${requiredCodecs}` + `ffmpeg is not installed, could not be located, or does not support the required codecs: ${requiredCodecs}`, ); }; diff --git a/apps/bridge-worker/package.json b/apps/bridge-worker/package.json index aa162e9..86e88fe 100644 --- a/apps/bridge-worker/package.json +++ b/apps/bridge-worker/package.json @@ -1,6 +1,6 @@ { "name": "@link-stack/bridge-worker", - "version": "2.2.0", + "version": "3.1.0", "type": "module", "main": "build/main/index.js", "author": "Darren Clarke ", @@ -16,14 +16,14 @@ "@link-stack/signal-api": "*", "fluent-ffmpeg": "^2.1.3", "graphile-worker": "^0.16.6", - "remeda": "^2.14.0", - "twilio": "^5.3.2" + "remeda": "^2.21.2", + "twilio": "^5.5.1" }, "devDependencies": { - "@types/fluent-ffmpeg": "^2.1.26", - "dotenv-cli": "^7.4.2", + "@types/fluent-ffmpeg": "^2.1.27", + "dotenv-cli": "^8.0.0", "@link-stack/eslint-config": "*", "@link-stack/typescript-config": "*", - "typescript": "^5.6.2" + "typescript": "^5.8.2" } } diff --git a/apps/bridge-worker/tasks/common/notify-webhooks.ts b/apps/bridge-worker/tasks/common/notify-webhooks.ts index 0ed2c17..3d81f93 100644 --- a/apps/bridge-worker/tasks/common/notify-webhooks.ts +++ b/apps/bridge-worker/tasks/common/notify-webhooks.ts @@ -19,13 +19,11 @@ const notifyWebhooksTask = async ( for (const webhook of webhooks) { const { endpointUrl, httpMethod, headers } = webhook; const finalHeaders = { "Content-Type": "application/json", ...headers }; - console.log({ endpointUrl, httpMethod, headers, finalHeaders }); const result = await fetch(endpointUrl, { method: httpMethod, headers: finalHeaders, body: JSON.stringify(payload), }); - console.log(result); } }; diff --git a/apps/bridge-worker/tasks/facebook/send-facebook-message.ts b/apps/bridge-worker/tasks/facebook/send-facebook-message.ts index 3f28fc8..9190e11 100644 --- a/apps/bridge-worker/tasks/facebook/send-facebook-message.ts +++ b/apps/bridge-worker/tasks/facebook/send-facebook-message.ts @@ -31,7 +31,6 @@ const sendFacebookMessageTask = async ( headers: { "Content-Type": "application/json" }, body: JSON.stringify(outgoingMessage), }); - console.log({ response }); } catch (error) { console.error({ error }); throw error; diff --git a/apps/bridge-worker/tasks/fetch-signal-messages.ts b/apps/bridge-worker/tasks/fetch-signal-messages.ts index 1044107..fd920fb 100644 --- a/apps/bridge-worker/tasks/fetch-signal-messages.ts +++ b/apps/bridge-worker/tasks/fetch-signal-messages.ts @@ -46,7 +46,6 @@ const processMessage = async ({ message: msg, }: ProcessMessageArgs): Promise[]> => { const { envelope } = msg; - console.log(envelope); const { source, sourceUuid, dataMessage } = envelope; if (!dataMessage) return []; @@ -125,7 +124,6 @@ const fetchSignalMessagesTask = async ({ phoneNumber, message, }); - console.log({ formattedMessages }); for (const formattedMessage of formattedMessages) { if (formattedMessage.to !== formattedMessage.from) { await worker.addJob( diff --git a/apps/bridge-worker/tasks/leafcutter/import-label-studio.ts b/apps/bridge-worker/tasks/leafcutter/import-label-studio.ts index cb1857e..4f50066 100644 --- a/apps/bridge-worker/tasks/leafcutter/import-label-studio.ts +++ b/apps/bridge-worker/tasks/leafcutter/import-label-studio.ts @@ -36,7 +36,6 @@ const getZammadTickets = async ( { headers }, ); const tickets: any = await rawTickets.json(); - console.log({ tickets }); if (!tickets || tickets.length === 0) { return [shouldContinue, docs]; } @@ -49,23 +48,9 @@ const getZammadTickets = async ( shouldContinue = true; if (source_closed_at <= minUpdatedTimestamp) { - console.log(`Skipping ticket`, { - source_id, - source_updated_at, - source_closed_at, - minUpdatedTimestamp, - }); continue; } - - console.log(`Processing ticket`, { - source_id, - source_updated_at, - source_closed_at, - minUpdatedTimestamp, - }); - - const rawArticles = await fetch( + const rawArticles = await fetch( `${zammadApiUrl}/ticket_articles/by_ticket/${source_id}`, { headers }, ); @@ -178,8 +163,6 @@ const sendToLabelStudio = async (tickets: FormattedZammadTicket[]) => { body: JSON.stringify([ticket]), }); const importResult = await res.json(); - - console.log(JSON.stringify(importResult, undefined, 2)); } }; */ @@ -201,7 +184,6 @@ const importLabelStudioTask = async (): Promise => { await sendToLabelStudio(tickets); const lastTicket = tickets.pop(); const newLastTimestamp = lastTicket.data.source_closed_at; - console.log({ newLastTimestamp }); await db.settings.upsert(settingName, { minUpdatedTimestamp: newLastTimestamp, }); diff --git a/apps/bridge-worker/tasks/leafcutter/import-leafcutter.ts b/apps/bridge-worker/tasks/leafcutter/import-leafcutter.ts index a3c3e4e..f410ca7 100644 --- a/apps/bridge-worker/tasks/leafcutter/import-leafcutter.ts +++ b/apps/bridge-worker/tasks/leafcutter/import-leafcutter.ts @@ -43,14 +43,11 @@ const getLabelStudioTickets = async ( page_size: "50", page: `${page}`, }); - console.log({ url: `${labelStudioApiUrl}/projects/1/tasks?${ticketsQuery}` }); const res = await fetch( `${labelStudioApiUrl}/projects/1/tasks?${ticketsQuery}`, { headers }, ); - console.log({ res }); const tasksResult: any = await res.json(); - console.log({ tasksResult }); return tasksResult; }; @@ -63,14 +60,11 @@ const fetchFromLabelStudio = async ( for await (const page of pages) { const docs = await getLabelStudioTickets(page + 1); - console.log({ page, docs }); if (docs && docs.length > 0) { for (const doc of docs) { const updatedAt = new Date(doc.updated_at); - console.log({ updatedAt, minUpdatedTimestamp }); if (updatedAt > minUpdatedTimestamp) { - console.log(`Adding doc`, { doc }); allDocs.push(doc); } } @@ -79,7 +73,6 @@ const fetchFromLabelStudio = async ( } } - console.log({ allDocs }); return allDocs; }; @@ -93,9 +86,7 @@ const sendToLeafcutter = async (tickets: LabelStudioTicket[]) => { }, } = config; - console.log({ tickets }); const filteredTickets = tickets.filter((ticket) => ticket.is_labeled); - console.log({ filteredTickets }); const finalTickets: LeafcutterTicket[] = filteredTickets.map((ticket) => { const { id, @@ -131,8 +122,7 @@ const sendToLeafcutter = async (tickets: LabelStudioTicket[]) => { }; }); - console.log("Sending to Leafcutter"); - console.log({ finalTickets }); + console.info("Sending to Leafcutter"); const result = await fetch(opensearchApiUrl, { method: "POST", @@ -157,15 +147,7 @@ const importLeafcutterTask = async (): Promise => { ? new Date(res.value.minUpdatedTimestamp as string) : new Date("2023-03-01"); const newLastTimestamp = new Date(); - console.log({ - contributorName, - settingName, - res, - startTimestamp, - newLastTimestamp, - }); const tickets = await fetchFromLabelStudio(startTimestamp); - console.log({ tickets }); await sendToLeafcutter(tickets); await db.settings.upsert(settingName, { minUpdatedTimestamp: newLastTimestamp, diff --git a/apps/bridge-worker/tasks/signal/receive-signal-message.ts b/apps/bridge-worker/tasks/signal/receive-signal-message.ts index b1688ea..a75b7cb 100644 --- a/apps/bridge-worker/tasks/signal/receive-signal-message.ts +++ b/apps/bridge-worker/tasks/signal/receive-signal-message.ts @@ -23,7 +23,6 @@ const receiveSignalMessageTask = async ({ filename, mimeType, }: ReceiveSignalMessageTaskOptions): Promise => { - console.log({ token, to, from }); const worker = await getWorkerUtils(); const row = await db .selectFrom("SignalBot") diff --git a/apps/bridge-worker/tasks/signal/send-signal-message.ts b/apps/bridge-worker/tasks/signal/send-signal-message.ts index 4150300..8426709 100644 --- a/apps/bridge-worker/tasks/signal/send-signal-message.ts +++ b/apps/bridge-worker/tasks/signal/send-signal-message.ts @@ -13,7 +13,6 @@ const sendSignalMessageTask = async ({ to, message, }: SendSignalMessageTaskOptions): Promise => { - console.log({ token, to }); const bot = await db .selectFrom("SignalBot") .selectAll() @@ -34,7 +33,6 @@ const sendSignalMessageTask = async ({ message, }, }); - console.log({ response }); } catch (error) { console.error({ error }); throw error; diff --git a/apps/bridge-worker/tasks/whatsapp/receive-whatsapp-message.ts b/apps/bridge-worker/tasks/whatsapp/receive-whatsapp-message.ts index 024c70e..48adc6f 100644 --- a/apps/bridge-worker/tasks/whatsapp/receive-whatsapp-message.ts +++ b/apps/bridge-worker/tasks/whatsapp/receive-whatsapp-message.ts @@ -23,8 +23,6 @@ const receiveWhatsappMessageTask = async ({ filename, mimeType, }: ReceiveWhatsappMessageTaskOptions): Promise => { - console.log({ token, to, from }); - const worker = await getWorkerUtils(); const row = await db .selectFrom("WhatsappBot") diff --git a/apps/bridge-worker/tasks/whatsapp/send-whatsapp-message.ts b/apps/bridge-worker/tasks/whatsapp/send-whatsapp-message.ts index 509371f..b04543a 100644 --- a/apps/bridge-worker/tasks/whatsapp/send-whatsapp-message.ts +++ b/apps/bridge-worker/tasks/whatsapp/send-whatsapp-message.ts @@ -25,7 +25,6 @@ const sendWhatsappMessageTask = async ({ headers: { "Content-Type": "application/json" }, body: JSON.stringify(params), }); - console.log({ result }); } catch (error) { console.error({ error }); throw new Error("Failed to send message"); diff --git a/apps/leafcutter/README.md b/apps/leafcutter/README.md index e03b35c..c644c6a 100644 --- a/apps/leafcutter/README.md +++ b/apps/leafcutter/README.md @@ -1,32 +1,195 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +# Leafcutter -## Getting Started +Data visualization and analytics platform for the CDR Link ecosystem. -First, run the development server: +## Overview + +Leafcutter provides powerful data visualization capabilities with multiple chart types, trend analysis, and OpenSearch integration. It enables users to create, save, and share custom visualizations of their data with support for multiple languages. + +## Features + +- **Multiple Visualization Types**: + - Vertical/Horizontal Bar Charts (including stacked) + - Line Charts (including stacked) + - Pie/Donut Charts + - Data Tables + - Metrics Display + - Tag Clouds + +- **Data Management**: + - Create and save custom searches + - User-specific visualizations + - Trend analysis and insights + - OpenSearch integration for data queries + +- **User Experience**: + - Internationalization (English, French) + - Responsive design + - Export capabilities + - Preview mode for sharing + +## Development + +### Prerequisites + +- Node.js >= 20 +- npm >= 10 +- OpenSearch instance +- PostgreSQL database (for user data) + +### Setup ```bash +# Install dependencies +npm install + +# Run development server (port 3001) npm run dev + +# Build for production +npm run build + +# Start production server +npm run start ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +### Environment Variables -You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. +Required environment variables: -[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. +- `OPENSEARCH_URL` - OpenSearch endpoint +- `OPENSEARCH_USERNAME` - OpenSearch username +- `OPENSEARCH_PASSWORD` - OpenSearch password +- `DATABASE_URL` - PostgreSQL connection +- `NEXTAUTH_URL` - Application URL +- `NEXTAUTH_SECRET` - NextAuth.js secret +- `GOOGLE_CLIENT_ID` - Google OAuth client ID +- `GOOGLE_CLIENT_SECRET` - Google OAuth client secret -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. +### Available Scripts -## Learn More +- `npm run dev` - Development server on port 3001 +- `npm run build` - Build for production +- `npm run start` - Start production server +- `npm run lint` - Run ESLint +- `npm run export` - Export static site +- `npm run aws:*` - AWS deployment utilities +- `npm run kubectl:*` - Kubernetes utilities -To learn more about Next.js, take a look at the following resources: +## Architecture -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +### Page Structure -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! +- `/` - Home dashboard +- `/create` - Create new visualizations +- `/visualizations/[id]` - View/edit visualizations +- `/preview/[id]` - Public preview mode +- `/trends` - Trend analysis +- `/about` - About page +- `/faq` - Frequently asked questions +- `/setup` - Initial setup wizard -## Deploy on Vercel +### API Routes -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +- `/api/searches/*` - Search management +- `/api/visualizations/*` - Visualization CRUD +- `/api/trends/*` - Trend data +- `/api/upload` - File upload handling +- `/api/link/auth` - Link authentication -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +### Visualization Configuration + +Each visualization type has a JSON configuration in `_config/visualizations/`: +- Chart options +- Data mapping +- Styling preferences +- Aggregation settings + +### Data Flow + +1. User creates search query +2. Query sent to OpenSearch +3. Results processed and aggregated +4. Data rendered in chosen visualization +5. Visualization saved to PostgreSQL + +## Internationalization + +Supported languages: +- English (`_locales/en.json`) +- French (`_locales/fr.json`) + +Language selection available in the UI with automatic persistence. + +## OpenSearch Integration + +### Query Structure + +Leafcutter translates user inputs into OpenSearch queries: +- Full-text search +- Field filtering +- Date ranges +- Aggregations + +### Index Management + +Works with OpenSearch indices for: +- Document storage +- Real-time analytics +- Historical data + +## Visualization Types + +### Bar Charts +- Vertical and horizontal orientations +- Stacked variants for multi-series data +- Customizable colors and labels + +### Line Charts +- Time series visualization +- Multiple series support +- Trend analysis + +### Pie/Donut Charts +- Proportional data display +- Interactive legends +- Percentage calculations + +### Data Tables +- Sortable columns +- Pagination +- Export functionality + +### Metrics +- Single value display +- Comparison indicators +- Real-time updates + +### Tag Clouds +- Word frequency visualization +- Interactive filtering +- Size-based importance + +## Security + +- Authentication via NextAuth.js +- User-scoped data access +- Secure OpenSearch queries +- Input validation + +## Docker Support + +```bash +# Build image +docker build -t link-stack/leafcutter . + +# Run with docker-compose +docker-compose -f docker/compose/leafcutter.yml up +``` + +## Deployment + +Includes utilities for: +- AWS deployment (S3, CloudFront) +- Kubernetes deployment +- Static site export \ No newline at end of file diff --git a/apps/leafcutter/app/(main)/about/page.tsx b/apps/leafcutter/app/(main)/about/page.tsx index b6958b2..1bfad23 100644 --- a/apps/leafcutter/app/(main)/about/page.tsx +++ b/apps/leafcutter/app/(main)/about/page.tsx @@ -1,5 +1,10 @@ +import { Suspense } from "react"; import { About } from "@link-stack/leafcutter-ui"; export default function Page() { - return ; + return ( + + + + ); } diff --git a/apps/leafcutter/app/(main)/faq/page.tsx b/apps/leafcutter/app/(main)/faq/page.tsx index 396a9b5..05b57de 100644 --- a/apps/leafcutter/app/(main)/faq/page.tsx +++ b/apps/leafcutter/app/(main)/faq/page.tsx @@ -1,5 +1,12 @@ +import { Suspense } from "react"; import { FAQ } from "@link-stack/leafcutter-ui"; -export default function Page() { - return ; +export const dynamic = "force-dynamic"; + +export default async function Page() { + return ( + + + + ); } diff --git a/apps/leafcutter/app/(main)/layout.tsx b/apps/leafcutter/app/(main)/layout.tsx index 5b9e9f1..d97fd7b 100644 --- a/apps/leafcutter/app/(main)/layout.tsx +++ b/apps/leafcutter/app/(main)/layout.tsx @@ -2,6 +2,8 @@ import { ReactNode } from "react"; import "app/_styles/global.css"; import { InternalLayout } from "../_components/InternalLayout"; +export const dynamic = "force-dynamic"; + type LayoutProps = { children: ReactNode; }; diff --git a/apps/leafcutter/app/(main)/preview/[...visualizationID]/page.tsx b/apps/leafcutter/app/(main)/preview/[...visualizationID]/page.tsx index 8daaaba..a1a60c6 100644 --- a/apps/leafcutter/app/(main)/preview/[...visualizationID]/page.tsx +++ b/apps/leafcutter/app/(main)/preview/[...visualizationID]/page.tsx @@ -69,19 +69,17 @@ export const getServerSideProps: GetServerSideProps = async ( }); }); } - console.log({ query }); const dataResponse = await client.search({ index: "demo_data", size: 200, body: { query }, }); - console.log({ dataResponse }); res.props.data = dataResponse.body.hits.hits.map((hit) => ({ id: hit._id, ...hit._source, })); - console.log({ data: res.props.data }); - console.log(res.props.data[0]); + + return res; }; diff --git a/apps/leafcutter/app/(main)/setup/_components/Setup.tsx b/apps/leafcutter/app/(main)/setup/_components/Setup.tsx index 1a079ec..7fe08ae 100644 --- a/apps/leafcutter/app/(main)/setup/_components/Setup.tsx +++ b/apps/leafcutter/app/(main)/setup/_components/Setup.tsx @@ -13,7 +13,7 @@ export const Setup: FC = () => { } = useLeafcutterContext(); const router = useRouter(); useLayoutEffect(() => { - setTimeout(() => router.push("/"), 4000); + setTimeout(() => router.push("/"), 2000); }, [router]); return ( diff --git a/apps/leafcutter/app/(main)/setup/page.tsx b/apps/leafcutter/app/(main)/setup/page.tsx index 4e62414..dad2ed0 100644 --- a/apps/leafcutter/app/(main)/setup/page.tsx +++ b/apps/leafcutter/app/(main)/setup/page.tsx @@ -1,5 +1,10 @@ +import { Suspense } from "react"; import { Setup } from "./_components/Setup"; export default function Page() { - return ; + return ( + + + + ); } diff --git a/apps/leafcutter/app/(main)/visualizations/[...visualizationID]/page.tsx b/apps/leafcutter/app/(main)/visualizations/[...visualizationID]/page.tsx index 5ef382b..d840ef4 100644 --- a/apps/leafcutter/app/(main)/visualizations/[...visualizationID]/page.tsx +++ b/apps/leafcutter/app/(main)/visualizations/[...visualizationID]/page.tsx @@ -22,9 +22,9 @@ const getVisualization = async (visualizationID: string) => { ); const hit = hits[0]; const visualization = { - id: hit._id.split(":")[1], - title: hit._source.visualization.title, - description: hit._source.visualization.description, + id: hit?._id.split(":")[1], + title: hit?._source?.visualization.title, + description: hit?._source?.visualization.description, url: `/app/visualize?security_tenant=global#/edit/${ hit._id.split(":")[1] }?embed=true`, @@ -34,12 +34,13 @@ const getVisualization = async (visualizationID: string) => { }; type PageProps = { - params: { + params: Promise<{ visualizationID: string; - }; + }>; }; -export default async function Page({ params: { visualizationID } }: PageProps) { +export default async function Page({ params }: PageProps) { + const { visualizationID } = await params; const visualization = await getVisualization(visualizationID); return ; diff --git a/apps/leafcutter/app/_components/Sidebar.tsx b/apps/leafcutter/app/_components/Sidebar.tsx index acdcef3..b3c0a43 100644 --- a/apps/leafcutter/app/_components/Sidebar.tsx +++ b/apps/leafcutter/app/_components/Sidebar.tsx @@ -48,7 +48,6 @@ const MenuItem = ({ return ( new Client({ - node: baseURL, - auth: { - username: process.env.OPENSEARCH_USERNAME!, - password: process.env.OPENSEARCH_PASSWORD!, - }, - ssl: { - rejectUnauthorized: false, - }, -}); +const createClient = () => + new Client({ + node: baseURL, + auth: { + username: process.env.OPENSEARCH_USERNAME!, + password: process.env.OPENSEARCH_PASSWORD!, + }, + ssl: { + rejectUnauthorized: false, + }, + }); -const createUserClient = (username: string, password: string) => new Client({ - node: baseURL, - auth: { - username, - password, - }, - ssl: { - rejectUnauthorized: false, - }, -}); +const createUserClient = (username: string, password: string) => + new Client({ + node: baseURL, + auth: { + username, + password, + }, + ssl: { + rejectUnauthorized: false, + }, + }); export const checkAuth = async (username: string, password: string) => { const client = createUserClient(username, password); @@ -115,7 +117,7 @@ export const getUserMetadata = async (username: string) => { await client.create({ id: username, index: userMetadataIndexName, - body: { username, savedSearches: [] } + body: { username, savedSearches: [] }, }); res = await client.get({ @@ -132,7 +134,7 @@ export const saveUserMetadata = async (username: string, metadata: any) => { await client.update({ id: username, index: userMetadataIndexName, - body: { doc: { username, ...metadata } } + body: { doc: { username, ...metadata } }, }); }; @@ -181,7 +183,7 @@ const getIndexPattern: any = async (index: string) => { sort: ["updated_at:desc"], }); - if (res.body.hits.total.value === 0) { + if (res?.body?.hits?.total?.valueOf() === 0) { // eslint-disable-next-line no-use-before-define return createCurrentUserIndexPattern(index); } @@ -226,7 +228,7 @@ interface createUserVisualizationProps { } export const createUserVisualization = async ( - props: createUserVisualizationProps + props: createUserVisualizationProps, ) => { const { email, query, visualizationID, title, description } = props; const userIndex = await getCurrentUserIndex(email); @@ -279,7 +281,7 @@ interface updateVisualizationProps { } export const updateUserVisualization = async ( - props: updateVisualizationProps + props: updateVisualizationProps, ) => { const { email, id, query, title, description } = props; const userIndex = await getCurrentUserIndex(email); @@ -300,8 +302,7 @@ export const updateUserVisualization = async ( body, }); } catch (e) { - // eslint-disable-next-line no-console - console.log({ e }); + console.error({ e }); } return id; @@ -469,10 +470,18 @@ export const performQuery = async (searchQuery: any, limit: number) => { const results = hits.map((hit: any) => ({ ...hit._source, id: hit._id, - incident: Array.isArray(hit._source.incident) ? hit._source.incident.join(", ") : hit._source.incident, - technology: Array.isArray(hit._source.technology) ? hit._source.technology.join(", ") : hit._source.technology, - targeted_group: Array.isArray(hit._source.targeted_group) ? hit._source.targeted_group.join(", ") : hit._source.targeted_group, - country: Array.isArray(hit._source.country) ? hit._source.country.join(", ") : hit._source.country, + incident: Array.isArray(hit._source.incident) + ? hit._source.incident.join(", ") + : hit._source.incident, + technology: Array.isArray(hit._source.technology) + ? hit._source.technology.join(", ") + : hit._source.technology, + targeted_group: Array.isArray(hit._source.targeted_group) + ? hit._source.targeted_group.join(", ") + : hit._source.targeted_group, + country: Array.isArray(hit._source.country) + ? hit._source.country.join(", ") + : hit._source.country, })); return results; @@ -570,7 +579,6 @@ export const getTemplates = async (limit: number) => { }, }; - const rawResponse = await client.search({ index: globalIndex, size: limit, diff --git a/apps/leafcutter/app/api/link/auth/route.ts b/apps/leafcutter/app/api/link/auth/route.ts index 37b356e..1960382 100644 --- a/apps/leafcutter/app/api/link/auth/route.ts +++ b/apps/leafcutter/app/api/link/auth/route.ts @@ -1,15 +1,13 @@ import { NextRequest, NextResponse } from "next/server"; export const GET = async (req: NextRequest) => { - const validDomains = "localhost"; - console.log({ req }); + const validDomains = "localhost"; - return NextResponse.json({ response: "ok" }); + return NextResponse.json({ response: "ok" }); }; export const POST = async (req: NextRequest) => { - const validDomains = "localhost"; - console.log({ req }); + const validDomains = "localhost"; - return NextResponse.json({ response: "ok" }); + return NextResponse.json({ response: "ok" }); }; diff --git a/apps/leafcutter/app/api/trends/recent/route.ts b/apps/leafcutter/app/api/trends/recent/route.ts index a635d29..d18b657 100644 --- a/apps/leafcutter/app/api/trends/recent/route.ts +++ b/apps/leafcutter/app/api/trends/recent/route.ts @@ -2,10 +2,9 @@ import { NextResponse } from "next/server"; import { getTrends } from "app/_lib/opensearch"; export const GET = async () => { - const results = await getTrends(5); - console.log({ results }); + const results = await getTrends(5); - NextResponse.json(results); + NextResponse.json(results); }; -export const dynamic = 'force-dynamic'; +export const dynamic = "force-dynamic"; diff --git a/apps/leafcutter/app/api/upload/index.ts b/apps/leafcutter/app/api/upload/index.ts index 4f3223e..afb5298 100644 --- a/apps/leafcutter/app/api/upload/index.ts +++ b/apps/leafcutter/app/api/upload/index.ts @@ -15,8 +15,8 @@ export const POST = async (req: NextRequest) => { rejectUnauthorized: false, }, headers: { - authorization - } + authorization, + }, }); const succeeded = []; @@ -28,11 +28,15 @@ export const POST = async (req: NextRequest) => { const country = ticket.country[0] ?? "none"; // @ts-expect-error const translatedCountry = taxonomy.country[country]?.display ?? "none"; - const countryDetails: any = unRegions.find((c) => c.name === translatedCountry); + const countryDetails: any = unRegions.find( + (c) => c.name === translatedCountry, + ); const augmentedTicket = { ...ticket, - region: countryDetails['sub-region']?.toLowerCase().replace(" ", "-") ?? null, - continent: countryDetails.region?.toLowerCase().replace(" ", "-") ?? null, + region: + countryDetails["sub-region"]?.toLowerCase().replace(" ", "-") ?? null, + continent: + countryDetails.region?.toLowerCase().replace(" ", "-") ?? null, }; const out = await client.create({ id: uuid(), @@ -40,10 +44,9 @@ export const POST = async (req: NextRequest) => { refresh: true, body: augmentedTicket, }); - console.log(out); succeeded.push(id); } catch (e) { - console.log(e); + console.error(e); failed.push(id); } } @@ -52,4 +55,3 @@ export const POST = async (req: NextRequest) => { return NextResponse.json(results); }; - diff --git a/apps/leafcutter/app/api/visualizations/delete/route.ts b/apps/leafcutter/app/api/visualizations/delete/route.ts index 2200ed7..20e10b5 100644 --- a/apps/leafcutter/app/api/visualizations/delete/route.ts +++ b/apps/leafcutter/app/api/visualizations/delete/route.ts @@ -3,13 +3,13 @@ import { getServerSession } from "next-auth"; import { authOptions } from "app/_lib/auth"; import { deleteUserVisualization } from "app/_lib/opensearch"; -export const POST = async (req: NextRequest, res: NextResponse) => { - const session = await getServerSession(authOptions); - const { user: { email } }: any = session; - const { id } = await req.json(); - await deleteUserVisualization(email as string, id); +export const POST = async (req: NextRequest) => { + const session = await getServerSession(authOptions); + const { + user: { email }, + }: any = session; + const { id } = await req.json(); + await deleteUserVisualization(email as string, id); - return NextResponse.json({ id }); + return NextResponse.json({ id }); }; - - diff --git a/apps/leafcutter/next-env.d.ts b/apps/leafcutter/next-env.d.ts index 725dd6f..3cd7048 100644 --- a/apps/leafcutter/next-env.d.ts +++ b/apps/leafcutter/next-env.d.ts @@ -3,4 +3,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/leafcutter/next.config.js b/apps/leafcutter/next.config.js index e8cb203..a57f18f 100644 --- a/apps/leafcutter/next.config.js +++ b/apps/leafcutter/next.config.js @@ -1,8 +1,5 @@ module.exports = { transpilePackages: ["@link-stack/leafcutter-ui", "@link-stack/opensearch-common"], - experimental: { - missingSuspenseWithCSRBailout: false, - }, poweredByHeader: false, rewrites: async () => ({ fallback: [ diff --git a/apps/leafcutter/package.json b/apps/leafcutter/package.json index 9c3e062..344db75 100644 --- a/apps/leafcutter/package.json +++ b/apps/leafcutter/package.json @@ -1,6 +1,6 @@ { "name": "@link-stack/leafcutter", - "version": "2.2.0", + "version": "3.1.0", "scripts": { "dev": "next dev -p 3001", "login": "aws sso login --sso-session cdr", @@ -13,37 +13,37 @@ "lint": "next lint" }, "dependencies": { - "@emotion/cache": "^11.13.1", - "@emotion/react": "^11.13.3", + "@emotion/cache": "^11.14.0", + "@emotion/react": "^11.14.0", "@emotion/server": "^11.11.0", - "@emotion/styled": "^11.13.0", + "@emotion/styled": "^11.14.0", "@link-stack/leafcutter-ui": "*", "@link-stack/opensearch-common": "*", - "@mui/icons-material": "^5", - "@mui/material": "^5", - "@mui/material-nextjs": "^5", - "@mui/x-date-pickers-pro": "^7.18.0", - "@opensearch-project/opensearch": "^2.12.0", + "@mui/icons-material": "^6", + "@mui/material": "^6", + "@mui/material-nextjs": "^6", + "@mui/x-date-pickers-pro": "^7.28.0", + "@opensearch-project/opensearch": "^3.4.0", "date-fns": "^4.1.0", - "http-proxy-middleware": "^3.0.2", - "material-ui-popup-state": "^5.3.1", - "next": "^14.2.23", - "next-auth": "^4.24.8", - "react": "18.3.1", - "react-cookie": "^7.2.0", + "http-proxy-middleware": "^3.0.3", + "material-ui-popup-state": "^5.3.3", + "next": "15.2.3", + "next-auth": "^4.24.11", + "react": "19.0.0", + "react-cookie": "^8.0.1", "react-cookie-consent": "^9.0.0", - "react-dom": "18.3.1", + "react-dom": "19.0.0", "react-iframe": "^1.8.5", "react-polyglot": "^0.7.2", "sharp": "^0.33.5", - "uuid": "^10.0.0" + "uuid": "^11.1.0" }, "devDependencies": { + "@types/node": "^22.13.12", + "@types/react": "19.0.12", + "@types/uuid": "^10.0.0", "@link-stack/eslint-config": "*", "@link-stack/typescript-config": "*", - "@types/node": "^22.7.3", - "@types/react": "18.3.9", - "@types/uuid": "^10.0.0", - "typescript": "5.6.2" + "typescript": "5.8.2" } } diff --git a/apps/leafcutter/pages/api/proxy/[[...path]].ts b/apps/leafcutter/pages/api/proxy/[[...path]].ts index 913b7cb..a4c2f38 100644 --- a/apps/leafcutter/pages/api/proxy/[[...path]].ts +++ b/apps/leafcutter/pages/api/proxy/[[...path]].ts @@ -24,17 +24,17 @@ const withAuthInfo = const requestSignature = req.query.signature; const url = new URL(req.headers.referer as string); const referrerSignature = url.searchParams.get("signature"); - - console.log({ requestSignature, referrerSignature }); const isAppPath = !!req.url?.startsWith("/app"); - const isResourcePath = !!req.url?.match(/\/(api|app|bootstrap|3961|ui|translations|internal|login|node_modules)/); + const isResourcePath = !!req.url?.match( + /\/(api|app|bootstrap|3961|ui|translations|internal|login|node_modules)/, + ); if (requestSignature && isAppPath) { - console.log("Has Signature"); + console.info("Has Signature"); } if (referrerSignature && isResourcePath) { - console.log("Has Signature"); + console.info("Has Signature"); } if (!email) { diff --git a/apps/leafcutter/public/robots.txt b/apps/leafcutter/public/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/apps/leafcutter/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/apps/link/README.md b/apps/link/README.md index e03b35c..f080763 100644 --- a/apps/link/README.md +++ b/apps/link/README.md @@ -1,32 +1,109 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +# CDR Link -## Getting Started +The main CDR (Center for Digital Resilience) Link application - a streamlined helpdesk interface built on top of Zammad with integrated communication channels and data visualization. -First, run the development server: +## Overview + +CDR Link provides a unified dashboard for managing support tickets, communication channels, and data analytics. It integrates multiple services including Zammad (ticketing), Bridge (multi-channel messaging), Leafcutter (data visualization), and OpenSearch. + +## Features + +- **Simplified Helpdesk Interface**: Streamlined UI for Zammad ticket management +- **Multi-Channel Communication**: Integration with Signal, WhatsApp, Facebook, and Voice channels +- **Data Visualization**: Embedded Leafcutter analytics and reporting +- **User Management**: Role-based access control with Google OAuth +- **Search**: Integrated OpenSearch for advanced queries +- **Label Studio Integration**: For data annotation workflows + +## Development + +### Prerequisites + +- Node.js >= 20 +- npm >= 10 +- Running instances of Zammad, PostgreSQL, and Redis +- Configured authentication providers + +### Setup ```bash +# Install dependencies +npm install + +# Run development server npm run dev + +# Build for production +npm run build + +# Start production server +npm run start ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +### Environment Variables -You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. +Key environment variables required: -[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. +- `ZAMMAD_URL` - Zammad instance URL +- `ZAMMAD_API_TOKEN` - Zammad API authentication token +- `DATABASE_URL` - PostgreSQL connection string +- `REDIS_URL` - Redis connection URL +- `NEXTAUTH_URL` - Application URL for authentication +- `NEXTAUTH_SECRET` - Secret for NextAuth.js +- `GOOGLE_CLIENT_ID` - Google OAuth client ID +- `GOOGLE_CLIENT_SECRET` - Google OAuth client secret -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. +### Available Scripts -## Learn More +- `npm run dev` - Start development server +- `npm run build` - Build for production +- `npm run start` - Start production server +- `npm run lint` - Run ESLint +- `npm run export` - Export static site -To learn more about Next.js, take a look at the following resources: +## Architecture -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +### Pages Structure -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! +- `/` - Main dashboard +- `/overview/[overview]` - Ticket overview pages +- `/tickets/[id]` - Individual ticket view/edit +- `/admin/bridge` - Bridge configuration management +- `/leafcutter` - Data visualization dashboard +- `/opensearch` - Search dashboard +- `/zammad` - Direct Zammad access +- `/profile` - User profile management -## Deploy on Vercel +### API Routes -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +- `/api/auth` - NextAuth.js authentication +- `/api/v2/users` - User management API +- `/api/[service]/bots` - Bot management for communication channels +- `/api/[service]/webhooks` - Webhook endpoints -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +### Key Components + +- `ZammadWrapper` - Embeds Zammad UI with authentication +- `SearchBox` - Global search functionality +- `TicketList` / `TicketDetail` - Ticket management components +- `Sidebar` - Navigation and service switching + +## Docker Support + +Build and run with Docker: + +```bash +# Build image +docker build -t link-stack/link . + +# Run with docker-compose +docker-compose -f docker/compose/link.yml up +``` + +## Integration Points + +- **Zammad**: GraphQL queries for ticket data +- **Bridge Services**: REST APIs for channel management +- **Leafcutter**: Embedded iframe integration +- **OpenSearch**: Direct dashboard embedding +- **Redis**: Session and cache storage \ No newline at end of file diff --git a/apps/link/app/(login)/login/_components/Login.tsx b/apps/link/app/(login)/login/_components/Login.tsx index 0cc1234..3207dfd 100644 --- a/apps/link/app/(login)/login/_components/Login.tsx +++ b/apps/link/app/(login)/login/_components/Login.tsx @@ -23,13 +23,15 @@ import { useSearchParams } from "next/navigation"; type LoginProps = { session: any; + baseURL: string; }; -export const Login: FC = ({ session }) => { - const origin = - typeof window !== "undefined" && window.location.origin - ? window.location.origin - : ""; +export const Login: FC = ({ session, baseURL }) => { + let origin = null; + if (typeof window !== "undefined") { + origin = window.location.origin; + } + const callbackUrl = `${origin}/link`; const [provider, setProvider] = useState(undefined); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -158,7 +160,7 @@ export const Login: FC = ({ session }) => { sx={buttonStyles} onClick={() => signIn("google", { - callbackUrl: `${origin}`, + callbackUrl, }) } > @@ -174,7 +176,7 @@ export const Login: FC = ({ session }) => { sx={buttonStyles} onClick={() => signIn("apple", { - callbackUrl: `${window.location.origin}`, + callbackUrl, }) } > @@ -189,7 +191,7 @@ export const Login: FC = ({ session }) => { sx={buttonStyles} onClick={() => signIn("azure-ad", { - callbackUrl: `${origin}`, + callbackUrl, }) } > @@ -226,13 +228,13 @@ export const Login: FC = ({ session }) => { + onClick={() => { signIn("credentials", { email, password, - callbackUrl: `${origin}/setup`, - }) - } + callbackUrl, + }); + }} > Sign in with Zammad credentials diff --git a/apps/link/app/(login)/login/page.tsx b/apps/link/app/(login)/login/page.tsx index 7bafdbb..ea944ba 100644 --- a/apps/link/app/(login)/login/page.tsx +++ b/apps/link/app/(login)/login/page.tsx @@ -9,10 +9,11 @@ export const metadata: Metadata = { export default async function Page() { const session = await getSession(); + const baseURL = process.env.LINK_URL; return ( Loading...}> - + ); } diff --git a/apps/link/app/(main)/_components/ClientOnly.tsx b/apps/link/app/(main)/_components/ClientOnly.tsx index 46e79e8..b669b4f 100644 --- a/apps/link/app/(main)/_components/ClientOnly.tsx +++ b/apps/link/app/(main)/_components/ClientOnly.tsx @@ -1,8 +1,9 @@ "use client"; +import { ReactNode } from "react"; import dynamic from "next/dynamic"; -type ClientOnlyProps = { children: JSX.Element }; +type ClientOnlyProps = { children: ReactNode }; const ClientOnly = (props: ClientOnlyProps) => { const { children } = props; diff --git a/apps/link/app/(main)/_components/Home.tsx b/apps/link/app/(main)/_components/Home.tsx index 1a404ae..44332f1 100644 --- a/apps/link/app/(main)/_components/Home.tsx +++ b/apps/link/app/(main)/_components/Home.tsx @@ -3,9 +3,10 @@ import { FC } from "react"; import { OpenSearchWrapper } from "@link-stack/leafcutter-ui"; -export const Home: FC = () => ( - +type HomeProps = { + url: string; +}; + +export const Home: FC = ({ url }) => ( + ); diff --git a/apps/link/app/(main)/_components/Sidebar.tsx b/apps/link/app/(main)/_components/Sidebar.tsx index c099c42..1bd7a1c 100644 --- a/apps/link/app/(main)/_components/Sidebar.tsx +++ b/apps/link/app/(main)/_components/Sidebar.tsx @@ -207,10 +207,6 @@ export const Sidebar: FC = ({ return () => clearInterval(interval); }, []); - const logout = () => { - signOut({ callbackUrl: "/login" }); - }; - return ( = ({ {open ? username : username - .split(" ") - .map((name) => name.substring(0, 1)) - .join("")} + .split(" ") + .map((name) => name.substring(0, 1)) + .join("")} @@ -485,7 +481,17 @@ export const Sidebar: FC = ({ selected={pathname.endsWith("/docs")} open={open} /> - {leafcutterEnabled && ( + {roles.includes("admin") && leafcutterEnabled && ( + + )} + {false && leafcutterEnabled && ( = ({ Icon={LogoutIcon} iconSize={20} open={open} - onClick={logout} /> diff --git a/apps/link/app/(main)/_components/ZammadWrapper.tsx b/apps/link/app/(main)/_components/ZammadWrapper.tsx index 1696040..4daec0f 100644 --- a/apps/link/app/(main)/_components/ZammadWrapper.tsx +++ b/apps/link/app/(main)/_components/ZammadWrapper.tsx @@ -41,7 +41,6 @@ export const ZammadWrapper: FC = ({ method: "GET", redirect: "manual", }); - console.log({ res }); if (res.type === "opaqueredirect") { setAuthenticated(true); } else { @@ -69,7 +68,6 @@ export const ZammadWrapper: FC = ({ }, [session]); if (!session || !authenticated) { - console.log("Not authenticated"); return ( = ({ } if (session && authenticated) { - console.log("Session and authenticated"); return (