From 5a5beee50e7f6ade3bf142392c847e66d524d472 Mon Sep 17 00:00:00 2001 From: whtis Date: Tue, 19 May 2026 01:08:20 +0800 Subject: [PATCH 1/9] feat(dashboard): add Simplified Chinese (zh) translations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Simplified Chinese locale to the dashboard, following the i18n pattern introduced in #64. Chinese is now available alongside English and Hebrew via the existing sidebar language switcher. What's included: - New `src/i18n/locales/zh.json` mirroring the structure of `en.json`, covering all pages, components, and toast messages (404 keys + a new `common.chinese` label, identical hierarchy to `en.json`) - `zh` registered in `src/i18n/index.ts` (`supportedLanguages`, `resources`) so the language detector picks up `zh-*` browsers automatically and the cycle-language button rotates through it - Language label in `Layout.tsx` switched from a binary `he`/EN ternary to a `SupportedLanguage` lookup, so adding more languages later only requires extending the map; `zh` shows as `中文` - `common.chinese` key added to `en.json` for parity with the existing `common.english` / `common.hebrew` entries No code, API, schema, or styling changes outside i18n. RTL list is unchanged (Chinese is LTR). Existing users see no behavior change unless their browser is set to `zh-*` or they pick 中文 from the sidebar. Verified: - `npm run build` passes (tsc -b clean, vite build succeeds) - `npm run lint` reports no new errors (3 pre-existing warnings) - Key parity check: `en.json` and `zh.json` have identical key sets apart from the intentional new `common.chinese` - Chinese strings present in the production bundle (verified via grep on `dist/assets/index-*.js`) --- dashboard/src/components/Layout.tsx | 7 +- dashboard/src/i18n/index.ts | 4 +- dashboard/src/i18n/locales/en.json | 1 + dashboard/src/i18n/locales/zh.json | 513 ++++++++++++++++++++++++++++ 4 files changed, 523 insertions(+), 2 deletions(-) create mode 100644 dashboard/src/i18n/locales/zh.json diff --git a/dashboard/src/components/Layout.tsx b/dashboard/src/components/Layout.tsx index f2815bb..c101f78 100644 --- a/dashboard/src/components/Layout.tsx +++ b/dashboard/src/components/Layout.tsx @@ -85,7 +85,12 @@ export function Layout({ onLogout, userRole }: LayoutProps) { const next = supportedLanguages[(idx + 1) % supportedLanguages.length]; void i18n.changeLanguage(next); }; - const languageLabel = currentLang === 'he' ? 'עברית' : 'EN'; + const languageLabels: Record = { + en: 'EN', + he: 'עברית', + zh: '中文', + }; + const languageLabel = languageLabels[currentLang] ?? 'EN'; const isRtl = currentLang === 'he'; return ( diff --git a/dashboard/src/i18n/index.ts b/dashboard/src/i18n/index.ts index cfa5b8c..51dd062 100644 --- a/dashboard/src/i18n/index.ts +++ b/dashboard/src/i18n/index.ts @@ -3,8 +3,9 @@ import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import en from './locales/en.json'; import he from './locales/he.json'; +import zh from './locales/zh.json'; -export const supportedLanguages = ['en', 'he'] as const; +export const supportedLanguages = ['en', 'he', 'zh'] as const; export type SupportedLanguage = (typeof supportedLanguages)[number]; export const rtlLanguages: SupportedLanguage[] = ['he']; @@ -16,6 +17,7 @@ void i18n resources: { en: { translation: en }, he: { translation: he }, + zh: { translation: zh }, }, fallbackLng: 'en', supportedLngs: supportedLanguages as unknown as string[], diff --git a/dashboard/src/i18n/locales/en.json b/dashboard/src/i18n/locales/en.json index 5304924..11b87b9 100644 --- a/dashboard/src/i18n/locales/en.json +++ b/dashboard/src/i18n/locales/en.json @@ -37,6 +37,7 @@ "language": "Language", "english": "English", "hebrew": "עברית", + "chinese": "中文", "back": "Back", "next": "Next", "previous": "Previous", diff --git a/dashboard/src/i18n/locales/zh.json b/dashboard/src/i18n/locales/zh.json new file mode 100644 index 0000000..37f81eb --- /dev/null +++ b/dashboard/src/i18n/locales/zh.json @@ -0,0 +1,513 @@ +{ + "common": { + "appName": "OpenWA", + "appSubtitle": "WhatsApp API", + "loading": "加载中...", + "save": "保存", + "cancel": "取消", + "delete": "删除", + "edit": "编辑", + "create": "新建", + "close": "关闭", + "submit": "提交", + "confirm": "确认", + "search": "搜索", + "filter": "筛选", + "actions": "操作", + "status": "状态", + "name": "名称", + "role": "角色", + "type": "类型", + "url": "URL", + "port": "端口", + "host": "主机", + "username": "用户名", + "password": "密码", + "active": "已启用", + "inactive": "未启用", + "enabled": "已开启", + "disabled": "已关闭", + "connected": "已连接", + "disconnected": "已断开", + "never": "从未", + "justNow": "刚刚", + "minAgo": "{{count}} 分钟前", + "hoursAgo": "{{count}} 小时前", + "optional": "可选", + "language": "语言", + "english": "English", + "hebrew": "עברית", + "chinese": "中文", + "back": "返回", + "next": "下一步", + "previous": "上一步", + "expand": "展开", + "collapse": "收起", + "logout": "退出登录", + "refresh": "刷新", + "errorGeneric": "发生错误", + "unknownError": "未知错误" + }, + "theme": { + "light": "浅色", + "dark": "深色", + "system": "跟随系统", + "label": "主题:{{value}}" + }, + "nav": { + "dashboard": "仪表盘", + "sessions": "会话", + "webhooks": "Webhook", + "apiKeys": "API 密钥", + "messageTester": "消息测试", + "infrastructure": "基础设施", + "plugins": "插件", + "logs": "日志" + }, + "errorBoundary": { + "title": "出了点问题", + "description": "仪表盘遇到意外错误。", + "reload": "重新加载仪表盘" + }, + "login": { + "apiKey": "API 密钥", + "apiKeyPlaceholder": "请输入你的 API 密钥", + "apiKeyRequired": "API 密钥不能为空", + "connect": "连接", + "connecting": "正在连接...", + "invalidKey": "API 密钥无效", + "connectionError": "无法连接到服务器,请稍后重试。", + "help": "需要帮助?", + "viewDocs": "查看文档", + "footer": "由 Yudhi Armyndharis 和 OpenWA 社区用心打造 ❤️", + "version": "v{{version}} · {{date}}" + }, + "dashboard": { + "title": "仪表盘", + "subtitle": "查看 WhatsApp 会话与活动概览", + "stats": { + "activeSessions": "活跃会话", + "messagesToday": "今日消息", + "webhooksConfigured": "已配置 Webhook", + "apiCalls": "API 调用(24 小时)" + }, + "sessionsOverview": "会话概览", + "showingSessions": "正在显示 {{shown}} / {{total}} 个会话", + "columns": { + "sessionId": "会话 ID", + "phone": "手机号", + "status": "状态", + "lastActive": "最近活跃", + "actions": "操作" + }, + "noSessions": "暂无会话,先新建一个吧。", + "view": "查看", + "disconnect": "断开连接", + "errorPrefix": "错误:{{message}}", + "loadError": "数据加载失败" + }, + "sessionStatus": { + "created": "新建", + "idle": "空闲", + "initializing": "正在启动...", + "connecting": "连接中...", + "qr_ready": "等待扫码", + "ready": "已连接", + "disconnected": "已断开" + }, + "sessions": { + "title": "会话", + "subtitle": "管理 WhatsApp 会话和二维码连接", + "newSession": "新建会话", + "searchPlaceholder": "搜索会话...", + "filter": { + "all": "全部状态", + "active": "活跃(已连接)", + "inactive": "未活跃", + "connecting": "连接中" + }, + "empty": { + "title": "暂无会话", + "description": "新建一个会话开始使用" + }, + "create": { + "title": "新建会话", + "label": "会话名称", + "placeholder": "例如 marketing-bot", + "hint": "只能使用小写字母、数字和连字符。示例:customer-supportbot-1", + "invalidChars": "包含非法字符,只能使用小写字母、数字和连字符。", + "tooLong": "名称长度不能超过 50 个字符(当前 {{length}}/50)。", + "duplicate": "已存在同名会话。", + "successTitle": "会话已创建", + "successDesc": "会话 “{{name}}” 创建成功", + "errorTitle": "创建失败", + "errorDefault": "会话创建失败" + }, + "delete": { + "title": "确认删除", + "message": "确定要删除会话 “{{name}}” 吗?", + "warning": "此操作无法撤销。", + "successTitle": "会话已删除", + "successDescNamed": "会话 “{{name}}” 已删除", + "successDescGeneric": "会话已删除", + "errorTitle": "删除失败", + "errorDefault": "会话删除失败" + }, + "qr": { + "title": "扫描二维码", + "step1": "1. 在手机上打开 WhatsApp", + "step2": "2. 依次点击 菜单已连接的设备", + "step3": "3. 点击 连接设备 并扫描此二维码", + "autoRefresh": "二维码每 5 秒自动刷新一次", + "generating": "正在生成二维码...", + "unavailable": "二维码尚未就绪,请稍后再试。", + "preparing": "准备二维码...", + "showQr": "显示二维码", + "loading": "加载中...", + "scanToConnect": "扫描二维码以连接" + }, + "details": { + "title": "会话详情", + "name": "名称", + "status": "状态", + "sessionId": "会话 ID", + "phone": "手机号", + "phoneNone": "未连接", + "created": "创建时间", + "lastActive": "最近活跃" + }, + "actions": { + "view": "查看", + "start": "启动", + "stop": "停止", + "reconnect": "重新连接", + "delete": "删除" + }, + "toasts": { + "readyTitle": "会话已就绪", + "readyDesc": "WhatsApp 会话已成功连接", + "disconnectedTitle": "会话已断开", + "disconnectedDesc": "WhatsApp 会话已断开连接" + }, + "card": { + "phone": "手机号", + "sessionId": "会话 ID", + "lastActive": "最近活跃" + } + }, + "webhooks": { + "title": "Webhook", + "subtitle": "配置 HTTP 回调,实时接收事件通知", + "addWebhook": "添加 Webhook", + "createTitle": "新建 Webhook", + "editTitle": "编辑 Webhook", + "deleteTitle": "删除 Webhook", + "deleteConfirm": "确定要删除这个 Webhook 吗?", + "selectSession": "选择会话...", + "session": "会话", + "events": "事件", + "available": "可订阅事件", + "saveChanges": "保存修改", + "columns": { + "url": "URL", + "events": "事件", + "session": "会话", + "status": "状态", + "actions": "操作" + }, + "empty": { + "title": "尚未配置 Webhook", + "description": "添加 Webhook 以实时接收事件通知" + }, + "toasts": { + "created": "Webhook 创建成功", + "createFailed": "Webhook 创建失败:{{message}}", + "deleted": "Webhook 已删除", + "deleteFailed": "Webhook 删除失败:{{message}}", + "updated": "Webhook 更新成功", + "updateFailed": "Webhook 更新失败:{{message}}", + "testOk": "Webhook 测试成功!状态码:{{status}}", + "testFailed": "Webhook 测试失败:{{message}}", + "testError": "Webhook 测试请求失败:{{message}}" + }, + "actions": { + "test": "测试", + "edit": "编辑", + "delete": "删除" + }, + "eventDescriptions": { + "message.received": "收到消息时触发", + "message.sent": "发送消息时触发", + "session.connected": "会话连接成功时触发", + "session.disconnected": "会话断开时触发", + "session.qr": "生成二维码时触发", + "all": "所有事件" + } + }, + "apiKeys": { + "title": "API 密钥", + "subtitle": "管理用于鉴权与访问控制的 API 密钥", + "createBtn": "新建 API 密钥", + "modalTitle": "新建 API 密钥", + "createdTitle": "API 密钥已创建", + "createdHint": "请立即复制此密钥,关闭后将无法再次查看。", + "namePlaceholder": "例如:生产环境密钥", + "rolesTitle": "角色说明", + "roles": { + "admin": "管理员", + "operator": "操作员", + "viewer": "只读" + }, + "roleDescriptions": { + "admin": "对所有资源拥有完整权限", + "operator": "可管理会话和消息", + "viewer": "仅可读,不可修改" + }, + "columns": { + "name": "名称", + "key": "密钥", + "role": "角色", + "status": "状态", + "lastUsed": "最近使用", + "actions": "操作" + }, + "statuses": { + "active": "有效", + "revoked": "已撤销" + }, + "empty": { + "title": "暂无 API 密钥", + "description": "新建 API 密钥以对请求进行鉴权" + }, + "confirm": { + "deleteTitle": "删除 API 密钥", + "revokeTitle": "撤销 API 密钥", + "deleteMessage": "确定要永久删除 {{name}} 吗?此操作无法撤销。", + "revokeMessage": "确定要撤销 {{name}} 吗?撤销后该密钥将立即失效。", + "delete": "删除", + "revoke": "撤销" + }, + "actions": { + "copy": "复制", + "revoke": "撤销", + "delete": "删除" + } + }, + "logs": { + "title": "审计日志", + "subtitle": "追踪和查看所有 API 操作与系统事件", + "exportCsv": "导出 CSV", + "searchPlaceholder": "搜索日志...", + "severity": { + "all": "全部级别", + "info": "信息", + "warn": "警告", + "error": "错误" + }, + "columns": { + "timestamp": "时间", + "action": "操作", + "session": "会话", + "apiKey": "API 密钥", + "ip": "IP 地址", + "severity": "级别" + }, + "empty": { + "title": "暂无日志", + "description": "执行操作后,审计日志会显示在这里" + } + }, + "messageTester": { + "title": "消息测试", + "subtitle": "通过 API 发送测试消息", + "compose": "发送测试消息", + "responseTitle": "API 响应", + "session": "会话", + "noReadySessions": "暂无可用会话", + "sessionOptionPhoneNone": "未绑定手机号", + "recipientType": "接收方类型", + "personal": "个人", + "group": "群组", + "selectGroup": "选择群组", + "recipientPhone": "接收方手机号", + "loadingGroups": "正在加载群组...", + "noGroupsFound": "未找到群组", + "selectGroupHint": "从你的 WhatsApp 中选择一个群组", + "phoneHint": "请使用国际格式,不带空格", + "messageType": "消息类型", + "types": { + "text": "文本", + "image": "图片", + "video": "视频", + "audio": "音频", + "document": "文档" + }, + "messageContent": "消息内容", + "messagePlaceholder": "在这里输入消息...", + "mediaUrl": "媒体 URL", + "caption": "说明文字", + "filename": "文件名", + "captionPlaceholder": "输入说明文字...", + "filenamePlaceholder": "document.pdf", + "send": "发送消息", + "sending": "发送中...", + "viewOnly": "只读", + "responseEmpty": "发送消息后,这里会显示 API 响应", + "successLabel": "200 OK - 成功", + "failedLabel": "400 - 失败", + "response": { + "timestamp": "时间", + "messageId": "消息 ID", + "error": "错误" + }, + "sendFailed": "消息发送失败" + }, + "infrastructure": { + "title": "基础设施", + "subtitle": "配置服务器、数据库、缓存、存储和引擎", + "saveConfig": "保存配置", + "saving": "保存中...", + "server": { + "title": "服务器配置", + "production": "生产环境", + "development": "开发环境", + "environment": "运行环境", + "domain": "域名", + "apiPort": "API 端口", + "dashboardPort": "仪表盘端口", + "corsOrigins": "CORS 来源", + "corsPlaceholder": "填写 * 或以逗号分隔的来源列表", + "publicApiUrl": "公网 API 地址(可选)", + "publicDashboardUrl": "公网仪表盘地址(可选)" + }, + "webhook": { + "title": "Webhook 与限流", + "settings": "Webhook 配置", + "timeout": "超时时间(毫秒)", + "maxRetries": "最大重试次数", + "retryDelay": "重试间隔(毫秒)", + "rateLimit": "请求限流", + "window": "时间窗口(秒)", + "maxReq": "窗口内最大请求数" + }, + "database": { + "title": "数据库配置", + "sqlite": "SQLite", + "sqliteDesc": "基于本地文件的数据库", + "postgres": "PostgreSQL", + "postgresDesc": "适合生产环境的数据库", + "useBuiltIn": "使用内置 PostgreSQL 容器", + "builtInDesc": "OpenWA 会为你自动管理一个 PostgreSQL 容器", + "dbName": "数据库名", + "poolSize": "连接池大小", + "ssl": "SSL 连接", + "sslDesc": "启用 TLS/SSL 以加密数据库连接", + "migrationsTitle": "数据库迁移", + "migrationsStatus": "Schema 由 TypeORM 自动同步", + "migrationsHint": "迁移会自动执行,如需手动迁移请使用 CLI。" + }, + "redis": { + "title": "Redis", + "enable": "启用 Redis", + "enableDesc": "缓存、会话存储和 BullMQ 队列需要 Redis", + "useBuiltIn": "使用内置 Redis 容器", + "builtInDesc": "OpenWA 会为你自动管理一个 Redis 容器", + "queueTitle": "启用 BullMQ 队列", + "queueDesc": "通过消息队列实现可靠的消息和 Webhook 投递", + "statsTitle": "队列统计", + "messageQueue": "消息队列", + "webhookQueue": "Webhook 队列", + "pending": "等待中", + "completed": "已完成", + "failed": "失败", + "clearFailed": "清空失败任务", + "viewBullMq": "查看 BullMQ 面板", + "disabledTitle": "Redis 已关闭", + "disabledDesc": "开启 Redis 以使用缓存、会话存储和 BullMQ 消息队列。", + "passwordOptional": "(可选)" + }, + "storage": { + "title": "存储配置", + "local": "本地文件系统", + "localDesc": "将媒体文件存放在本地", + "s3": "Amazon S3", + "s3Desc": "云存储(兼容 S3)", + "useBuiltIn": "使用内置 MinIO 容器", + "builtInDesc": "OpenWA 会为你自动管理一个 MinIO(S3 兼容)容器", + "storagePath": "存储路径", + "bucket": "Bucket 名称", + "region": "区域", + "accessKey": "Access Key", + "secretKey": "Secret Key", + "endpoint": "自定义 Endpoint(可选)", + "endpointHint": "适用于 MinIO 或其他 S3 兼容服务" + }, + "statusLabels": { + "connected": "已连接", + "disconnected": "已断开", + "disabled": "已关闭" + }, + "restart": { + "idleTitle": "⚙️ 配置已保存", + "restartingTitle": "🔄 正在重启服务...", + "waitingTitle": "⏳ 请稍候...", + "successTitle": "✅ 服务已就绪", + "errorTitle": "❌ 重启失败", + "idleDesc": "配置已写入 .env.generated
需要重启服务才能让改动生效。", + "later": "稍后重启", + "now": "立即重启", + "restartingMsg": "服务重启中... 已用时 {{count}} 秒", + "checking": "正在检查服务状态...", + "dontClose": "请不要关闭此窗口", + "successMsg": "服务已恢复,页面即将自动刷新。", + "errorMsg": "30 秒内未收到服务响应,请检查 Docker 日志并手动重启。", + "reload": "刷新页面" + }, + "toasts": { + "saveFailed": "保存失败" + } + }, + "plugins": { + "title": "插件", + "subtitle": "管理引擎、存储后端和各类扩展", + "refresh": "刷新", + "engineCard": "当前 WhatsApp 引擎", + "running": "运行中", + "supportedFeatures": "支持的功能:", + "more": "另有 {{count}} 项", + "builtIn": "内置", + "noDescription": "暂无描述", + "required": "必需", + "active": "已启用", + "activate": "启用", + "enable": "开启", + "disable": "关闭", + "healthCheck": "健康检查", + "configure": "配置", + "empty": { + "title": "未找到插件", + "description": "安装插件即可扩展 OpenWA 的能力" + }, + "config": { + "title": "配置 {{name}}", + "restartNotice": "需要重启服务才能让改动生效", + "engineType": "引擎类型", + "headless": "无界面模式", + "headlessDesc": "运行浏览器但不显示窗口(推荐用于生产环境)", + "sessionDataPath": "会话数据路径", + "browserArgs": "浏览器启动参数", + "noOptions": "此插件没有可配置项", + "save": "保存配置" + }, + "toasts": { + "errorTitle": "插件错误", + "errorDefault": "插件切换失败", + "healthOk": "健康检查通过", + "healthFail": "健康检查失败", + "healthError": "健康检查出错", + "savedTitle": "配置已保存", + "savedDesc": "需要重启服务才能让改动生效。", + "saveFailed": "保存失败" + } + } +} From 49328bc4c0519aef39f5529591c2e179ba4489ce Mon Sep 17 00:00:00 2001 From: rmyndharis Date: Wed, 20 May 2026 15:40:34 +0700 Subject: [PATCH 2/9] fix(swagger): apply X-API-Key security scheme globally The Swagger document defined the X-API-Key scheme via addApiKey() but never applied it, so no operation declared a security requirement and Swagger UI never sent the key. Requests reached the global ApiKeyGuard with no key and got 401 Unauthorized. Extract the config into createSwaggerConfig(), apply the scheme globally with addSecurityRequirements, and remove 5 stray @ApiBearerAuth() decorators that referenced an undefined bearer scheme. Fixes #104 --- src/config/swagger.config.spec.ts | 12 +++++++++ src/config/swagger.config.ts | 32 +++++++++++++++++++++++ src/main.ts | 18 +++---------- src/modules/auth/auth.controller.ts | 3 +-- src/modules/catalog/catalog.controller.ts | 3 +-- src/modules/plugins/plugins.controller.ts | 3 +-- src/modules/stats/stats.controller.ts | 3 +-- src/modules/status/status.controller.ts | 3 +-- 8 files changed, 52 insertions(+), 25 deletions(-) create mode 100644 src/config/swagger.config.spec.ts create mode 100644 src/config/swagger.config.ts diff --git a/src/config/swagger.config.spec.ts b/src/config/swagger.config.spec.ts new file mode 100644 index 0000000..b5e5324 --- /dev/null +++ b/src/config/swagger.config.spec.ts @@ -0,0 +1,12 @@ +import { createSwaggerConfig } from './swagger.config'; + +describe('createSwaggerConfig', () => { + // Regression test for issue #104: Swagger UI returned "Unauthorized" because the + // X-API-Key scheme was defined but never applied — no operation declared a security + // requirement, so Swagger UI never sent the key. The fix applies it globally. + it('applies the X-API-Key security scheme as a global requirement', () => { + const config = createSwaggerConfig(); + + expect(config.security).toContainEqual({ 'X-API-Key': [] }); + }); +}); diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts new file mode 100644 index 0000000..db379f6 --- /dev/null +++ b/src/config/swagger.config.ts @@ -0,0 +1,32 @@ +import { DocumentBuilder, OpenAPIObject } from '@nestjs/swagger'; + +/** + * Security scheme name for the API key, used both when defining the scheme and + * when applying it as a global requirement so Swagger UI sends the header. + */ +export const API_KEY_SECURITY_SCHEME = 'X-API-Key'; + +/** + * Builds the OpenAPI document configuration for the OpenWA API. + */ +export function createSwaggerConfig(): Omit { + return ( + new DocumentBuilder() + .setTitle('OpenWA API') + .setDescription('Open Source WhatsApp API Gateway - Free, Self-Hosted HTTP API') + .setVersion('0.1.6') + .addApiKey({ type: 'apiKey', name: 'X-API-Key', in: 'header' }, API_KEY_SECURITY_SCHEME) + // Apply the scheme globally so Swagger UI sends the key with every request + // (mirrors the global ApiKeyGuard). Without this, "Authorize" is cosmetic. + .addSecurityRequirements(API_KEY_SECURITY_SCHEME) + .addTag('sessions', 'WhatsApp session management') + .addTag('messages', 'Send and manage messages') + .addTag('webhooks', 'Webhook configuration') + .addTag('contacts', 'Contact management') + .addTag('groups', 'Group management') + .addTag('labels', 'Label management (WhatsApp Business)') + .addTag('channels', 'Channel/Newsletter management') + .addTag('health', 'Health check endpoints') + .build() + ); +} diff --git a/src/main.ts b/src/main.ts index 61966fb..d598990 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,10 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; -import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; +import { SwaggerModule } from '@nestjs/swagger'; import helmet from 'helmet'; import { AppModule } from './app.module'; import { ShutdownService } from './common/services/shutdown.service'; +import { createSwaggerConfig } from './config/swagger.config'; import * as dotenv from 'dotenv'; import * as fs from 'fs'; import * as path from 'path'; @@ -141,20 +142,7 @@ async function bootstrap() { ); // Swagger documentation - const config = new DocumentBuilder() - .setTitle('OpenWA API') - .setDescription('Open Source WhatsApp API Gateway - Free, Self-Hosted HTTP API') - .setVersion('0.1.6') - .addApiKey({ type: 'apiKey', name: 'X-API-Key', in: 'header' }, 'X-API-Key') - .addTag('sessions', 'WhatsApp session management') - .addTag('messages', 'Send and manage messages') - .addTag('webhooks', 'Webhook configuration') - .addTag('contacts', 'Contact management') - .addTag('groups', 'Group management') - .addTag('labels', 'Label management (WhatsApp Business)') - .addTag('channels', 'Channel/Newsletter management') - .addTag('health', 'Health check endpoints') - .build(); + const config = createSwaggerConfig(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api/docs', app, document); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 3d723f3..dc15780 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,12 +1,11 @@ import { Controller, Get, Post, Put, Delete, Body, Param, HttpCode, HttpStatus } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { AuthService } from './auth.service'; import { CreateApiKeyDto, UpdateApiKeyDto, ApiKeyResponseDto, ApiKeyCreatedResponseDto } from './dto'; import { RequireRole } from './decorators/auth.decorators'; import { ApiKeyRole } from './entities/api-key.entity'; @ApiTags('auth') -@ApiBearerAuth() @Controller('auth/api-keys') export class AuthController { constructor(private readonly authService: AuthService) {} diff --git a/src/modules/catalog/catalog.controller.ts b/src/modules/catalog/catalog.controller.ts index 5fece45..d2ecf98 100644 --- a/src/modules/catalog/catalog.controller.ts +++ b/src/modules/catalog/catalog.controller.ts @@ -1,10 +1,9 @@ import { Controller, Get, Post, Param, Body, Query } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { CatalogService } from './catalog.service'; import { SendProductDto, SendCatalogDto, ProductQueryDto } from './dto/send-product.dto'; @ApiTags('Catalog') -@ApiBearerAuth() @Controller('sessions/:sessionId') export class CatalogController { constructor(private readonly catalogService: CatalogService) {} diff --git a/src/modules/plugins/plugins.controller.ts b/src/modules/plugins/plugins.controller.ts index 870bcce..4792fec 100644 --- a/src/modules/plugins/plugins.controller.ts +++ b/src/modules/plugins/plugins.controller.ts @@ -1,10 +1,9 @@ import { Controller, Get, Post, Put, Param, Body, HttpCode, HttpStatus } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { PluginsService } from './plugins.service'; import { PluginDto, PluginConfigDto } from './dto/plugin.dto'; @ApiTags('plugins') -@ApiBearerAuth() @Controller('plugins') export class PluginsController { constructor(private readonly pluginsService: PluginsService) {} diff --git a/src/modules/stats/stats.controller.ts b/src/modules/stats/stats.controller.ts index 803e507..edde544 100644 --- a/src/modules/stats/stats.controller.ts +++ b/src/modules/stats/stats.controller.ts @@ -1,10 +1,9 @@ import { Controller, Get, Param, Query } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { StatsService } from './stats.service'; import { StatsQueryDto } from './dto/stats-query.dto'; @ApiTags('Statistics') -@ApiBearerAuth() @Controller('stats') export class StatsController { constructor(private readonly statsService: StatsService) {} diff --git a/src/modules/status/status.controller.ts b/src/modules/status/status.controller.ts index 34574b1..5840695 100644 --- a/src/modules/status/status.controller.ts +++ b/src/modules/status/status.controller.ts @@ -1,11 +1,10 @@ import { Controller, Get, Post, Delete, Param, Body } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { StatusService } from './status.service'; import { SendTextStatusDto } from './dto/send-text-status.dto'; import { SendImageStatusDto, SendVideoStatusDto } from './dto/send-media-status.dto'; @ApiTags('Status') -@ApiBearerAuth() @Controller('sessions/:sessionId/status') export class StatusController { constructor(private readonly statusService: StatusService) {} From 03e8aba0c1136f4ddba530202e68c4386a281f4a Mon Sep 17 00:00:00 2001 From: MastersCon Date: Wed, 20 May 2026 12:36:12 -0400 Subject: [PATCH 3/9] fix: resolve windows postinstall crash and postgresql entity validation --- package-lock.json | 51 ++++++++----------------------------- package.json | 2 +- src/database/data-source.ts | 13 ++++++++-- 3 files changed, 23 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index f3f6b17..41d3f01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -745,7 +745,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1251,7 +1250,6 @@ "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-7.1.5.tgz", "integrity": "sha512-EW0sbTtGIysu9vipdVpPQeToPqOpPgVZTt+pn1Ut3gbSS/GLWbEgIfFtMmSQDUoSL9WH00RzjgUY5K+43nWh0A==", "license": "MIT", - "peer": true, "dependencies": { "redis-info": "^3.1.0" }, @@ -1290,7 +1288,6 @@ "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-7.1.5.tgz", "integrity": "sha512-2IkatKwNRx/1M9/lAZIptcxS1FPNq6icpp2M46Upwd4olVxs/ujF9Kvs+Ff9ExtIO/OgYfwx7mG2IprGZ+nQCg==", "license": "MIT", - "peer": true, "dependencies": { "@bull-board/api": "7.1.5" } @@ -1580,7 +1577,6 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" @@ -1612,7 +1608,6 @@ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", @@ -2880,7 +2875,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-11.0.4.tgz", "integrity": "sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "2.8.1" }, @@ -2955,7 +2949,6 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3126,7 +3119,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.21.tgz", "integrity": "sha512-YV1HYDGsm2rnR0vrLKidtrG6jYX5yqiIjeur1j8++dKGqhhsJ6cjMs0RfQRSTUH7IjgDemA59/znQ8nRrE0D9g==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.3.4", "iterare": "1.2.1", @@ -3174,7 +3166,6 @@ "integrity": "sha512-fqo0BHgny3MOuAL8GSfG3ZUKFVVBaBQD/0iyibnwTONT5vPexjQxJzu+945iloVvBDmrnAaRWxC1gqCDEs/AXQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -3235,7 +3226,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.21.tgz", "integrity": "sha512-lA3ViycOnz4Df3EstIKpuAVFhqxQixTnjAVk0M+LRyNBlGM6VSCaNJaAIrb9Pcry39T4hTHpNVbRqGLSvhL8gA==", "license": "MIT", - "peer": true, "dependencies": { "cors": "2.8.6", "express": "5.2.1", @@ -3257,7 +3247,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.21.tgz", "integrity": "sha512-Tq5JgaVS+auD3DXuRBy8UMU3mf69HJO8Ep+BuRS9GYMXGd/5sdMHqIQvXlXkGih9tQXdeeG9WoqURe/+IjPKng==", "license": "MIT", - "peer": true, "dependencies": { "socket.io": "4.8.3", "tslib": "2.8.1" @@ -3443,7 +3432,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.1.tgz", "integrity": "sha512-8rw/nKT0S+L+MkzgE9F2/mox7mAgsPlwfzmW9gsESN1lmQtIrVEfiiBwC2O8+guS1jBfQehJIdcdUj2OAp4VUQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/core": "^10.0.0 || ^11.0.0", @@ -4040,7 +4028,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -4160,7 +4147,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -4382,7 +4368,6 @@ "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", @@ -5096,7 +5081,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5186,7 +5170,6 @@ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5734,7 +5717,6 @@ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -6037,7 +6019,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -6128,7 +6109,6 @@ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.76.10.tgz", "integrity": "sha512-LWve7SpQjYSpCP2GEsWmoyzTz2H37L8HRmSTu3YihYsTOr5kJxrfEX6aEV7m6eskEMWXSHZYTMZepX6qNaH6CQ==", "license": "MIT", - "peer": true, "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", @@ -6455,15 +6435,13 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-validator": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", "license": "MIT", - "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -7183,8 +7161,7 @@ "version": "0.0.1581282", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dezalgo": { "version": "1.0.4", @@ -7683,7 +7660,6 @@ "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -7740,7 +7716,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9202,7 +9177,6 @@ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", "license": "MIT", - "peer": true, "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", @@ -9487,7 +9461,6 @@ "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.4.2", "@jest/types": "30.4.1", @@ -11413,6 +11386,7 @@ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 6" } @@ -11792,7 +11766,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", @@ -12085,7 +12058,6 @@ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12774,8 +12746,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/require-directory": { "version": "2.1.1", @@ -12925,7 +12896,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -13452,7 +13422,6 @@ "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", "hasInstallScript": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.0.0", @@ -13975,7 +13944,6 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14329,7 +14297,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14514,7 +14481,6 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.29.tgz", "integrity": "sha512-wwPEX/df4l72gCmOsrs0otJZYLGA9lLQkUZCkukbsymEycV4zXv2KM7wU7v2r8L01TaCgY9ApSSqHQWBOUhEoQ==", "license": "MIT", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", @@ -14734,7 +14700,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15153,6 +15118,7 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -15171,6 +15137,7 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -15184,6 +15151,7 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -15198,6 +15166,7 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -15207,7 +15176,8 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", @@ -15215,6 +15185,7 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/package.json b/package.json index ec1de08..2ee4cb4 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dashboard:install": "cd dashboard && npm install", "dashboard:dev": "cd dashboard && npm run dev", "dashboard:build": "cd dashboard && npm run build", - "postinstall": "[ -d dashboard ] && npm run dashboard:install || true", + "postinstall": "node -e \"if (require('fs').existsSync('dashboard')) { require('child_process').execSync('npm run dashboard:install', { stdio: 'inherit' }); }\"", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", diff --git a/src/database/data-source.ts b/src/database/data-source.ts index b1715a5..9cca160 100644 --- a/src/database/data-source.ts +++ b/src/database/data-source.ts @@ -10,7 +10,11 @@ const dbType = process.env.DATABASE_TYPE || 'sqlite'; const sqliteDataSource = new DataSource({ type: 'sqlite', database: process.env.DATABASE_NAME || './data/openwa.sqlite', - entities: [__dirname + '/../**/*.entity{.ts,.js}'], + entities: [ + __dirname + '/../modules/session/**/*.entity{.ts,.js}', + __dirname + '/../modules/webhook/**/*.entity{.ts,.js}', + __dirname + '/../modules/message/**/*.entity{.ts,.js}', + ], migrations: [__dirname + '/migrations/*{.ts,.js}'], synchronize: false, logging: process.env.DATABASE_LOGGING === 'true', @@ -24,7 +28,11 @@ const postgresDataSource = new DataSource({ username: process.env.DATABASE_USERNAME, password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_NAME || 'openwa', - entities: [__dirname + '/../**/*.entity{.ts,.js}'], + entities: [ + __dirname + '/../modules/session/**/*.entity{.ts,.js}', + __dirname + '/../modules/webhook/**/*.entity{.ts,.js}', + __dirname + '/../modules/message/**/*.entity{.ts,.js}', + ], migrations: [__dirname + '/migrations/*{.ts,.js}'], synchronize: false, // Never auto-sync in production logging: process.env.DATABASE_LOGGING === 'true', @@ -41,3 +49,4 @@ const postgresDataSource = new DataSource({ // Export the appropriate data source based on DATABASE_TYPE export default dbType === 'postgres' ? postgresDataSource : sqliteDataSource; + From ea4858e2c8ccf8130e2aeec4ff5a21f02b91748d Mon Sep 17 00:00:00 2001 From: MastersCon Date: Wed, 20 May 2026 12:36:17 -0400 Subject: [PATCH 4/9] feat: add full spanish translation and custom dropdown language switcher --- dashboard/src/components/Layout.css | 94 +++++ dashboard/src/components/Layout.tsx | 32 +- dashboard/src/i18n/index.ts | 4 +- dashboard/src/i18n/locales/es.json | 513 ++++++++++++++++++++++++++++ 4 files changed, 627 insertions(+), 16 deletions(-) create mode 100644 dashboard/src/i18n/locales/es.json diff --git a/dashboard/src/components/Layout.css b/dashboard/src/components/Layout.css index 0db1288..c5f6863 100644 --- a/dashboard/src/components/Layout.css +++ b/dashboard/src/components/Layout.css @@ -195,6 +195,100 @@ color: var(--text-primary); } +.language-select-wrapper { + position: relative; + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.6rem 0.9rem; + color: var(--text-secondary); + background: none; + border: 1px solid var(--border); + border-radius: var(--radius); + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + overflow: hidden; +} + +.language-select-wrapper:hover { + background: var(--bg-light); + color: var(--text-primary); +} + +.language-icon { + flex-shrink: 0; + pointer-events: none; + z-index: 1; +} + +.language-select { + width: 100%; + border: none; + background: transparent; + color: inherit; + font-family: inherit; + font-size: inherit; + font-weight: inherit; + cursor: pointer; + outline: none; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + padding-right: 1.2rem; + padding-left: 0; + z-index: 2; +} + +.language-select option { + background: var(--bg-white); + color: var(--text-primary); +} + +.language-arrow { + position: absolute; + right: 0.9rem; + font-size: 0.6rem; + color: var(--text-muted); + pointer-events: none; + transition: transform 0.2s; + z-index: 1; +} + +/* RTL styles */ +[dir="rtl"] .language-select { + padding-right: 0; + padding-left: 1.2rem; +} + +[dir="rtl"] .language-arrow { + right: auto; + left: 0.9rem; +} + +/* Collapsed sidebar styles */ +.sidebar.collapsed .language-select-wrapper { + justify-content: center; + padding: 0.6rem; + width: auto; + overflow: visible; +} + +.sidebar.collapsed .language-select { + position: absolute; + inset: 0; + opacity: 0; + width: 100%; + height: 100%; + padding: 0; +} + +.sidebar.collapsed .language-arrow { + display: none; +} + .logout-btn { display: flex; align-items: center; diff --git a/dashboard/src/components/Layout.tsx b/dashboard/src/components/Layout.tsx index f2815bb..3c34a68 100644 --- a/dashboard/src/components/Layout.tsx +++ b/dashboard/src/components/Layout.tsx @@ -22,7 +22,7 @@ import { } from 'lucide-react'; import { useTheme } from '../hooks/useTheme'; import { type UserRole } from '../hooks/useRole'; -import { supportedLanguages, type SupportedLanguage } from '../i18n'; +import { type SupportedLanguage } from '../i18n'; import './Layout.css'; interface LayoutProps { @@ -80,12 +80,9 @@ export function Layout({ onLogout, userRole }: LayoutProps) { const toggleMobile = () => setIsMobileOpen(!isMobileOpen); const currentLang = (i18n.resolvedLanguage || i18n.language || 'en').split('-')[0] as SupportedLanguage; - const cycleLanguage = () => { - const idx = supportedLanguages.indexOf(currentLang); - const next = supportedLanguages[(idx + 1) % supportedLanguages.length]; - void i18n.changeLanguage(next); + const handleLanguageChange = (lang: string) => { + void i18n.changeLanguage(lang); }; - const languageLabel = currentLang === 'he' ? 'עברית' : 'EN'; const isRtl = currentLang === 'he'; return ( @@ -151,15 +148,20 @@ export function Layout({ onLogout, userRole }: LayoutProps) {
- +
+ + + {!isCollapsed && } +
- {messageType === 'text' ? ( + {messageType === 'text' && (
- -