ChiragPatankar commited on
Commit
1cf0854
·
verified ·
1 Parent(s): ac3e952

Upload 33 files

Browse files
.env ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Server Configuration
2
+ PORT=3001
3
+ NODE_ENV=development
4
+
5
+ # JWT Secret (generate a secure random string)
6
+ JWT_SECRET=your-super-secure-jwt-secret-key-here-min-32-chars
7
+
8
+ # Google OAuth
9
+ GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
10
+ GOOGLE_CLIENT_SECRET=your-google-client-secret
11
+
12
+ # Database
13
+ DATABASE_URL=./database.sqlite
14
+
15
+ # MCP Server Configuration
16
+ MCP_SERVER_URL=https://gemini-mcp-server-production.up.railway.app
17
+ MCP_AUTH_TOKEN=test-token
18
+
19
+ # Email Configuration (optional - for notifications)
20
+ SMTP_HOST=smtp.gmail.com
21
+ SMTP_PORT=587
22
+ SMTP_USER=your-email@gmail.com
23
+ SMTP_PASS=your-app-password
24
+
25
+ # Frontend URL
26
+ FRONTEND_URL=http://localhost:5173
27
+
28
+ # File Upload
29
+ MAX_FILE_SIZE=10485760
30
+ UPLOAD_DIR=./uploads
.env.production ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ VITE_API_URL=https://watery-light-production.up.railway.app/api
2
+ VITE_MCP_AUTH_TOKEN=test-token
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ database.sqlite filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ node_modules/
2
+ .env
3
+ database.sqlite
4
+ dist/
5
+ uploads/
6
+ *.log
database.sqlite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:838c9a68ad659fe7ba4f31586e43b381cb01ce2e074fb514bc61af9d1952fb77
3
+ size 131072
env.example ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Server Configuration
2
+ PORT=3001
3
+ NODE_ENV=development
4
+
5
+ # JWT Secret (generate a secure random string)
6
+ JWT_SECRET=your-super-secure-jwt-secret-key-here-min-32-chars
7
+
8
+ # Google OAuth
9
+ GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
10
+ GOOGLE_CLIENT_SECRET=your-google-client-secret
11
+
12
+ # Database
13
+ DATABASE_URL=./database.sqlite
14
+
15
+ # MCP Server Configuration
16
+ MCP_SERVER_URL=https://gemini-mcp-server-production.up.railway.app
17
+ MCP_AUTH_TOKEN=test-token
18
+
19
+ # Email Configuration (optional - for notifications)
20
+ SMTP_HOST=smtp.gmail.com
21
+ SMTP_PORT=587
22
+ SMTP_USER=your-email@gmail.com
23
+ SMTP_PASS=your-app-password
24
+
25
+ # Frontend URL
26
+ FRONTEND_URL=http://localhost:5173
27
+
28
+ # File Upload
29
+ MAX_FILE_SIZE=10485760
30
+ UPLOAD_DIR=./uploads
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "mcp-chat-support-backend",
3
+ "version": "1.0.0",
4
+ "description": "Backend API for MCP Chat Support Platform",
5
+ "main": "dist/server.js",
6
+ "scripts": {
7
+ "dev": "tsx watch src/server.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/server.js",
10
+ "migrate": "tsx src/db/migrate.ts"
11
+ },
12
+ "dependencies": {
13
+ "axios": "^1.6.2",
14
+ "bcryptjs": "^2.4.3",
15
+ "cors": "^2.8.5",
16
+ "dotenv": "^16.3.1",
17
+ "express": "^4.18.2",
18
+ "express-rate-limit": "^7.1.5",
19
+ "express-validator": "^7.0.1",
20
+ "google-auth-library": "^9.4.1",
21
+ "helmet": "^7.1.0",
22
+ "jsonwebtoken": "^9.0.2",
23
+ "multer": "^1.4.5-lts.1",
24
+ "nodemailer": "^6.9.7",
25
+ "sqlite3": "^5.1.6",
26
+ "uuid": "^9.0.1",
27
+ "ws": "^8.14.2",
28
+ "zod": "^3.22.4"
29
+ },
30
+ "devDependencies": {
31
+ "@types/bcryptjs": "^2.4.6",
32
+ "@types/cors": "^2.8.17",
33
+ "@types/express": "^4.17.21",
34
+ "@types/jsonwebtoken": "^9.0.5",
35
+ "@types/multer": "^1.4.11",
36
+ "@types/node": "^22.15.29",
37
+ "@types/nodemailer": "^6.4.14",
38
+ "@types/uuid": "^9.0.7",
39
+ "@types/ws": "^8.5.10",
40
+ "tsx": "^4.6.2",
41
+ "typescript": "^5.3.3"
42
+ }
43
+ }
render.yaml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ - type: web
3
+ name: mcp-chat-backend
4
+ env: node
5
+ buildCommand: npm install && npm run build
6
+ startCommand: npm start
7
+ envVars:
8
+ - key: NODE_ENV
9
+ value: production
10
+ - key: PORT
11
+ value: 3001
12
+ - key: JWT_SECRET
13
+ generateValue: true
14
+ - key: MCP_SERVER_URL
15
+ value: https://gemini-mcp-server-production.up.railway.app
16
+ - key: MCP_AUTH_TOKEN
17
+ value: test-token
18
+ - key: FRONTEND_URL
19
+ value: https://mcp-chat-support.vercel.app
src/db/database.js ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.database = void 0;
40
+ exports.initializeDatabase = initializeDatabase;
41
+ var sqlite3_1 = require("sqlite3");
42
+ var util_1 = require("util");
43
+ var path_1 = require("path");
44
+ var fs_1 = require("fs");
45
+ // Enable verbose mode for debugging
46
+ var Database = sqlite3_1.default.verbose().Database;
47
+ var DatabaseConnection = /** @class */ (function () {
48
+ function DatabaseConnection() {
49
+ this.db = null;
50
+ }
51
+ DatabaseConnection.prototype.connect = function () {
52
+ return __awaiter(this, void 0, void 0, function () {
53
+ var dbPath, dbDir;
54
+ var _this = this;
55
+ return __generator(this, function (_a) {
56
+ if (this.db) {
57
+ return [2 /*return*/, this.db];
58
+ }
59
+ dbPath = process.env.DATABASE_URL || path_1.default.join(__dirname, '../../database.sqlite');
60
+ dbDir = path_1.default.dirname(dbPath);
61
+ if (!fs_1.default.existsSync(dbDir)) {
62
+ fs_1.default.mkdirSync(dbDir, { recursive: true });
63
+ }
64
+ return [2 /*return*/, new Promise(function (resolve, reject) {
65
+ _this.db = new Database(dbPath, function (err) {
66
+ if (err) {
67
+ console.error('Error opening database:', err.message);
68
+ reject(err);
69
+ }
70
+ else {
71
+ console.log('Connected to SQLite database');
72
+ resolve(_this.db);
73
+ }
74
+ });
75
+ })];
76
+ });
77
+ });
78
+ };
79
+ DatabaseConnection.prototype.query = function (sql_1) {
80
+ return __awaiter(this, arguments, void 0, function (sql, params) {
81
+ var db, all;
82
+ if (params === void 0) { params = []; }
83
+ return __generator(this, function (_a) {
84
+ switch (_a.label) {
85
+ case 0: return [4 /*yield*/, this.connect()];
86
+ case 1:
87
+ db = _a.sent();
88
+ all = (0, util_1.promisify)(db.all.bind(db));
89
+ return [2 /*return*/, all(sql, params)];
90
+ }
91
+ });
92
+ });
93
+ };
94
+ DatabaseConnection.prototype.run = function (sql_1) {
95
+ return __awaiter(this, arguments, void 0, function (sql, params) {
96
+ var db, run;
97
+ if (params === void 0) { params = []; }
98
+ return __generator(this, function (_a) {
99
+ switch (_a.label) {
100
+ case 0: return [4 /*yield*/, this.connect()];
101
+ case 1:
102
+ db = _a.sent();
103
+ run = (0, util_1.promisify)(db.run.bind(db));
104
+ return [2 /*return*/, run(sql, params)];
105
+ }
106
+ });
107
+ });
108
+ };
109
+ DatabaseConnection.prototype.get = function (sql_1) {
110
+ return __awaiter(this, arguments, void 0, function (sql, params) {
111
+ var db, get;
112
+ if (params === void 0) { params = []; }
113
+ return __generator(this, function (_a) {
114
+ switch (_a.label) {
115
+ case 0: return [4 /*yield*/, this.connect()];
116
+ case 1:
117
+ db = _a.sent();
118
+ get = (0, util_1.promisify)(db.get.bind(db));
119
+ return [2 /*return*/, get(sql, params)];
120
+ }
121
+ });
122
+ });
123
+ };
124
+ DatabaseConnection.prototype.close = function () {
125
+ return __awaiter(this, void 0, void 0, function () {
126
+ var _this = this;
127
+ return __generator(this, function (_a) {
128
+ if (this.db) {
129
+ return [2 /*return*/, new Promise(function (resolve, reject) {
130
+ _this.db.close(function (err) {
131
+ if (err) {
132
+ reject(err);
133
+ }
134
+ else {
135
+ _this.db = null;
136
+ resolve();
137
+ }
138
+ });
139
+ })];
140
+ }
141
+ return [2 /*return*/];
142
+ });
143
+ });
144
+ };
145
+ return DatabaseConnection;
146
+ }());
147
+ // Singleton instance
148
+ exports.database = new DatabaseConnection();
149
+ // Database initialization function
150
+ function initializeDatabase() {
151
+ return __awaiter(this, void 0, void 0, function () {
152
+ var createTables, createIndexes, _i, createTables_1, sql, _a, createIndexes_1, sql, error_1;
153
+ return __generator(this, function (_b) {
154
+ switch (_b.label) {
155
+ case 0:
156
+ createTables = [
157
+ // Users table
158
+ "CREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n email TEXT UNIQUE NOT NULL,\n name TEXT NOT NULL,\n password_hash TEXT,\n avatar TEXT,\n google_id TEXT UNIQUE,\n email_verified BOOLEAN DEFAULT FALSE,\n verification_token TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP\n )",
159
+ // Tenants table
160
+ "CREATE TABLE IF NOT EXISTS tenants (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n subdomain TEXT UNIQUE,\n plan TEXT DEFAULT 'starter',\n settings TEXT DEFAULT '{}',\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP\n )",
161
+ // User-Tenant relationships
162
+ "CREATE TABLE IF NOT EXISTS user_tenants (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n tenant_id INTEGER NOT NULL,\n role TEXT DEFAULT 'owner',\n permissions TEXT DEFAULT '{}',\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,\n FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE,\n UNIQUE(user_id, tenant_id)\n )",
163
+ // Domains table
164
+ "CREATE TABLE IF NOT EXISTS domains (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n tenant_id INTEGER NOT NULL,\n domain TEXT NOT NULL,\n verified BOOLEAN DEFAULT FALSE,\n verification_token TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE\n )",
165
+ // Knowledge base table
166
+ "CREATE TABLE IF NOT EXISTS knowledge_base (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n tenant_id INTEGER NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n source TEXT NOT NULL,\n status TEXT DEFAULT 'processing',\n size INTEGER,\n metadata TEXT DEFAULT '{}',\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE\n )",
167
+ // Chat sessions table
168
+ "CREATE TABLE IF NOT EXISTS chat_sessions (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n tenant_id INTEGER NOT NULL,\n domain TEXT,\n user_ip TEXT,\n user_agent TEXT,\n session_token TEXT UNIQUE NOT NULL,\n started_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n ended_at DATETIME,\n resolved BOOLEAN DEFAULT FALSE,\n rating INTEGER,\n feedback TEXT,\n FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE\n )",
169
+ // Chat messages table
170
+ "CREATE TABLE IF NOT EXISTS chat_messages (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id INTEGER NOT NULL,\n sender TEXT NOT NULL, -- 'user' or 'ai'\n message TEXT NOT NULL,\n metadata TEXT DEFAULT '{}',\n timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (session_id) REFERENCES chat_sessions (id) ON DELETE CASCADE\n )",
171
+ // Analytics events table
172
+ "CREATE TABLE IF NOT EXISTS analytics_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n tenant_id INTEGER NOT NULL,\n event_type TEXT NOT NULL,\n event_data TEXT DEFAULT '{}',\n timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE\n )",
173
+ // Widget configurations table
174
+ "CREATE TABLE IF NOT EXISTS widget_configs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n tenant_id INTEGER NOT NULL,\n config TEXT NOT NULL DEFAULT '{}',\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE\n )"
175
+ ];
176
+ createIndexes = [
177
+ "CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)",
178
+ "CREATE INDEX IF NOT EXISTS idx_users_google_id ON users(google_id)",
179
+ "CREATE INDEX IF NOT EXISTS idx_tenants_subdomain ON tenants(subdomain)",
180
+ "CREATE INDEX IF NOT EXISTS idx_user_tenants_user_id ON user_tenants(user_id)",
181
+ "CREATE INDEX IF NOT EXISTS idx_user_tenants_tenant_id ON user_tenants(tenant_id)",
182
+ "CREATE INDEX IF NOT EXISTS idx_domains_tenant_id ON domains(tenant_id)",
183
+ "CREATE INDEX IF NOT EXISTS idx_knowledge_base_tenant_id ON knowledge_base(tenant_id)",
184
+ "CREATE INDEX IF NOT EXISTS idx_chat_sessions_tenant_id ON chat_sessions(tenant_id)",
185
+ "CREATE INDEX IF NOT EXISTS idx_chat_sessions_token ON chat_sessions(session_token)",
186
+ "CREATE INDEX IF NOT EXISTS idx_chat_messages_session_id ON chat_messages(session_id)",
187
+ "CREATE INDEX IF NOT EXISTS idx_analytics_events_tenant_id ON analytics_events(tenant_id)",
188
+ "CREATE INDEX IF NOT EXISTS idx_analytics_events_timestamp ON analytics_events(timestamp)"
189
+ ];
190
+ _b.label = 1;
191
+ case 1:
192
+ _b.trys.push([1, 10, , 11]);
193
+ _i = 0, createTables_1 = createTables;
194
+ _b.label = 2;
195
+ case 2:
196
+ if (!(_i < createTables_1.length)) return [3 /*break*/, 5];
197
+ sql = createTables_1[_i];
198
+ return [4 /*yield*/, exports.database.run(sql)];
199
+ case 3:
200
+ _b.sent();
201
+ _b.label = 4;
202
+ case 4:
203
+ _i++;
204
+ return [3 /*break*/, 2];
205
+ case 5:
206
+ _a = 0, createIndexes_1 = createIndexes;
207
+ _b.label = 6;
208
+ case 6:
209
+ if (!(_a < createIndexes_1.length)) return [3 /*break*/, 9];
210
+ sql = createIndexes_1[_a];
211
+ return [4 /*yield*/, exports.database.run(sql)];
212
+ case 7:
213
+ _b.sent();
214
+ _b.label = 8;
215
+ case 8:
216
+ _a++;
217
+ return [3 /*break*/, 6];
218
+ case 9:
219
+ console.log('Database tables and indexes created successfully');
220
+ return [3 /*break*/, 11];
221
+ case 10:
222
+ error_1 = _b.sent();
223
+ console.error('Error initializing database:', error_1);
224
+ throw error_1;
225
+ case 11: return [2 /*return*/];
226
+ }
227
+ });
228
+ });
229
+ }
src/db/database.ts ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3 from 'sqlite3';
2
+ import { promisify } from 'util';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+
6
+ // Enable verbose mode for debugging
7
+ const Database = sqlite3.verbose().Database;
8
+
9
+ class DatabaseConnection {
10
+ private db: sqlite3.Database | null = null;
11
+
12
+ async connect(): Promise<sqlite3.Database> {
13
+ if (this.db) {
14
+ return this.db;
15
+ }
16
+
17
+ const dbPath = process.env.DATABASE_URL || path.join(__dirname, '../../database.sqlite');
18
+
19
+ // Ensure directory exists
20
+ const dbDir = path.dirname(dbPath);
21
+ if (!fs.existsSync(dbDir)) {
22
+ fs.mkdirSync(dbDir, { recursive: true });
23
+ }
24
+
25
+ return new Promise((resolve, reject) => {
26
+ this.db = new Database(dbPath, (err) => {
27
+ if (err) {
28
+ console.error('Error opening database:', err.message);
29
+ reject(err);
30
+ } else {
31
+ console.log('Connected to SQLite database');
32
+ resolve(this.db!);
33
+ }
34
+ });
35
+ });
36
+ }
37
+
38
+ async query(sql: string, params: any[] = []): Promise<any[]> {
39
+ const db = await this.connect();
40
+ return new Promise((resolve, reject) => {
41
+ db.all(sql, params, (err, rows) => {
42
+ if (err) reject(err);
43
+ else resolve(rows || []);
44
+ });
45
+ });
46
+ }
47
+
48
+ async run(sql: string, params: any[] = []): Promise<{ lastID: number; changes: number }> {
49
+ const db = await this.connect();
50
+ return new Promise((resolve, reject) => {
51
+ db.run(sql, params, function(err) {
52
+ if (err) {
53
+ reject(err);
54
+ } else {
55
+ resolve({
56
+ lastID: this.lastID || 0,
57
+ changes: this.changes || 0
58
+ });
59
+ }
60
+ });
61
+ });
62
+ }
63
+
64
+ async get(sql: string, params: any[] = []): Promise<any> {
65
+ const db = await this.connect();
66
+ return new Promise((resolve, reject) => {
67
+ db.get(sql, params, (err, row) => {
68
+ if (err) reject(err);
69
+ else resolve(row);
70
+ });
71
+ });
72
+ }
73
+
74
+ async close(): Promise<void> {
75
+ if (this.db) {
76
+ return new Promise((resolve, reject) => {
77
+ this.db!.close((err) => {
78
+ if (err) {
79
+ reject(err);
80
+ } else {
81
+ this.db = null;
82
+ resolve();
83
+ }
84
+ });
85
+ });
86
+ }
87
+ }
88
+ }
89
+
90
+ // Singleton instance
91
+ export const database = new DatabaseConnection();
92
+
93
+ // Database initialization function
94
+ export async function initializeDatabase(): Promise<void> {
95
+ const createTables = [
96
+ // Users table
97
+ `CREATE TABLE IF NOT EXISTS users (
98
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
99
+ email TEXT UNIQUE NOT NULL,
100
+ name TEXT NOT NULL,
101
+ password_hash TEXT,
102
+ avatar TEXT,
103
+ google_id TEXT UNIQUE,
104
+ email_verified BOOLEAN DEFAULT FALSE,
105
+ verification_token TEXT,
106
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
107
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
108
+ )`,
109
+
110
+ // Tenants table
111
+ `CREATE TABLE IF NOT EXISTS tenants (
112
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
113
+ name TEXT NOT NULL,
114
+ subdomain TEXT UNIQUE,
115
+ plan TEXT DEFAULT 'starter',
116
+ settings TEXT DEFAULT '{}',
117
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
118
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
119
+ )`,
120
+
121
+ // User-Tenant relationships
122
+ `CREATE TABLE IF NOT EXISTS user_tenants (
123
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
124
+ user_id INTEGER NOT NULL,
125
+ tenant_id INTEGER NOT NULL,
126
+ role TEXT DEFAULT 'owner',
127
+ permissions TEXT DEFAULT '{}',
128
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
129
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
130
+ FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE,
131
+ UNIQUE(user_id, tenant_id)
132
+ )`,
133
+
134
+ // Domains table
135
+ `CREATE TABLE IF NOT EXISTS domains (
136
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
137
+ tenant_id INTEGER NOT NULL,
138
+ domain TEXT NOT NULL,
139
+ verified BOOLEAN DEFAULT FALSE,
140
+ verification_token TEXT,
141
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
142
+ FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE
143
+ )`,
144
+
145
+ // Knowledge base table
146
+ `CREATE TABLE IF NOT EXISTS knowledge_base (
147
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
148
+ tenant_id INTEGER NOT NULL,
149
+ name TEXT NOT NULL,
150
+ type TEXT NOT NULL,
151
+ source TEXT NOT NULL,
152
+ status TEXT DEFAULT 'processing',
153
+ size INTEGER,
154
+ metadata TEXT DEFAULT '{}',
155
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
156
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
157
+ FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE
158
+ )`,
159
+
160
+ // Chat sessions table
161
+ `CREATE TABLE IF NOT EXISTS chat_sessions (
162
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
163
+ tenant_id INTEGER NOT NULL,
164
+ domain TEXT,
165
+ user_ip TEXT,
166
+ user_agent TEXT,
167
+ session_token TEXT UNIQUE NOT NULL,
168
+ started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
169
+ ended_at DATETIME,
170
+ resolved BOOLEAN DEFAULT FALSE,
171
+ rating INTEGER,
172
+ feedback TEXT,
173
+ FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE
174
+ )`,
175
+
176
+ // Chat messages table
177
+ `CREATE TABLE IF NOT EXISTS chat_messages (
178
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
179
+ session_id INTEGER NOT NULL,
180
+ sender TEXT NOT NULL, -- 'user' or 'ai'
181
+ message TEXT NOT NULL,
182
+ metadata TEXT DEFAULT '{}',
183
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
184
+ FOREIGN KEY (session_id) REFERENCES chat_sessions (id) ON DELETE CASCADE
185
+ )`,
186
+
187
+ // Analytics events table
188
+ `CREATE TABLE IF NOT EXISTS analytics_events (
189
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
190
+ tenant_id INTEGER NOT NULL,
191
+ event_type TEXT NOT NULL,
192
+ event_data TEXT DEFAULT '{}',
193
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
194
+ FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE
195
+ )`,
196
+
197
+ // Widget configurations table
198
+ `CREATE TABLE IF NOT EXISTS widget_configs (
199
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
200
+ tenant_id INTEGER NOT NULL,
201
+ config TEXT NOT NULL DEFAULT '{}',
202
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
203
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
204
+ FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE
205
+ )`
206
+ ];
207
+
208
+ const createIndexes = [
209
+ `CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)`,
210
+ `CREATE INDEX IF NOT EXISTS idx_users_google_id ON users(google_id)`,
211
+ `CREATE INDEX IF NOT EXISTS idx_tenants_subdomain ON tenants(subdomain)`,
212
+ `CREATE INDEX IF NOT EXISTS idx_user_tenants_user_id ON user_tenants(user_id)`,
213
+ `CREATE INDEX IF NOT EXISTS idx_user_tenants_tenant_id ON user_tenants(tenant_id)`,
214
+ `CREATE INDEX IF NOT EXISTS idx_domains_tenant_id ON domains(tenant_id)`,
215
+ `CREATE INDEX IF NOT EXISTS idx_knowledge_base_tenant_id ON knowledge_base(tenant_id)`,
216
+ `CREATE INDEX IF NOT EXISTS idx_chat_sessions_tenant_id ON chat_sessions(tenant_id)`,
217
+ `CREATE INDEX IF NOT EXISTS idx_chat_sessions_token ON chat_sessions(session_token)`,
218
+ `CREATE INDEX IF NOT EXISTS idx_chat_messages_session_id ON chat_messages(session_id)`,
219
+ `CREATE INDEX IF NOT EXISTS idx_analytics_events_tenant_id ON analytics_events(tenant_id)`,
220
+ `CREATE INDEX IF NOT EXISTS idx_analytics_events_timestamp ON analytics_events(timestamp)`
221
+ ];
222
+
223
+ try {
224
+ // Create tables
225
+ for (const sql of createTables) {
226
+ await database.run(sql);
227
+ }
228
+
229
+ // Create indexes
230
+ for (const sql of createIndexes) {
231
+ await database.run(sql);
232
+ }
233
+
234
+ console.log('Database tables and indexes created successfully');
235
+ } catch (error) {
236
+ console.error('Error initializing database:', error);
237
+ throw error;
238
+ }
239
+ }
src/middleware/auth.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authenticateToken = void 0;
4
+ var jsonwebtoken_1 = require("jsonwebtoken");
5
+ var authenticateToken = function (req, res, next) {
6
+ var authHeader = req.headers['authorization'];
7
+ var token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
8
+ if (!token) {
9
+ return res.status(401).json({ error: 'Access token required' });
10
+ }
11
+ try {
12
+ var decoded = jsonwebtoken_1.default.verify(token, process.env.JWT_SECRET);
13
+ req.user = {
14
+ userId: decoded.userId,
15
+ tenantId: decoded.tenantId
16
+ };
17
+ next();
18
+ }
19
+ catch (error) {
20
+ return res.status(403).json({ error: 'Invalid or expired token' });
21
+ }
22
+ };
23
+ exports.authenticateToken = authenticateToken;
src/middleware/auth.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import jwt from 'jsonwebtoken';
2
+ import { Request, Response, NextFunction } from 'express';
3
+
4
+ export interface AuthenticatedRequest extends Request {
5
+ user?: {
6
+ userId: number;
7
+ tenantId: number;
8
+ };
9
+ }
10
+
11
+ export const authenticateToken = (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
12
+ const authHeader = req.headers['authorization'];
13
+ const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
14
+
15
+ if (!token) {
16
+ return res.status(401).json({ error: 'Access token required' });
17
+ }
18
+
19
+ try {
20
+ const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;
21
+ req.user = {
22
+ userId: decoded.userId,
23
+ tenantId: decoded.tenantId
24
+ };
25
+ next();
26
+ } catch (error) {
27
+ return res.status(403).json({ error: 'Invalid or expired token' });
28
+ }
29
+ };
src/middleware/errorHandler.js ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.errorHandler = void 0;
15
+ var errorHandler = function (error, req, res, next) {
16
+ var statusCode = error.statusCode || 500;
17
+ var message = error.message || 'Internal Server Error';
18
+ // Log error for debugging
19
+ console.error('Error:', error);
20
+ // Don't leak error details in production
21
+ var isDevelopment = process.env.NODE_ENV === 'development';
22
+ res.status(statusCode).json(__assign({ error: message }, (isDevelopment && { stack: error.stack })));
23
+ };
24
+ exports.errorHandler = errorHandler;
src/middleware/errorHandler.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+
3
+ export interface AppError extends Error {
4
+ statusCode?: number;
5
+ isOperational?: boolean;
6
+ }
7
+
8
+ export const errorHandler = (
9
+ error: AppError,
10
+ req: Request,
11
+ res: Response,
12
+ next: NextFunction
13
+ ) => {
14
+ const statusCode = error.statusCode || 500;
15
+ const message = error.message || 'Internal Server Error';
16
+
17
+ // Log error for debugging
18
+ console.error('Error:', error);
19
+
20
+ // Don't leak error details in production
21
+ const isDevelopment = process.env.NODE_ENV === 'development';
22
+
23
+ res.status(statusCode).json({
24
+ error: message,
25
+ ...(isDevelopment && { stack: error.stack })
26
+ });
27
+ };
src/routes/analytics.js ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
39
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
40
+ if (ar || !(i in from)) {
41
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
42
+ ar[i] = from[i];
43
+ }
44
+ }
45
+ return to.concat(ar || Array.prototype.slice.call(from));
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ var express_1 = require("express");
49
+ var database_1 = require("../db/database");
50
+ var router = express_1.default.Router();
51
+ // Get dashboard metrics
52
+ router.get('/metrics', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
53
+ var tenantId, totalConversations, thisMonthConversations, avgRating, resolutionRate, knowledgeBaseCount, error_1;
54
+ return __generator(this, function (_a) {
55
+ switch (_a.label) {
56
+ case 0:
57
+ _a.trys.push([0, 6, , 7]);
58
+ tenantId = req.user.tenantId;
59
+ return [4 /*yield*/, database_1.database.get('SELECT COUNT(*) as count FROM chat_sessions WHERE tenant_id = ?', [tenantId])];
60
+ case 1:
61
+ totalConversations = _a.sent();
62
+ return [4 /*yield*/, database_1.database.get("SELECT COUNT(*) as count FROM chat_sessions \n WHERE tenant_id = ? AND created_at >= date('now', 'start of month')", [tenantId])];
63
+ case 2:
64
+ thisMonthConversations = _a.sent();
65
+ return [4 /*yield*/, database_1.database.get('SELECT AVG(rating) as avg FROM chat_sessions WHERE tenant_id = ? AND rating IS NOT NULL', [tenantId])];
66
+ case 3:
67
+ avgRating = _a.sent();
68
+ return [4 /*yield*/, database_1.database.get("SELECT \n COUNT(CASE WHEN resolved = 1 THEN 1 END) * 100.0 / COUNT(*) as rate\n FROM chat_sessions WHERE tenant_id = ?", [tenantId])];
69
+ case 4:
70
+ resolutionRate = _a.sent();
71
+ return [4 /*yield*/, database_1.database.get('SELECT COUNT(*) as count FROM knowledge_base WHERE tenant_id = ? AND status = "active"', [tenantId])];
72
+ case 5:
73
+ knowledgeBaseCount = _a.sent();
74
+ res.json({
75
+ totalConversations: totalConversations.count || 0,
76
+ thisMonthConversations: thisMonthConversations.count || 0,
77
+ averageRating: parseFloat((avgRating.avg || 0).toFixed(1)),
78
+ resolutionRate: parseFloat((resolutionRate.rate || 0).toFixed(1)),
79
+ knowledgeBaseDocuments: knowledgeBaseCount.count || 0
80
+ });
81
+ return [3 /*break*/, 7];
82
+ case 6:
83
+ error_1 = _a.sent();
84
+ console.error('Get metrics error:', error_1);
85
+ res.status(500).json({ error: 'Internal server error' });
86
+ return [3 /*break*/, 7];
87
+ case 7: return [2 /*return*/];
88
+ }
89
+ });
90
+ }); });
91
+ // Get conversation trends
92
+ router.get('/conversations', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
93
+ var tenantId, _a, period, dateRange, conversations, error_2;
94
+ return __generator(this, function (_b) {
95
+ switch (_b.label) {
96
+ case 0:
97
+ _b.trys.push([0, 2, , 3]);
98
+ tenantId = req.user.tenantId;
99
+ _a = req.query.period, period = _a === void 0 ? '7d' : _a;
100
+ dateRange = '';
101
+ switch (period) {
102
+ case '1d':
103
+ dateRange = "date('now', '-1 day')";
104
+ break;
105
+ case '7d':
106
+ dateRange = "date('now', '-7 days')";
107
+ break;
108
+ case '30d':
109
+ dateRange = "date('now', '-30 days')";
110
+ break;
111
+ default:
112
+ dateRange = "date('now', '-7 days')";
113
+ }
114
+ return [4 /*yield*/, database_1.database.query("SELECT \n date(started_at) as date,\n COUNT(*) as count,\n AVG(CASE WHEN rating IS NOT NULL THEN rating END) as avg_rating,\n COUNT(CASE WHEN resolved = 1 THEN 1 END) * 100.0 / COUNT(*) as resolution_rate\n FROM chat_sessions \n WHERE tenant_id = ? AND started_at >= ".concat(dateRange, "\n GROUP BY date(started_at)\n ORDER BY date"), [tenantId])];
115
+ case 1:
116
+ conversations = _b.sent();
117
+ res.json({ conversations: conversations });
118
+ return [3 /*break*/, 3];
119
+ case 2:
120
+ error_2 = _b.sent();
121
+ console.error('Get conversations error:', error_2);
122
+ res.status(500).json({ error: 'Internal server error' });
123
+ return [3 /*break*/, 3];
124
+ case 3: return [2 /*return*/];
125
+ }
126
+ });
127
+ }); });
128
+ // Get chat history with filters
129
+ router.get('/chat-history', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
130
+ var tenantId, _a, _b, limit, _c, offset, status_1, sentiment, whereClause, params, conversations, error_3;
131
+ return __generator(this, function (_d) {
132
+ switch (_d.label) {
133
+ case 0:
134
+ _d.trys.push([0, 2, , 3]);
135
+ tenantId = req.user.tenantId;
136
+ _a = req.query, _b = _a.limit, limit = _b === void 0 ? 50 : _b, _c = _a.offset, offset = _c === void 0 ? 0 : _c, status_1 = _a.status, sentiment = _a.sentiment;
137
+ whereClause = 'WHERE cs.tenant_id = ?';
138
+ params = [tenantId];
139
+ if (status_1 === 'resolved') {
140
+ whereClause += ' AND cs.resolved = 1';
141
+ }
142
+ else if (status_1 === 'unresolved') {
143
+ whereClause += ' AND cs.resolved = 0';
144
+ }
145
+ return [4 /*yield*/, database_1.database.query("SELECT \n cs.id, cs.session_token, cs.started_at, cs.ended_at, cs.resolved, cs.rating, cs.feedback,\n (SELECT message FROM chat_messages WHERE session_id = cs.id AND sender = 'user' ORDER BY timestamp LIMIT 1) as first_message,\n (SELECT COUNT(*) FROM chat_messages WHERE session_id = cs.id) as message_count\n FROM chat_sessions cs\n ".concat(whereClause, "\n ORDER BY cs.started_at DESC\n LIMIT ? OFFSET ?"), __spreadArray(__spreadArray([], params, true), [limit, offset], false))];
146
+ case 1:
147
+ conversations = _d.sent();
148
+ res.json({ conversations: conversations });
149
+ return [3 /*break*/, 3];
150
+ case 2:
151
+ error_3 = _d.sent();
152
+ console.error('Get chat history error:', error_3);
153
+ res.status(500).json({ error: 'Internal server error' });
154
+ return [3 /*break*/, 3];
155
+ case 3: return [2 /*return*/];
156
+ }
157
+ });
158
+ }); });
159
+ // Get top questions/issues
160
+ router.get('/top-questions', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
161
+ var tenantId, topQuestions, error_4;
162
+ return __generator(this, function (_a) {
163
+ switch (_a.label) {
164
+ case 0:
165
+ _a.trys.push([0, 2, , 3]);
166
+ tenantId = req.user.tenantId;
167
+ return [4 /*yield*/, database_1.database.query("SELECT \n cm.message,\n COUNT(*) as frequency\n FROM chat_messages cm\n JOIN chat_sessions cs ON cm.session_id = cs.id\n WHERE cs.tenant_id = ? AND cm.sender = 'user'\n GROUP BY cm.message\n HAVING frequency > 1\n ORDER BY frequency DESC\n LIMIT 10", [tenantId])];
168
+ case 1:
169
+ topQuestions = _a.sent();
170
+ res.json({ topQuestions: topQuestions });
171
+ return [3 /*break*/, 3];
172
+ case 2:
173
+ error_4 = _a.sent();
174
+ console.error('Get top questions error:', error_4);
175
+ res.status(500).json({ error: 'Internal server error' });
176
+ return [3 /*break*/, 3];
177
+ case 3: return [2 /*return*/];
178
+ }
179
+ });
180
+ }); });
181
+ // Get sentiment analysis
182
+ router.get('/sentiment', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
183
+ var tenantId, totalSessions, resolvedSessions, highRatingSessions, total, positive, negative, neutral, error_5;
184
+ return __generator(this, function (_a) {
185
+ switch (_a.label) {
186
+ case 0:
187
+ _a.trys.push([0, 4, , 5]);
188
+ tenantId = req.user.tenantId;
189
+ return [4 /*yield*/, database_1.database.get('SELECT COUNT(*) as count FROM chat_sessions WHERE tenant_id = ?', [tenantId])];
190
+ case 1:
191
+ totalSessions = _a.sent();
192
+ return [4 /*yield*/, database_1.database.get('SELECT COUNT(*) as count FROM chat_sessions WHERE tenant_id = ? AND resolved = 1', [tenantId])];
193
+ case 2:
194
+ resolvedSessions = _a.sent();
195
+ return [4 /*yield*/, database_1.database.get('SELECT COUNT(*) as count FROM chat_sessions WHERE tenant_id = ? AND rating >= 4', [tenantId])];
196
+ case 3:
197
+ highRatingSessions = _a.sent();
198
+ total = totalSessions.count || 1;
199
+ positive = (resolvedSessions.count || 0) * 0.7 + (highRatingSessions.count || 0) * 0.3;
200
+ negative = total * 0.1;
201
+ neutral = total - positive - negative;
202
+ res.json({
203
+ sentiment: {
204
+ positive: Math.round((positive / total) * 100),
205
+ neutral: Math.round((neutral / total) * 100),
206
+ negative: Math.round((negative / total) * 100)
207
+ }
208
+ });
209
+ return [3 /*break*/, 5];
210
+ case 4:
211
+ error_5 = _a.sent();
212
+ console.error('Get sentiment error:', error_5);
213
+ res.status(500).json({ error: 'Internal server error' });
214
+ return [3 /*break*/, 5];
215
+ case 5: return [2 /*return*/];
216
+ }
217
+ });
218
+ }); });
219
+ exports.default = router;
src/routes/analytics.ts ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { database } from '../db/database';
3
+ import { AuthenticatedRequest } from '../middleware/auth';
4
+
5
+ const router = express.Router();
6
+
7
+ // Get dashboard metrics
8
+ router.get('/metrics', async (req: AuthenticatedRequest, res) => {
9
+ try {
10
+ const { tenantId } = req.user!;
11
+
12
+ // Get total conversations
13
+ const totalConversations = await database.get(
14
+ 'SELECT COUNT(*) as count FROM chat_sessions WHERE tenant_id = ?',
15
+ [tenantId]
16
+ );
17
+
18
+ // Get conversations this month
19
+ const thisMonthConversations = await database.get(
20
+ `SELECT COUNT(*) as count FROM chat_sessions
21
+ WHERE tenant_id = ? AND started_at >= date('now', 'start of month')`,
22
+ [tenantId]
23
+ );
24
+
25
+ // Get average rating
26
+ const avgRating = await database.get(
27
+ 'SELECT AVG(rating) as avg FROM chat_sessions WHERE tenant_id = ? AND rating IS NOT NULL',
28
+ [tenantId]
29
+ );
30
+
31
+ // Get resolution rate
32
+ const resolutionRate = await database.get(
33
+ `SELECT
34
+ COUNT(CASE WHEN resolved = 1 THEN 1 END) * 100.0 / COUNT(*) as rate
35
+ FROM chat_sessions WHERE tenant_id = ?`,
36
+ [tenantId]
37
+ );
38
+
39
+ // Get knowledge base count
40
+ const knowledgeBaseCount = await database.get(
41
+ 'SELECT COUNT(*) as count FROM knowledge_base WHERE tenant_id = ? AND status = "active"',
42
+ [tenantId]
43
+ );
44
+
45
+ res.json({
46
+ totalConversations: totalConversations.count || 0,
47
+ thisMonthConversations: thisMonthConversations.count || 0,
48
+ averageRating: parseFloat((avgRating.avg || 0).toFixed(1)),
49
+ resolutionRate: parseFloat((resolutionRate.rate || 0).toFixed(1)),
50
+ knowledgeBaseDocuments: knowledgeBaseCount.count || 0
51
+ });
52
+ } catch (error) {
53
+ console.error('Get metrics error:', error);
54
+ res.status(500).json({ error: 'Internal server error' });
55
+ }
56
+ });
57
+
58
+ // Get conversation trends
59
+ router.get('/conversations', async (req: AuthenticatedRequest, res) => {
60
+ try {
61
+ const { tenantId } = req.user!;
62
+ const { period = '7d' } = req.query;
63
+
64
+ let dateRange = '';
65
+ switch (period) {
66
+ case '1d':
67
+ dateRange = "date('now', '-1 day')";
68
+ break;
69
+ case '7d':
70
+ dateRange = "date('now', '-7 days')";
71
+ break;
72
+ case '30d':
73
+ dateRange = "date('now', '-30 days')";
74
+ break;
75
+ default:
76
+ dateRange = "date('now', '-7 days')";
77
+ }
78
+
79
+ const conversations = await database.query(
80
+ `SELECT
81
+ date(started_at) as date,
82
+ COUNT(*) as count,
83
+ AVG(CASE WHEN rating IS NOT NULL THEN rating END) as avg_rating,
84
+ COUNT(CASE WHEN resolved = 1 THEN 1 END) * 100.0 / COUNT(*) as resolution_rate
85
+ FROM chat_sessions
86
+ WHERE tenant_id = ? AND started_at >= ${dateRange}
87
+ GROUP BY date(started_at)
88
+ ORDER BY date`,
89
+ [tenantId]
90
+ );
91
+
92
+ res.json({ conversations });
93
+ } catch (error) {
94
+ console.error('Get conversations error:', error);
95
+ res.status(500).json({ error: 'Internal server error' });
96
+ }
97
+ });
98
+
99
+ // Get chat history with filters
100
+ router.get('/chat-history', async (req: AuthenticatedRequest, res) => {
101
+ try {
102
+ const { tenantId } = req.user!;
103
+ const { limit = 50, offset = 0, status, sentiment } = req.query;
104
+
105
+ let whereClause = 'WHERE cs.tenant_id = ?';
106
+ const params = [tenantId];
107
+
108
+ if (status === 'resolved') {
109
+ whereClause += ' AND cs.resolved = 1';
110
+ } else if (status === 'unresolved') {
111
+ whereClause += ' AND cs.resolved = 0';
112
+ }
113
+
114
+ const conversations = await database.query(
115
+ `SELECT
116
+ cs.id, cs.session_token, cs.started_at, cs.ended_at, cs.resolved, cs.rating, cs.feedback,
117
+ (SELECT message FROM chat_messages WHERE session_id = cs.id AND sender = 'user' ORDER BY timestamp LIMIT 1) as first_message,
118
+ (SELECT COUNT(*) FROM chat_messages WHERE session_id = cs.id) as message_count
119
+ FROM chat_sessions cs
120
+ ${whereClause}
121
+ ORDER BY cs.started_at DESC
122
+ LIMIT ? OFFSET ?`,
123
+ [...params, limit, offset]
124
+ );
125
+
126
+ res.json({ conversations });
127
+ } catch (error) {
128
+ console.error('Get chat history error:', error);
129
+ res.status(500).json({ error: 'Internal server error' });
130
+ }
131
+ });
132
+
133
+ // Get top questions/issues
134
+ router.get('/top-questions', async (req: AuthenticatedRequest, res) => {
135
+ try {
136
+ const { tenantId } = req.user!;
137
+
138
+ const topQuestions = await database.query(
139
+ `SELECT
140
+ cm.message,
141
+ COUNT(*) as frequency
142
+ FROM chat_messages cm
143
+ JOIN chat_sessions cs ON cm.session_id = cs.id
144
+ WHERE cs.tenant_id = ? AND cm.sender = 'user'
145
+ GROUP BY cm.message
146
+ HAVING frequency > 1
147
+ ORDER BY frequency DESC
148
+ LIMIT 10`,
149
+ [tenantId]
150
+ );
151
+
152
+ res.json({ topQuestions });
153
+ } catch (error) {
154
+ console.error('Get top questions error:', error);
155
+ res.status(500).json({ error: 'Internal server error' });
156
+ }
157
+ });
158
+
159
+ // Get sentiment analysis
160
+ router.get('/sentiment', async (req: AuthenticatedRequest, res) => {
161
+ try {
162
+ const { tenantId } = req.user!;
163
+
164
+ // For now, we'll simulate sentiment analysis
165
+ // In a real app, you'd analyze message content
166
+ const totalSessions = await database.get(
167
+ 'SELECT COUNT(*) as count FROM chat_sessions WHERE tenant_id = ?',
168
+ [tenantId]
169
+ );
170
+
171
+ const resolvedSessions = await database.get(
172
+ 'SELECT COUNT(*) as count FROM chat_sessions WHERE tenant_id = ? AND resolved = 1',
173
+ [tenantId]
174
+ );
175
+
176
+ const highRatingSessions = await database.get(
177
+ 'SELECT COUNT(*) as count FROM chat_sessions WHERE tenant_id = ? AND rating >= 4',
178
+ [tenantId]
179
+ );
180
+
181
+ const total = totalSessions.count || 1;
182
+ const positive = (resolvedSessions.count || 0) * 0.7 + (highRatingSessions.count || 0) * 0.3;
183
+ const negative = total * 0.1; // Assume 10% negative
184
+ const neutral = total - positive - negative;
185
+
186
+ res.json({
187
+ sentiment: {
188
+ positive: Math.round((positive / total) * 100),
189
+ neutral: Math.round((neutral / total) * 100),
190
+ negative: Math.round((negative / total) * 100)
191
+ }
192
+ });
193
+ } catch (error) {
194
+ console.error('Get sentiment error:', error);
195
+ res.status(500).json({ error: 'Internal server error' });
196
+ }
197
+ });
198
+
199
+ export default router;
src/routes/auth.js ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ var __rest = (this && this.__rest) || function (s, e) {
39
+ var t = {};
40
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
41
+ t[p] = s[p];
42
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
43
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
44
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
45
+ t[p[i]] = s[p[i]];
46
+ }
47
+ return t;
48
+ };
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ var express_1 = require("express");
51
+ var bcryptjs_1 = require("bcryptjs");
52
+ var jsonwebtoken_1 = require("jsonwebtoken");
53
+ var uuid_1 = require("uuid");
54
+ var google_auth_library_1 = require("google-auth-library");
55
+ var database_1 = require("../db/database");
56
+ var auth_1 = require("../middleware/auth");
57
+ var router = express_1.default.Router();
58
+ var client = new google_auth_library_1.OAuth2Client(process.env.GOOGLE_CLIENT_ID);
59
+ // Sign up with email/password
60
+ router.post('/signup', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
61
+ var _a, email, password, name_1, existingUser, saltRounds, passwordHash, verificationToken, result, tenantResult, token, user, error_1;
62
+ return __generator(this, function (_b) {
63
+ switch (_b.label) {
64
+ case 0:
65
+ _b.trys.push([0, 7, , 8]);
66
+ _a = req.body, email = _a.email, password = _a.password, name_1 = _a.name;
67
+ // Validation
68
+ if (!email || !password || !name_1) {
69
+ return [2 /*return*/, res.status(400).json({ error: 'Email, password, and name are required' })];
70
+ }
71
+ if (password.length < 6) {
72
+ return [2 /*return*/, res.status(400).json({ error: 'Password must be at least 6 characters' })];
73
+ }
74
+ return [4 /*yield*/, database_1.database.get('SELECT id FROM users WHERE email = ?', [email])];
75
+ case 1:
76
+ existingUser = _b.sent();
77
+ if (existingUser) {
78
+ return [2 /*return*/, res.status(400).json({ error: 'User already exists' })];
79
+ }
80
+ saltRounds = 10;
81
+ return [4 /*yield*/, bcryptjs_1.default.hash(password, saltRounds)];
82
+ case 2:
83
+ passwordHash = _b.sent();
84
+ verificationToken = (0, uuid_1.v4)();
85
+ return [4 /*yield*/, database_1.database.run('INSERT INTO users (email, name, password_hash, verification_token) VALUES (?, ?, ?, ?)', [email, name_1, passwordHash, verificationToken])];
86
+ case 3:
87
+ result = _b.sent();
88
+ return [4 /*yield*/, database_1.database.run('INSERT INTO tenants (name, subdomain) VALUES (?, ?)', ["".concat(name_1, "'s Workspace"), "tenant-".concat(result.lastID)])];
89
+ case 4:
90
+ tenantResult = _b.sent();
91
+ // Link user to tenant
92
+ return [4 /*yield*/, database_1.database.run('INSERT INTO user_tenants (user_id, tenant_id, role) VALUES (?, ?, ?)', [result.lastID, tenantResult.lastID, 'owner'])];
93
+ case 5:
94
+ // Link user to tenant
95
+ _b.sent();
96
+ token = jsonwebtoken_1.default.sign({ userId: result.lastID, tenantId: tenantResult.lastID }, process.env.JWT_SECRET, { expiresIn: '7d' });
97
+ return [4 /*yield*/, database_1.database.get('SELECT id, email, name, avatar, created_at FROM users WHERE id = ?', [result.lastID])];
98
+ case 6:
99
+ user = _b.sent();
100
+ res.status(201).json({
101
+ message: 'User created successfully',
102
+ token: token,
103
+ user: user,
104
+ tenant: { id: tenantResult.lastID, name: "".concat(name_1, "'s Workspace") }
105
+ });
106
+ return [3 /*break*/, 8];
107
+ case 7:
108
+ error_1 = _b.sent();
109
+ console.error('Signup error:', error_1);
110
+ res.status(500).json({ error: 'Internal server error' });
111
+ return [3 /*break*/, 8];
112
+ case 8: return [2 /*return*/];
113
+ }
114
+ });
115
+ }); });
116
+ // Sign in with email/password
117
+ router.post('/signin', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
118
+ var _a, email, password, user, isValidPassword, userTenant, token, password_hash, verification_token, safeUser, error_2;
119
+ return __generator(this, function (_b) {
120
+ switch (_b.label) {
121
+ case 0:
122
+ _b.trys.push([0, 4, , 5]);
123
+ _a = req.body, email = _a.email, password = _a.password;
124
+ if (!email || !password) {
125
+ return [2 /*return*/, res.status(400).json({ error: 'Email and password are required' })];
126
+ }
127
+ return [4 /*yield*/, database_1.database.get('SELECT * FROM users WHERE email = ?', [email])];
128
+ case 1:
129
+ user = _b.sent();
130
+ if (!user || !user.password_hash) {
131
+ return [2 /*return*/, res.status(401).json({ error: 'Invalid credentials' })];
132
+ }
133
+ return [4 /*yield*/, bcryptjs_1.default.compare(password, user.password_hash)];
134
+ case 2:
135
+ isValidPassword = _b.sent();
136
+ if (!isValidPassword) {
137
+ return [2 /*return*/, res.status(401).json({ error: 'Invalid credentials' })];
138
+ }
139
+ return [4 /*yield*/, database_1.database.get("SELECT t.id, t.name, t.subdomain, t.plan \n FROM tenants t \n JOIN user_tenants ut ON t.id = ut.tenant_id \n WHERE ut.user_id = ? AND ut.role = 'owner'", [user.id])];
140
+ case 3:
141
+ userTenant = _b.sent();
142
+ token = jsonwebtoken_1.default.sign({ userId: user.id, tenantId: userTenant === null || userTenant === void 0 ? void 0 : userTenant.id }, process.env.JWT_SECRET, { expiresIn: '7d' });
143
+ password_hash = user.password_hash, verification_token = user.verification_token, safeUser = __rest(user, ["password_hash", "verification_token"]);
144
+ res.json({
145
+ message: 'Signed in successfully',
146
+ token: token,
147
+ user: safeUser,
148
+ tenant: userTenant
149
+ });
150
+ return [3 /*break*/, 5];
151
+ case 4:
152
+ error_2 = _b.sent();
153
+ console.error('Signin error:', error_2);
154
+ res.status(500).json({ error: 'Internal server error' });
155
+ return [3 /*break*/, 5];
156
+ case 5: return [2 /*return*/];
157
+ }
158
+ });
159
+ }); });
160
+ // Google OAuth
161
+ router.post('/google', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
162
+ var credential, ticket, payload, googleId, email, name_2, picture, user, result, tenantResult, userTenant, token, password_hash, verification_token, safeUser, error_3;
163
+ return __generator(this, function (_a) {
164
+ switch (_a.label) {
165
+ case 0:
166
+ _a.trys.push([0, 11, , 12]);
167
+ credential = req.body.credential;
168
+ if (!credential) {
169
+ return [2 /*return*/, res.status(400).json({ error: 'Google credential is required' })];
170
+ }
171
+ return [4 /*yield*/, client.verifyIdToken({
172
+ idToken: credential,
173
+ audience: process.env.GOOGLE_CLIENT_ID,
174
+ })];
175
+ case 1:
176
+ ticket = _a.sent();
177
+ payload = ticket.getPayload();
178
+ if (!payload) {
179
+ return [2 /*return*/, res.status(400).json({ error: 'Invalid Google token' })];
180
+ }
181
+ googleId = payload.sub, email = payload.email, name_2 = payload.name, picture = payload.picture;
182
+ return [4 /*yield*/, database_1.database.get('SELECT * FROM users WHERE google_id = ? OR email = ?', [googleId, email])];
183
+ case 2:
184
+ user = _a.sent();
185
+ if (!user) return [3 /*break*/, 5];
186
+ if (!!user.google_id) return [3 /*break*/, 4];
187
+ return [4 /*yield*/, database_1.database.run('UPDATE users SET google_id = ?, avatar = ? WHERE id = ?', [googleId, picture, user.id])];
188
+ case 3:
189
+ _a.sent();
190
+ user.google_id = googleId;
191
+ user.avatar = picture;
192
+ _a.label = 4;
193
+ case 4: return [3 /*break*/, 9];
194
+ case 5: return [4 /*yield*/, database_1.database.run('INSERT INTO users (email, name, google_id, avatar, email_verified) VALUES (?, ?, ?, ?, ?)', [email, name_2, googleId, picture, true])];
195
+ case 6:
196
+ result = _a.sent();
197
+ return [4 /*yield*/, database_1.database.run('INSERT INTO tenants (name, subdomain) VALUES (?, ?)', ["".concat(name_2, "'s Workspace"), "tenant-".concat(result.lastID)])];
198
+ case 7:
199
+ tenantResult = _a.sent();
200
+ // Link user to tenant
201
+ return [4 /*yield*/, database_1.database.run('INSERT INTO user_tenants (user_id, tenant_id, role) VALUES (?, ?, ?)', [result.lastID, tenantResult.lastID, 'owner'])];
202
+ case 8:
203
+ // Link user to tenant
204
+ _a.sent();
205
+ user = {
206
+ id: result.lastID,
207
+ email: email,
208
+ name: name_2,
209
+ google_id: googleId,
210
+ avatar: picture,
211
+ email_verified: true
212
+ };
213
+ _a.label = 9;
214
+ case 9: return [4 /*yield*/, database_1.database.get("SELECT t.id, t.name, t.subdomain, t.plan \n FROM tenants t \n JOIN user_tenants ut ON t.id = ut.tenant_id \n WHERE ut.user_id = ? AND ut.role = 'owner'", [user.id])];
215
+ case 10:
216
+ userTenant = _a.sent();
217
+ token = jsonwebtoken_1.default.sign({ userId: user.id, tenantId: userTenant === null || userTenant === void 0 ? void 0 : userTenant.id }, process.env.JWT_SECRET, { expiresIn: '7d' });
218
+ password_hash = user.password_hash, verification_token = user.verification_token, safeUser = __rest(user, ["password_hash", "verification_token"]);
219
+ res.json({
220
+ message: 'Google authentication successful',
221
+ token: token,
222
+ user: safeUser,
223
+ tenant: userTenant
224
+ });
225
+ return [3 /*break*/, 12];
226
+ case 11:
227
+ error_3 = _a.sent();
228
+ console.error('Google auth error:', error_3);
229
+ res.status(500).json({ error: 'Google authentication failed' });
230
+ return [3 /*break*/, 12];
231
+ case 12: return [2 /*return*/];
232
+ }
233
+ });
234
+ }); });
235
+ // Get current user
236
+ router.get('/me', auth_1.authenticateToken, function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
237
+ var _a, userId, tenantId, user, tenant, error_4;
238
+ return __generator(this, function (_b) {
239
+ switch (_b.label) {
240
+ case 0:
241
+ _b.trys.push([0, 3, , 4]);
242
+ _a = req.user, userId = _a.userId, tenantId = _a.tenantId;
243
+ return [4 /*yield*/, database_1.database.get('SELECT id, email, name, avatar, email_verified, created_at FROM users WHERE id = ?', [userId])];
244
+ case 1:
245
+ user = _b.sent();
246
+ if (!user) {
247
+ return [2 /*return*/, res.status(404).json({ error: 'User not found' })];
248
+ }
249
+ return [4 /*yield*/, database_1.database.get('SELECT id, name, subdomain, plan, settings FROM tenants WHERE id = ?', [tenantId])];
250
+ case 2:
251
+ tenant = _b.sent();
252
+ res.json({
253
+ user: user,
254
+ tenant: tenant
255
+ });
256
+ return [3 /*break*/, 4];
257
+ case 3:
258
+ error_4 = _b.sent();
259
+ console.error('Get user error:', error_4);
260
+ res.status(500).json({ error: 'Internal server error' });
261
+ return [3 /*break*/, 4];
262
+ case 4: return [2 /*return*/];
263
+ }
264
+ });
265
+ }); });
266
+ // Update user profile
267
+ router.put('/profile', auth_1.authenticateToken, function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
268
+ var userId, _a, name_3, avatar, updates, params, user, error_5;
269
+ return __generator(this, function (_b) {
270
+ switch (_b.label) {
271
+ case 0:
272
+ _b.trys.push([0, 3, , 4]);
273
+ userId = req.user.userId;
274
+ _a = req.body, name_3 = _a.name, avatar = _a.avatar;
275
+ updates = [];
276
+ params = [];
277
+ if (name_3) {
278
+ updates.push('name = ?');
279
+ params.push(name_3);
280
+ }
281
+ if (avatar) {
282
+ updates.push('avatar = ?');
283
+ params.push(avatar);
284
+ }
285
+ if (updates.length === 0) {
286
+ return [2 /*return*/, res.status(400).json({ error: 'No valid fields to update' })];
287
+ }
288
+ updates.push('updated_at = CURRENT_TIMESTAMP');
289
+ params.push(userId);
290
+ return [4 /*yield*/, database_1.database.run("UPDATE users SET ".concat(updates.join(', '), " WHERE id = ?"), params)];
291
+ case 1:
292
+ _b.sent();
293
+ return [4 /*yield*/, database_1.database.get('SELECT id, email, name, avatar, email_verified, created_at FROM users WHERE id = ?', [userId])];
294
+ case 2:
295
+ user = _b.sent();
296
+ res.json({
297
+ message: 'Profile updated successfully',
298
+ user: user
299
+ });
300
+ return [3 /*break*/, 4];
301
+ case 3:
302
+ error_5 = _b.sent();
303
+ console.error('Update profile error:', error_5);
304
+ res.status(500).json({ error: 'Internal server error' });
305
+ return [3 /*break*/, 4];
306
+ case 4: return [2 /*return*/];
307
+ }
308
+ });
309
+ }); });
310
+ // Logout (client-side token removal, but we can blacklist if needed)
311
+ router.post('/logout', auth_1.authenticateToken, function (req, res) {
312
+ res.json({ message: 'Logged out successfully' });
313
+ });
314
+ exports.default = router;
src/routes/auth.ts ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import bcrypt from 'bcryptjs';
3
+ import jwt from 'jsonwebtoken';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+ import { OAuth2Client } from 'google-auth-library';
6
+ import { database } from '../db/database';
7
+ import { authenticateToken } from '../middleware/auth';
8
+
9
+ const router = express.Router();
10
+ const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
11
+
12
+ // Sign up with email/password
13
+ router.post('/signup', async (req, res) => {
14
+ try {
15
+ const { email, password, name } = req.body;
16
+
17
+ // Validation
18
+ if (!email || !password || !name) {
19
+ return res.status(400).json({ error: 'Email, password, and name are required' });
20
+ }
21
+
22
+ if (password.length < 6) {
23
+ return res.status(400).json({ error: 'Password must be at least 6 characters' });
24
+ }
25
+
26
+ // Check if user already exists
27
+ const existingUser = await database.get('SELECT id FROM users WHERE email = ?', [email]);
28
+ if (existingUser) {
29
+ return res.status(400).json({ error: 'User already exists' });
30
+ }
31
+
32
+ // Hash password
33
+ const saltRounds = 10;
34
+ const passwordHash = await bcrypt.hash(password, saltRounds);
35
+
36
+ // Generate verification token
37
+ const verificationToken = uuidv4();
38
+
39
+ // Create user
40
+ const result = await database.run(
41
+ 'INSERT INTO users (email, name, password_hash, verification_token) VALUES (?, ?, ?, ?)',
42
+ [email, name, passwordHash, verificationToken]
43
+ );
44
+
45
+ // Create default tenant for user
46
+ const tenantResult = await database.run(
47
+ 'INSERT INTO tenants (name, subdomain) VALUES (?, ?)',
48
+ [`${name}'s Workspace`, `tenant-${result.lastID}`]
49
+ );
50
+
51
+ // Link user to tenant
52
+ await database.run(
53
+ 'INSERT INTO user_tenants (user_id, tenant_id, role) VALUES (?, ?, ?)',
54
+ [result.lastID, tenantResult.lastID, 'owner']
55
+ );
56
+
57
+ // Generate JWT token
58
+ const token = jwt.sign(
59
+ { userId: result.lastID, tenantId: tenantResult.lastID },
60
+ process.env.JWT_SECRET!,
61
+ { expiresIn: '7d' }
62
+ );
63
+
64
+ // Get user data
65
+ const user = await database.get(
66
+ 'SELECT id, email, name, avatar, created_at FROM users WHERE id = ?',
67
+ [result.lastID]
68
+ );
69
+
70
+ res.status(201).json({
71
+ message: 'User created successfully',
72
+ token,
73
+ user,
74
+ tenant: { id: tenantResult.lastID, name: `${name}'s Workspace` }
75
+ });
76
+ } catch (error) {
77
+ console.error('Signup error:', error);
78
+ res.status(500).json({ error: 'Internal server error' });
79
+ }
80
+ });
81
+
82
+ // Sign in with email/password
83
+ router.post('/signin', async (req, res) => {
84
+ try {
85
+ const { email, password } = req.body;
86
+
87
+ if (!email || !password) {
88
+ return res.status(400).json({ error: 'Email and password are required' });
89
+ }
90
+
91
+ // Get user
92
+ const user = await database.get(
93
+ 'SELECT * FROM users WHERE email = ?',
94
+ [email]
95
+ );
96
+
97
+ if (!user || !user.password_hash) {
98
+ return res.status(401).json({ error: 'Invalid credentials' });
99
+ }
100
+
101
+ // Check password
102
+ const isValidPassword = await bcrypt.compare(password, user.password_hash);
103
+ if (!isValidPassword) {
104
+ return res.status(401).json({ error: 'Invalid credentials' });
105
+ }
106
+
107
+ // Get user's tenant
108
+ const userTenant = await database.get(
109
+ `SELECT t.id, t.name, t.subdomain, t.plan
110
+ FROM tenants t
111
+ JOIN user_tenants ut ON t.id = ut.tenant_id
112
+ WHERE ut.user_id = ? AND ut.role = 'owner'`,
113
+ [user.id]
114
+ );
115
+
116
+ // Generate JWT token
117
+ const token = jwt.sign(
118
+ { userId: user.id, tenantId: userTenant?.id },
119
+ process.env.JWT_SECRET!,
120
+ { expiresIn: '7d' }
121
+ );
122
+
123
+ // Remove sensitive data
124
+ const { password_hash, verification_token, ...safeUser } = user;
125
+
126
+ res.json({
127
+ message: 'Signed in successfully',
128
+ token,
129
+ user: safeUser,
130
+ tenant: userTenant
131
+ });
132
+ } catch (error) {
133
+ console.error('Signin error:', error);
134
+ res.status(500).json({ error: 'Internal server error' });
135
+ }
136
+ });
137
+
138
+ // Google OAuth
139
+ router.post('/google', async (req, res) => {
140
+ try {
141
+ const { credential } = req.body;
142
+
143
+ if (!credential) {
144
+ return res.status(400).json({ error: 'Google credential is required' });
145
+ }
146
+
147
+ // Verify Google token
148
+ const ticket = await client.verifyIdToken({
149
+ idToken: credential,
150
+ audience: process.env.GOOGLE_CLIENT_ID,
151
+ });
152
+
153
+ const payload = ticket.getPayload();
154
+ if (!payload) {
155
+ return res.status(400).json({ error: 'Invalid Google token' });
156
+ }
157
+
158
+ const { sub: googleId, email, name, picture } = payload;
159
+
160
+ // Check if user exists
161
+ let user = await database.get('SELECT * FROM users WHERE google_id = ? OR email = ?', [googleId, email]);
162
+
163
+ if (user) {
164
+ // Update Google ID if needed
165
+ if (!user.google_id) {
166
+ await database.run('UPDATE users SET google_id = ?, avatar = ? WHERE id = ?', [googleId, picture, user.id]);
167
+ user.google_id = googleId;
168
+ user.avatar = picture;
169
+ }
170
+ } else {
171
+ // Create new user
172
+ const result = await database.run(
173
+ 'INSERT INTO users (email, name, google_id, avatar, email_verified) VALUES (?, ?, ?, ?, ?)',
174
+ [email, name, googleId, picture, true]
175
+ );
176
+
177
+ // Create default tenant
178
+ const tenantResult = await database.run(
179
+ 'INSERT INTO tenants (name, subdomain) VALUES (?, ?)',
180
+ [`${name}'s Workspace`, `tenant-${result.lastID}`]
181
+ );
182
+
183
+ // Link user to tenant
184
+ await database.run(
185
+ 'INSERT INTO user_tenants (user_id, tenant_id, role) VALUES (?, ?, ?)',
186
+ [result.lastID, tenantResult.lastID, 'owner']
187
+ );
188
+
189
+ user = {
190
+ id: result.lastID,
191
+ email,
192
+ name,
193
+ google_id: googleId,
194
+ avatar: picture,
195
+ email_verified: true
196
+ };
197
+ }
198
+
199
+ // Get user's tenant
200
+ const userTenant = await database.get(
201
+ `SELECT t.id, t.name, t.subdomain, t.plan
202
+ FROM tenants t
203
+ JOIN user_tenants ut ON t.id = ut.tenant_id
204
+ WHERE ut.user_id = ? AND ut.role = 'owner'`,
205
+ [user.id]
206
+ );
207
+
208
+ // Generate JWT token
209
+ const token = jwt.sign(
210
+ { userId: user.id, tenantId: userTenant?.id },
211
+ process.env.JWT_SECRET!,
212
+ { expiresIn: '7d' }
213
+ );
214
+
215
+ // Remove sensitive data
216
+ const { password_hash, verification_token, ...safeUser } = user;
217
+
218
+ res.json({
219
+ message: 'Google authentication successful',
220
+ token,
221
+ user: safeUser,
222
+ tenant: userTenant
223
+ });
224
+ } catch (error) {
225
+ console.error('Google auth error:', error);
226
+ res.status(500).json({ error: 'Google authentication failed' });
227
+ }
228
+ });
229
+
230
+ // Get current user
231
+ router.get('/me', authenticateToken, async (req, res) => {
232
+ try {
233
+ const { userId, tenantId } = (req as any).user;
234
+
235
+ // Get user data
236
+ const user = await database.get(
237
+ 'SELECT id, email, name, avatar, email_verified, created_at FROM users WHERE id = ?',
238
+ [userId]
239
+ );
240
+
241
+ if (!user) {
242
+ return res.status(404).json({ error: 'User not found' });
243
+ }
244
+
245
+ // Get tenant data
246
+ const tenant = await database.get(
247
+ 'SELECT id, name, subdomain, plan, settings FROM tenants WHERE id = ?',
248
+ [tenantId]
249
+ );
250
+
251
+ res.json({
252
+ user,
253
+ tenant
254
+ });
255
+ } catch (error) {
256
+ console.error('Get user error:', error);
257
+ res.status(500).json({ error: 'Internal server error' });
258
+ }
259
+ });
260
+
261
+ // Update user profile
262
+ router.put('/profile', authenticateToken, async (req, res) => {
263
+ try {
264
+ const { userId } = (req as any).user;
265
+ const { name, avatar } = req.body;
266
+
267
+ const updates = [];
268
+ const params = [];
269
+
270
+ if (name) {
271
+ updates.push('name = ?');
272
+ params.push(name);
273
+ }
274
+
275
+ if (avatar) {
276
+ updates.push('avatar = ?');
277
+ params.push(avatar);
278
+ }
279
+
280
+ if (updates.length === 0) {
281
+ return res.status(400).json({ error: 'No valid fields to update' });
282
+ }
283
+
284
+ updates.push('updated_at = CURRENT_TIMESTAMP');
285
+ params.push(userId);
286
+
287
+ await database.run(
288
+ `UPDATE users SET ${updates.join(', ')} WHERE id = ?`,
289
+ params
290
+ );
291
+
292
+ // Get updated user
293
+ const user = await database.get(
294
+ 'SELECT id, email, name, avatar, email_verified, created_at FROM users WHERE id = ?',
295
+ [userId]
296
+ );
297
+
298
+ res.json({
299
+ message: 'Profile updated successfully',
300
+ user
301
+ });
302
+ } catch (error) {
303
+ console.error('Update profile error:', error);
304
+ res.status(500).json({ error: 'Internal server error' });
305
+ }
306
+ });
307
+
308
+ // Logout (client-side token removal, but we can blacklist if needed)
309
+ router.post('/logout', authenticateToken, (req, res) => {
310
+ res.json({ message: 'Logged out successfully' });
311
+ });
312
+
313
+ export default router;
src/routes/chat.js ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ var express_1 = require("express");
40
+ var uuid_1 = require("uuid");
41
+ var axios_1 = require("axios");
42
+ var database_1 = require("../db/database");
43
+ var router = express_1.default.Router();
44
+ // Create a new chat session
45
+ router.post('/sessions', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
46
+ var _a, tenantId, domain, userAgent, userIp, tenant, sessionToken, result, error_1;
47
+ return __generator(this, function (_b) {
48
+ switch (_b.label) {
49
+ case 0:
50
+ _b.trys.push([0, 4, , 5]);
51
+ _a = req.body, tenantId = _a.tenantId, domain = _a.domain, userAgent = _a.userAgent;
52
+ userIp = req.ip || req.connection.remoteAddress;
53
+ if (!tenantId) {
54
+ return [2 /*return*/, res.status(400).json({ error: 'Tenant ID is required' })];
55
+ }
56
+ return [4 /*yield*/, database_1.database.get('SELECT id FROM tenants WHERE id = ?', [tenantId])];
57
+ case 1:
58
+ tenant = _b.sent();
59
+ if (!tenant) {
60
+ return [2 /*return*/, res.status(404).json({ error: 'Tenant not found' })];
61
+ }
62
+ sessionToken = (0, uuid_1.v4)();
63
+ return [4 /*yield*/, database_1.database.run('INSERT INTO chat_sessions (tenant_id, domain, user_ip, user_agent, session_token) VALUES (?, ?, ?, ?, ?)', [tenantId, domain, userIp, userAgent, sessionToken])];
64
+ case 2:
65
+ result = _b.sent();
66
+ // Log analytics event
67
+ return [4 /*yield*/, database_1.database.run('INSERT INTO analytics_events (tenant_id, event_type, event_data) VALUES (?, ?, ?)', [tenantId, 'chat_session_started', JSON.stringify({ sessionId: result.lastID, domain: domain })])];
68
+ case 3:
69
+ // Log analytics event
70
+ _b.sent();
71
+ res.status(201).json({
72
+ sessionId: result.lastID,
73
+ sessionToken: sessionToken,
74
+ message: 'Chat session created successfully'
75
+ });
76
+ return [3 /*break*/, 5];
77
+ case 4:
78
+ error_1 = _b.sent();
79
+ console.error('Create session error:', error_1);
80
+ res.status(500).json({ error: 'Internal server error' });
81
+ return [3 /*break*/, 5];
82
+ case 5: return [2 /*return*/];
83
+ }
84
+ });
85
+ }); });
86
+ // Send a message and get AI response
87
+ router.post('/messages', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
88
+ var _a, sessionToken, message, tenantId, session, chatHistory, knowledgeBase, mcpResponse, aiResponse, mcpError_1, fallbackResponse, error_2;
89
+ return __generator(this, function (_b) {
90
+ switch (_b.label) {
91
+ case 0:
92
+ _b.trys.push([0, 12, , 13]);
93
+ _a = req.body, sessionToken = _a.sessionToken, message = _a.message, tenantId = _a.tenantId;
94
+ if (!sessionToken || !message || !tenantId) {
95
+ return [2 /*return*/, res.status(400).json({ error: 'Session token, message, and tenant ID are required' })];
96
+ }
97
+ return [4 /*yield*/, database_1.database.get('SELECT * FROM chat_sessions WHERE session_token = ? AND tenant_id = ?', [sessionToken, tenantId])];
98
+ case 1:
99
+ session = _b.sent();
100
+ if (!session) {
101
+ return [2 /*return*/, res.status(404).json({ error: 'Chat session not found' })];
102
+ }
103
+ // Save user message
104
+ return [4 /*yield*/, database_1.database.run('INSERT INTO chat_messages (session_id, sender, message) VALUES (?, ?, ?)', [session.id, 'user', message])];
105
+ case 2:
106
+ // Save user message
107
+ _b.sent();
108
+ return [4 /*yield*/, database_1.database.query('SELECT sender, message, timestamp FROM chat_messages WHERE session_id = ? ORDER BY timestamp ASC', [session.id])];
109
+ case 3:
110
+ chatHistory = _b.sent();
111
+ return [4 /*yield*/, database_1.database.query('SELECT name, type, source FROM knowledge_base WHERE tenant_id = ? AND status = "active"', [tenantId])];
112
+ case 4:
113
+ knowledgeBase = _b.sent();
114
+ _b.label = 5;
115
+ case 5:
116
+ _b.trys.push([5, 9, , 11]);
117
+ return [4 /*yield*/, axios_1.default.post("".concat(process.env.MCP_SERVER_URL, "/chat"), {
118
+ message: message,
119
+ history: chatHistory,
120
+ knowledge_base: knowledgeBase,
121
+ tenant_id: tenantId
122
+ }, {
123
+ headers: {
124
+ 'Authorization': "Bearer ".concat(process.env.MCP_AUTH_TOKEN),
125
+ 'Content-Type': 'application/json'
126
+ },
127
+ timeout: 30000 // 30 second timeout
128
+ })];
129
+ case 6:
130
+ mcpResponse = _b.sent();
131
+ aiResponse = mcpResponse.data.response || 'I apologize, but I encountered an issue processing your request.';
132
+ // Save AI response
133
+ return [4 /*yield*/, database_1.database.run('INSERT INTO chat_messages (session_id, sender, message, metadata) VALUES (?, ?, ?, ?)', [session.id, 'ai', aiResponse, JSON.stringify({
134
+ model: mcpResponse.data.model || 'gemini-1.5-flash',
135
+ confidence: mcpResponse.data.confidence || 0.8
136
+ })])];
137
+ case 7:
138
+ // Save AI response
139
+ _b.sent();
140
+ // Log analytics event
141
+ return [4 /*yield*/, database_1.database.run('INSERT INTO analytics_events (tenant_id, event_type, event_data) VALUES (?, ?, ?)', [tenantId, 'message_sent', JSON.stringify({
142
+ sessionId: session.id,
143
+ messageLength: message.length,
144
+ responseLength: aiResponse.length
145
+ })])];
146
+ case 8:
147
+ // Log analytics event
148
+ _b.sent();
149
+ res.json({
150
+ response: aiResponse,
151
+ messageId: session.id,
152
+ timestamp: new Date().toISOString()
153
+ });
154
+ return [3 /*break*/, 11];
155
+ case 9:
156
+ mcpError_1 = _b.sent();
157
+ console.error('MCP Server error:', mcpError_1);
158
+ fallbackResponse = "I'm sorry, but I'm experiencing technical difficulties at the moment. Please try again in a few minutes or contact support if the issue persists.";
159
+ return [4 /*yield*/, database_1.database.run('INSERT INTO chat_messages (session_id, sender, message, metadata) VALUES (?, ?, ?, ?)', [session.id, 'ai', fallbackResponse, JSON.stringify({ error: 'mcp_server_unavailable' })])];
160
+ case 10:
161
+ _b.sent();
162
+ res.json({
163
+ response: fallbackResponse,
164
+ messageId: session.id,
165
+ timestamp: new Date().toISOString(),
166
+ error: 'Service temporarily unavailable'
167
+ });
168
+ return [3 /*break*/, 11];
169
+ case 11: return [3 /*break*/, 13];
170
+ case 12:
171
+ error_2 = _b.sent();
172
+ console.error('Send message error:', error_2);
173
+ res.status(500).json({ error: 'Internal server error' });
174
+ return [3 /*break*/, 13];
175
+ case 13: return [2 /*return*/];
176
+ }
177
+ });
178
+ }); });
179
+ // Get chat history
180
+ router.get('/sessions/:sessionToken/history', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
181
+ var sessionToken, tenantId, session, messages, error_3;
182
+ return __generator(this, function (_a) {
183
+ switch (_a.label) {
184
+ case 0:
185
+ _a.trys.push([0, 3, , 4]);
186
+ sessionToken = req.params.sessionToken;
187
+ tenantId = req.query.tenantId;
188
+ if (!tenantId) {
189
+ return [2 /*return*/, res.status(400).json({ error: 'Tenant ID is required' })];
190
+ }
191
+ return [4 /*yield*/, database_1.database.get('SELECT * FROM chat_sessions WHERE session_token = ? AND tenant_id = ?', [sessionToken, tenantId])];
192
+ case 1:
193
+ session = _a.sent();
194
+ if (!session) {
195
+ return [2 /*return*/, res.status(404).json({ error: 'Chat session not found' })];
196
+ }
197
+ return [4 /*yield*/, database_1.database.query('SELECT sender, message, metadata, timestamp FROM chat_messages WHERE session_id = ? ORDER BY timestamp ASC', [session.id])];
198
+ case 2:
199
+ messages = _a.sent();
200
+ res.json({
201
+ sessionId: session.id,
202
+ messages: messages,
203
+ session: {
204
+ started_at: session.started_at,
205
+ resolved: session.resolved,
206
+ rating: session.rating
207
+ }
208
+ });
209
+ return [3 /*break*/, 4];
210
+ case 3:
211
+ error_3 = _a.sent();
212
+ console.error('Get history error:', error_3);
213
+ res.status(500).json({ error: 'Internal server error' });
214
+ return [3 /*break*/, 4];
215
+ case 4: return [2 /*return*/];
216
+ }
217
+ });
218
+ }); });
219
+ // Rate conversation
220
+ router.post('/sessions/:sessionToken/rate', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
221
+ var sessionToken, _a, rating, feedback, tenantId, error_4;
222
+ return __generator(this, function (_b) {
223
+ switch (_b.label) {
224
+ case 0:
225
+ _b.trys.push([0, 3, , 4]);
226
+ sessionToken = req.params.sessionToken;
227
+ _a = req.body, rating = _a.rating, feedback = _a.feedback, tenantId = _a.tenantId;
228
+ if (!tenantId || rating === undefined) {
229
+ return [2 /*return*/, res.status(400).json({ error: 'Tenant ID and rating are required' })];
230
+ }
231
+ if (rating < 1 || rating > 5) {
232
+ return [2 /*return*/, res.status(400).json({ error: 'Rating must be between 1 and 5' })];
233
+ }
234
+ // Update session
235
+ return [4 /*yield*/, database_1.database.run('UPDATE chat_sessions SET rating = ?, feedback = ?, resolved = TRUE WHERE session_token = ? AND tenant_id = ?', [rating, feedback, sessionToken, tenantId])];
236
+ case 1:
237
+ // Update session
238
+ _b.sent();
239
+ // Log analytics event
240
+ return [4 /*yield*/, database_1.database.run('INSERT INTO analytics_events (tenant_id, event_type, event_data) VALUES (?, ?, ?)', [tenantId, 'conversation_rated', JSON.stringify({
241
+ sessionToken: sessionToken,
242
+ rating: rating,
243
+ hasFeedback: !!feedback
244
+ })])];
245
+ case 2:
246
+ // Log analytics event
247
+ _b.sent();
248
+ res.json({ message: 'Rating submitted successfully' });
249
+ return [3 /*break*/, 4];
250
+ case 3:
251
+ error_4 = _b.sent();
252
+ console.error('Rate conversation error:', error_4);
253
+ res.status(500).json({ error: 'Internal server error' });
254
+ return [3 /*break*/, 4];
255
+ case 4: return [2 /*return*/];
256
+ }
257
+ });
258
+ }); });
259
+ // End chat session
260
+ router.post('/sessions/:sessionToken/end', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
261
+ var sessionToken, tenantId, error_5;
262
+ return __generator(this, function (_a) {
263
+ switch (_a.label) {
264
+ case 0:
265
+ _a.trys.push([0, 3, , 4]);
266
+ sessionToken = req.params.sessionToken;
267
+ tenantId = req.body.tenantId;
268
+ if (!tenantId) {
269
+ return [2 /*return*/, res.status(400).json({ error: 'Tenant ID is required' })];
270
+ }
271
+ // Update session
272
+ return [4 /*yield*/, database_1.database.run('UPDATE chat_sessions SET ended_at = CURRENT_TIMESTAMP WHERE session_token = ? AND tenant_id = ?', [sessionToken, tenantId])];
273
+ case 1:
274
+ // Update session
275
+ _a.sent();
276
+ // Log analytics event
277
+ return [4 /*yield*/, database_1.database.run('INSERT INTO analytics_events (tenant_id, event_type, event_data) VALUES (?, ?, ?)', [tenantId, 'chat_session_ended', JSON.stringify({ sessionToken: sessionToken })])];
278
+ case 2:
279
+ // Log analytics event
280
+ _a.sent();
281
+ res.json({ message: 'Chat session ended successfully' });
282
+ return [3 /*break*/, 4];
283
+ case 3:
284
+ error_5 = _a.sent();
285
+ console.error('End session error:', error_5);
286
+ res.status(500).json({ error: 'Internal server error' });
287
+ return [3 /*break*/, 4];
288
+ case 4: return [2 /*return*/];
289
+ }
290
+ });
291
+ }); });
292
+ exports.default = router;
src/routes/chat.ts ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import axios from 'axios';
4
+ import { database } from '../db/database';
5
+ import { AuthenticatedRequest, authenticateToken } from '../middleware/auth';
6
+
7
+ const router = express.Router();
8
+
9
+ // Create a new chat session (authenticated - for tenant dashboard)
10
+ router.post('/sessions', authenticateToken, async (req: AuthenticatedRequest, res) => {
11
+ try {
12
+ const { tenantId } = req.user!;
13
+ const { domain, userAgent } = req.body;
14
+ const userIp = req.ip || req.connection.remoteAddress;
15
+
16
+ // Verify tenant exists
17
+ const tenant = await database.get('SELECT id FROM tenants WHERE id = ?', [tenantId]);
18
+ if (!tenant) {
19
+ return res.status(404).json({ error: 'Tenant not found' });
20
+ }
21
+
22
+ // Generate session token
23
+ const sessionToken = uuidv4();
24
+
25
+ // Create chat session
26
+ const result = await database.run(
27
+ 'INSERT INTO chat_sessions (tenant_id, domain, user_ip, user_agent, session_token) VALUES (?, ?, ?, ?, ?)',
28
+ [tenantId, domain, userIp, userAgent, sessionToken]
29
+ );
30
+
31
+ // Log analytics event
32
+ await database.run(
33
+ 'INSERT INTO analytics_events (tenant_id, event_type, event_data) VALUES (?, ?, ?)',
34
+ [tenantId, 'chat_session_started', JSON.stringify({ sessionId: result.lastID, domain })]
35
+ );
36
+
37
+ res.status(201).json({
38
+ sessionId: result.lastID,
39
+ sessionToken,
40
+ tenantId,
41
+ message: 'Chat session created successfully'
42
+ });
43
+ } catch (error) {
44
+ console.error('Create session error:', error);
45
+ res.status(500).json({ error: 'Internal server error' });
46
+ }
47
+ });
48
+
49
+ // Create a new chat session (public - for widget)
50
+ router.post('/public/sessions', async (req, res) => {
51
+ try {
52
+ const { tenantId, domain, userAgent } = req.body;
53
+ const userIp = req.ip || req.connection.remoteAddress;
54
+
55
+ if (!tenantId) {
56
+ return res.status(400).json({ error: 'Tenant ID is required' });
57
+ }
58
+
59
+ // Verify tenant exists
60
+ const tenant = await database.get('SELECT id FROM tenants WHERE id = ?', [tenantId]);
61
+ if (!tenant) {
62
+ return res.status(404).json({ error: 'Tenant not found' });
63
+ }
64
+
65
+ // Generate session token
66
+ const sessionToken = uuidv4();
67
+
68
+ // Create chat session
69
+ const result = await database.run(
70
+ 'INSERT INTO chat_sessions (tenant_id, domain, user_ip, user_agent, session_token) VALUES (?, ?, ?, ?, ?)',
71
+ [tenantId, domain, userIp, userAgent, sessionToken]
72
+ );
73
+
74
+ // Log analytics event
75
+ await database.run(
76
+ 'INSERT INTO analytics_events (tenant_id, event_type, event_data) VALUES (?, ?, ?)',
77
+ [tenantId, 'chat_session_started', JSON.stringify({ sessionId: result.lastID, domain })]
78
+ );
79
+
80
+ res.status(201).json({
81
+ sessionId: result.lastID,
82
+ sessionToken,
83
+ message: 'Chat session created successfully'
84
+ });
85
+ } catch (error) {
86
+ console.error('Create session error:', error);
87
+ res.status(500).json({ error: 'Internal server error' });
88
+ }
89
+ });
90
+
91
+ // Send a message and get AI response
92
+ router.post('/messages', async (req, res) => {
93
+ try {
94
+ const { sessionToken, message, tenantId } = req.body;
95
+
96
+ if (!sessionToken || !message || !tenantId) {
97
+ return res.status(400).json({ error: 'Session token, message, and tenant ID are required' });
98
+ }
99
+
100
+ // Get chat session
101
+ const session = await database.get(
102
+ 'SELECT * FROM chat_sessions WHERE session_token = ? AND tenant_id = ?',
103
+ [sessionToken, tenantId]
104
+ );
105
+
106
+ if (!session) {
107
+ return res.status(404).json({ error: 'Chat session not found' });
108
+ }
109
+
110
+ // Save user message
111
+ await database.run(
112
+ 'INSERT INTO chat_messages (session_id, sender, message) VALUES (?, ?, ?)',
113
+ [session.id, 'user', message]
114
+ );
115
+
116
+ // Get chat history for context
117
+ const chatHistory = await database.query(
118
+ 'SELECT sender, message, timestamp FROM chat_messages WHERE session_id = ? ORDER BY timestamp ASC',
119
+ [session.id]
120
+ );
121
+
122
+ // Get tenant's knowledge base
123
+ const knowledgeBase = await database.query(
124
+ 'SELECT name, type, source FROM knowledge_base WHERE tenant_id = ? AND status = "active"',
125
+ [tenantId]
126
+ );
127
+
128
+ try {
129
+ // Call MCP server for AI response
130
+ const mcpResponse = await axios.post(`${process.env.MCP_SERVER_URL}/chat`, {
131
+ message,
132
+ history: chatHistory,
133
+ knowledge_base: knowledgeBase,
134
+ tenant_id: tenantId
135
+ }, {
136
+ headers: {
137
+ 'Authorization': `Bearer ${process.env.MCP_AUTH_TOKEN}`,
138
+ 'Content-Type': 'application/json'
139
+ },
140
+ timeout: 30000 // 30 second timeout
141
+ });
142
+
143
+ const aiResponse = mcpResponse.data.response || 'I apologize, but I encountered an issue processing your request.';
144
+
145
+ // Save AI response
146
+ await database.run(
147
+ 'INSERT INTO chat_messages (session_id, sender, message, metadata) VALUES (?, ?, ?, ?)',
148
+ [session.id, 'ai', aiResponse, JSON.stringify({
149
+ model: mcpResponse.data.model || 'gemini-1.5-flash',
150
+ confidence: mcpResponse.data.confidence || 0.8
151
+ })]
152
+ );
153
+
154
+ // Log analytics event
155
+ await database.run(
156
+ 'INSERT INTO analytics_events (tenant_id, event_type, event_data) VALUES (?, ?, ?)',
157
+ [tenantId, 'message_sent', JSON.stringify({
158
+ sessionId: session.id,
159
+ messageLength: message.length,
160
+ responseLength: aiResponse.length
161
+ })]
162
+ );
163
+
164
+ res.json({
165
+ response: aiResponse,
166
+ messageId: session.id,
167
+ timestamp: new Date().toISOString()
168
+ });
169
+
170
+ } catch (mcpError) {
171
+ console.error('MCP Server error:', mcpError);
172
+
173
+ // Fallback response if MCP server is down
174
+ const fallbackResponse = "I'm sorry, but I'm experiencing technical difficulties at the moment. Please try again in a few minutes or contact support if the issue persists.";
175
+
176
+ await database.run(
177
+ 'INSERT INTO chat_messages (session_id, sender, message, metadata) VALUES (?, ?, ?, ?)',
178
+ [session.id, 'ai', fallbackResponse, JSON.stringify({ error: 'mcp_server_unavailable' })]
179
+ );
180
+
181
+ res.json({
182
+ response: fallbackResponse,
183
+ messageId: session.id,
184
+ timestamp: new Date().toISOString(),
185
+ error: 'Service temporarily unavailable'
186
+ });
187
+ }
188
+
189
+ } catch (error) {
190
+ console.error('Send message error:', error);
191
+ res.status(500).json({ error: 'Internal server error' });
192
+ }
193
+ });
194
+
195
+ // Get chat history
196
+ router.get('/sessions/:sessionToken/history', async (req, res) => {
197
+ try {
198
+ const { sessionToken } = req.params;
199
+ const { tenantId } = req.query;
200
+
201
+ if (!tenantId) {
202
+ return res.status(400).json({ error: 'Tenant ID is required' });
203
+ }
204
+
205
+ // Get session
206
+ const session = await database.get(
207
+ 'SELECT * FROM chat_sessions WHERE session_token = ? AND tenant_id = ?',
208
+ [sessionToken, tenantId]
209
+ );
210
+
211
+ if (!session) {
212
+ return res.status(404).json({ error: 'Chat session not found' });
213
+ }
214
+
215
+ // Get messages
216
+ const messages = await database.query(
217
+ 'SELECT sender, message, metadata, timestamp FROM chat_messages WHERE session_id = ? ORDER BY timestamp ASC',
218
+ [session.id]
219
+ );
220
+
221
+ res.json({
222
+ sessionId: session.id,
223
+ messages,
224
+ session: {
225
+ started_at: session.started_at,
226
+ resolved: session.resolved,
227
+ rating: session.rating
228
+ }
229
+ });
230
+ } catch (error) {
231
+ console.error('Get history error:', error);
232
+ res.status(500).json({ error: 'Internal server error' });
233
+ }
234
+ });
235
+
236
+ // Rate conversation
237
+ router.post('/sessions/:sessionToken/rate', async (req, res) => {
238
+ try {
239
+ const { sessionToken } = req.params;
240
+ const { rating, feedback, tenantId } = req.body;
241
+
242
+ if (!tenantId || rating === undefined) {
243
+ return res.status(400).json({ error: 'Tenant ID and rating are required' });
244
+ }
245
+
246
+ if (rating < 1 || rating > 5) {
247
+ return res.status(400).json({ error: 'Rating must be between 1 and 5' });
248
+ }
249
+
250
+ // Update session
251
+ await database.run(
252
+ 'UPDATE chat_sessions SET rating = ?, feedback = ?, resolved = TRUE WHERE session_token = ? AND tenant_id = ?',
253
+ [rating, feedback, sessionToken, tenantId]
254
+ );
255
+
256
+ // Log analytics event
257
+ await database.run(
258
+ 'INSERT INTO analytics_events (tenant_id, event_type, event_data) VALUES (?, ?, ?)',
259
+ [tenantId, 'conversation_rated', JSON.stringify({
260
+ sessionToken,
261
+ rating,
262
+ hasFeedback: !!feedback
263
+ })]
264
+ );
265
+
266
+ res.json({ message: 'Rating submitted successfully' });
267
+ } catch (error) {
268
+ console.error('Rate conversation error:', error);
269
+ res.status(500).json({ error: 'Internal server error' });
270
+ }
271
+ });
272
+
273
+ // End chat session
274
+ router.post('/sessions/:sessionToken/end', async (req, res) => {
275
+ try {
276
+ const { sessionToken } = req.params;
277
+ const { tenantId } = req.body;
278
+
279
+ if (!tenantId) {
280
+ return res.status(400).json({ error: 'Tenant ID is required' });
281
+ }
282
+
283
+ // Update session
284
+ await database.run(
285
+ 'UPDATE chat_sessions SET ended_at = CURRENT_TIMESTAMP WHERE session_token = ? AND tenant_id = ?',
286
+ [sessionToken, tenantId]
287
+ );
288
+
289
+ // Log analytics event
290
+ await database.run(
291
+ 'INSERT INTO analytics_events (tenant_id, event_type, event_data) VALUES (?, ?, ?)',
292
+ [tenantId, 'chat_session_ended', JSON.stringify({ sessionToken })]
293
+ );
294
+
295
+ res.json({ message: 'Chat session ended successfully' });
296
+ } catch (error) {
297
+ console.error('End session error:', error);
298
+ res.status(500).json({ error: 'Internal server error' });
299
+ }
300
+ });
301
+
302
+ export default router;
src/routes/knowledge-base.js ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
14
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
15
+ return new (P || (P = Promise))(function (resolve, reject) {
16
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
17
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
18
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
19
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
20
+ });
21
+ };
22
+ var __generator = (this && this.__generator) || function (thisArg, body) {
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
24
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
+ function verb(n) { return function (v) { return step([n, v]); }; }
26
+ function step(op) {
27
+ if (f) throw new TypeError("Generator is already executing.");
28
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
29
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
30
+ if (y = 0, t) op = [op[0] & 2, t.value];
31
+ switch (op[0]) {
32
+ case 0: case 1: t = op; break;
33
+ case 4: _.label++; return { value: op[1], done: false };
34
+ case 5: _.label++; y = op[1]; op = [0]; continue;
35
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
36
+ default:
37
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
38
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
39
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
40
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
41
+ if (t[2]) _.ops.pop();
42
+ _.trys.pop(); continue;
43
+ }
44
+ op = body.call(thisArg, _);
45
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
46
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
+ }
48
+ };
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ var express_1 = require("express");
51
+ var multer_1 = require("multer");
52
+ var path_1 = require("path");
53
+ var fs_1 = require("fs");
54
+ var database_1 = require("../db/database");
55
+ var router = express_1.default.Router();
56
+ // Configure multer for file uploads
57
+ var storage = multer_1.default.diskStorage({
58
+ destination: function (req, file, cb) {
59
+ var uploadDir = process.env.UPLOAD_DIR || './uploads';
60
+ if (!fs_1.default.existsSync(uploadDir)) {
61
+ fs_1.default.mkdirSync(uploadDir, { recursive: true });
62
+ }
63
+ cb(null, uploadDir);
64
+ },
65
+ filename: function (req, file, cb) {
66
+ var uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
67
+ cb(null, file.fieldname + '-' + uniqueSuffix + path_1.default.extname(file.originalname));
68
+ }
69
+ });
70
+ var upload = (0, multer_1.default)({
71
+ storage: storage,
72
+ limits: {
73
+ fileSize: parseInt(process.env.MAX_FILE_SIZE || '10485760') // 10MB default
74
+ },
75
+ fileFilter: function (req, file, cb) {
76
+ var allowedTypes = ['.pdf', '.docx', '.txt', '.md'];
77
+ var ext = path_1.default.extname(file.originalname).toLowerCase();
78
+ if (allowedTypes.includes(ext)) {
79
+ cb(null, true);
80
+ }
81
+ else {
82
+ cb(new Error('Invalid file type. Only PDF, DOCX, TXT, and MD files are allowed.'));
83
+ }
84
+ }
85
+ });
86
+ // Get knowledge base documents
87
+ router.get('/', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
88
+ var tenantId, documents, error_1;
89
+ return __generator(this, function (_a) {
90
+ switch (_a.label) {
91
+ case 0:
92
+ _a.trys.push([0, 2, , 3]);
93
+ tenantId = req.user.tenantId;
94
+ return [4 /*yield*/, database_1.database.query('SELECT id, name, type, source, status, size, created_at, updated_at FROM knowledge_base WHERE tenant_id = ? ORDER BY created_at DESC', [tenantId])];
95
+ case 1:
96
+ documents = _a.sent();
97
+ res.json({ documents: documents });
98
+ return [3 /*break*/, 3];
99
+ case 2:
100
+ error_1 = _a.sent();
101
+ console.error('Get knowledge base error:', error_1);
102
+ res.status(500).json({ error: 'Internal server error' });
103
+ return [3 /*break*/, 3];
104
+ case 3: return [2 /*return*/];
105
+ }
106
+ });
107
+ }); });
108
+ // Upload document
109
+ router.post('/upload', upload.single('document'), function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
110
+ var tenantId, _a, originalname, filename, size, mimetype, fileType, result, error_2;
111
+ return __generator(this, function (_b) {
112
+ switch (_b.label) {
113
+ case 0:
114
+ _b.trys.push([0, 3, , 4]);
115
+ tenantId = req.user.tenantId;
116
+ if (!req.file) {
117
+ return [2 /*return*/, res.status(400).json({ error: 'No file uploaded' })];
118
+ }
119
+ _a = req.file, originalname = _a.originalname, filename = _a.filename, size = _a.size, mimetype = _a.mimetype;
120
+ fileType = path_1.default.extname(originalname).toLowerCase().substring(1);
121
+ return [4 /*yield*/, database_1.database.run('INSERT INTO knowledge_base (tenant_id, name, type, source, status, size, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)', [
122
+ tenantId,
123
+ originalname,
124
+ fileType,
125
+ filename,
126
+ 'processing',
127
+ size,
128
+ JSON.stringify({ mimetype: mimetype, uploadedAt: new Date().toISOString() })
129
+ ])];
130
+ case 1:
131
+ result = _b.sent();
132
+ // Log analytics event
133
+ return [4 /*yield*/, database_1.database.run('INSERT INTO analytics_events (tenant_id, event_type, event_data) VALUES (?, ?, ?)', [tenantId, 'document_uploaded', JSON.stringify({
134
+ documentId: result.lastID,
135
+ type: fileType,
136
+ size: size
137
+ })])];
138
+ case 2:
139
+ // Log analytics event
140
+ _b.sent();
141
+ res.status(201).json({
142
+ id: result.lastID,
143
+ name: originalname,
144
+ type: fileType,
145
+ status: 'processing',
146
+ message: 'Document uploaded successfully'
147
+ });
148
+ return [3 /*break*/, 4];
149
+ case 3:
150
+ error_2 = _b.sent();
151
+ console.error('Upload document error:', error_2);
152
+ res.status(500).json({ error: 'Internal server error' });
153
+ return [3 /*break*/, 4];
154
+ case 4: return [2 /*return*/];
155
+ }
156
+ });
157
+ }); });
158
+ // Add website URL
159
+ router.post('/url', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
160
+ var tenantId, _a, url, name_1, displayName, result, error_3;
161
+ return __generator(this, function (_b) {
162
+ switch (_b.label) {
163
+ case 0:
164
+ _b.trys.push([0, 3, , 4]);
165
+ tenantId = req.user.tenantId;
166
+ _a = req.body, url = _a.url, name_1 = _a.name;
167
+ if (!url) {
168
+ return [2 /*return*/, res.status(400).json({ error: 'URL is required' })];
169
+ }
170
+ // Basic URL validation
171
+ try {
172
+ new URL(url);
173
+ }
174
+ catch (_c) {
175
+ return [2 /*return*/, res.status(400).json({ error: 'Invalid URL format' })];
176
+ }
177
+ displayName = name_1 || new URL(url).hostname;
178
+ return [4 /*yield*/, database_1.database.run('INSERT INTO knowledge_base (tenant_id, name, type, source, status, metadata) VALUES (?, ?, ?, ?, ?, ?)', [
179
+ tenantId,
180
+ displayName,
181
+ 'website',
182
+ url,
183
+ 'processing',
184
+ JSON.stringify({ addedAt: new Date().toISOString() })
185
+ ])];
186
+ case 1:
187
+ result = _b.sent();
188
+ // Log analytics event
189
+ return [4 /*yield*/, database_1.database.run('INSERT INTO analytics_events (tenant_id, event_type, event_data) VALUES (?, ?, ?)', [tenantId, 'website_added', JSON.stringify({
190
+ documentId: result.lastID,
191
+ url: url
192
+ })])];
193
+ case 2:
194
+ // Log analytics event
195
+ _b.sent();
196
+ res.status(201).json({
197
+ id: result.lastID,
198
+ name: displayName,
199
+ type: 'website',
200
+ source: url,
201
+ status: 'processing',
202
+ message: 'Website added successfully'
203
+ });
204
+ return [3 /*break*/, 4];
205
+ case 3:
206
+ error_3 = _b.sent();
207
+ console.error('Add URL error:', error_3);
208
+ res.status(500).json({ error: 'Internal server error' });
209
+ return [3 /*break*/, 4];
210
+ case 4: return [2 /*return*/];
211
+ }
212
+ });
213
+ }); });
214
+ // Delete document
215
+ router.delete('/:documentId', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
216
+ var tenantId, documentId, document_1, filePath, error_4;
217
+ return __generator(this, function (_a) {
218
+ switch (_a.label) {
219
+ case 0:
220
+ _a.trys.push([0, 4, , 5]);
221
+ tenantId = req.user.tenantId;
222
+ documentId = req.params.documentId;
223
+ return [4 /*yield*/, database_1.database.get('SELECT * FROM knowledge_base WHERE id = ? AND tenant_id = ?', [documentId, tenantId])];
224
+ case 1:
225
+ document_1 = _a.sent();
226
+ if (!document_1) {
227
+ return [2 /*return*/, res.status(404).json({ error: 'Document not found' })];
228
+ }
229
+ // Delete file if it's a uploaded document
230
+ if (document_1.type !== 'website') {
231
+ filePath = path_1.default.join(process.env.UPLOAD_DIR || './uploads', document_1.source);
232
+ if (fs_1.default.existsSync(filePath)) {
233
+ fs_1.default.unlinkSync(filePath);
234
+ }
235
+ }
236
+ // Delete from database
237
+ return [4 /*yield*/, database_1.database.run('DELETE FROM knowledge_base WHERE id = ? AND tenant_id = ?', [documentId, tenantId])];
238
+ case 2:
239
+ // Delete from database
240
+ _a.sent();
241
+ // Log analytics event
242
+ return [4 /*yield*/, database_1.database.run('INSERT INTO analytics_events (tenant_id, event_type, event_data) VALUES (?, ?, ?)', [tenantId, 'document_deleted', JSON.stringify({
243
+ documentId: documentId,
244
+ name: document_1.name,
245
+ type: document_1.type
246
+ })])];
247
+ case 3:
248
+ // Log analytics event
249
+ _a.sent();
250
+ res.json({ message: 'Document deleted successfully' });
251
+ return [3 /*break*/, 5];
252
+ case 4:
253
+ error_4 = _a.sent();
254
+ console.error('Delete document error:', error_4);
255
+ res.status(500).json({ error: 'Internal server error' });
256
+ return [3 /*break*/, 5];
257
+ case 5: return [2 /*return*/];
258
+ }
259
+ });
260
+ }); });
261
+ // Update document status (for processing updates)
262
+ router.put('/:documentId/status', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
263
+ var tenantId, documentId, status_1, error_5;
264
+ return __generator(this, function (_a) {
265
+ switch (_a.label) {
266
+ case 0:
267
+ _a.trys.push([0, 2, , 3]);
268
+ tenantId = req.user.tenantId;
269
+ documentId = req.params.documentId;
270
+ status_1 = req.body.status;
271
+ if (!['processing', 'active', 'error'].includes(status_1)) {
272
+ return [2 /*return*/, res.status(400).json({ error: 'Invalid status' })];
273
+ }
274
+ return [4 /*yield*/, database_1.database.run('UPDATE knowledge_base SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND tenant_id = ?', [status_1, documentId, tenantId])];
275
+ case 1:
276
+ _a.sent();
277
+ res.json({ message: 'Document status updated successfully' });
278
+ return [3 /*break*/, 3];
279
+ case 2:
280
+ error_5 = _a.sent();
281
+ console.error('Update document status error:', error_5);
282
+ res.status(500).json({ error: 'Internal server error' });
283
+ return [3 /*break*/, 3];
284
+ case 3: return [2 /*return*/];
285
+ }
286
+ });
287
+ }); });
288
+ // Get document by ID
289
+ router.get('/:documentId', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
290
+ var tenantId, documentId, document_2, error_6;
291
+ return __generator(this, function (_a) {
292
+ switch (_a.label) {
293
+ case 0:
294
+ _a.trys.push([0, 2, , 3]);
295
+ tenantId = req.user.tenantId;
296
+ documentId = req.params.documentId;
297
+ return [4 /*yield*/, database_1.database.get('SELECT * FROM knowledge_base WHERE id = ? AND tenant_id = ?', [documentId, tenantId])];
298
+ case 1:
299
+ document_2 = _a.sent();
300
+ if (!document_2) {
301
+ return [2 /*return*/, res.status(404).json({ error: 'Document not found' })];
302
+ }
303
+ res.json(__assign(__assign({}, document_2), { metadata: JSON.parse(document_2.metadata || '{}') }));
304
+ return [3 /*break*/, 3];
305
+ case 2:
306
+ error_6 = _a.sent();
307
+ console.error('Get document error:', error_6);
308
+ res.status(500).json({ error: 'Internal server error' });
309
+ return [3 /*break*/, 3];
310
+ case 3: return [2 /*return*/];
311
+ }
312
+ });
313
+ }); });
314
+ exports.default = router;
src/routes/knowledge-base.ts ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import multer from 'multer';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { database } from '../db/database';
6
+ import { AuthenticatedRequest } from '../middleware/auth';
7
+
8
+ const router = express.Router();
9
+
10
+ // Configure multer for file uploads
11
+ const storage = multer.diskStorage({
12
+ destination: (req, file, cb) => {
13
+ const uploadDir = process.env.UPLOAD_DIR || './uploads';
14
+ if (!fs.existsSync(uploadDir)) {
15
+ fs.mkdirSync(uploadDir, { recursive: true });
16
+ }
17
+ cb(null, uploadDir);
18
+ },
19
+ filename: (req, file, cb) => {
20
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
21
+ cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
22
+ }
23
+ });
24
+
25
+ const upload = multer({
26
+ storage,
27
+ limits: {
28
+ fileSize: parseInt(process.env.MAX_FILE_SIZE || '10485760') // 10MB default
29
+ },
30
+ fileFilter: (req, file, cb) => {
31
+ const allowedTypes = ['.pdf', '.docx', '.txt', '.md'];
32
+ const ext = path.extname(file.originalname).toLowerCase();
33
+ if (allowedTypes.includes(ext)) {
34
+ cb(null, true);
35
+ } else {
36
+ cb(new Error('Invalid file type. Only PDF, DOCX, TXT, and MD files are allowed.'));
37
+ }
38
+ }
39
+ });
40
+
41
+ // Get knowledge base documents
42
+ router.get('/', async (req: AuthenticatedRequest, res) => {
43
+ try {
44
+ const { tenantId } = req.user!;
45
+
46
+ const documents = await database.query(
47
+ 'SELECT id, name, type, source, status, size, created_at, updated_at FROM knowledge_base WHERE tenant_id = ? ORDER BY created_at DESC',
48
+ [tenantId]
49
+ );
50
+
51
+ res.json({ documents });
52
+ } catch (error) {
53
+ console.error('Get knowledge base error:', error);
54
+ res.status(500).json({ error: 'Internal server error' });
55
+ }
56
+ });
57
+
58
+ // Upload document
59
+ router.post('/upload', upload.single('document'), async (req: AuthenticatedRequest, res) => {
60
+ try {
61
+ const { tenantId } = req.user!;
62
+
63
+ if (!req.file) {
64
+ return res.status(400).json({ error: 'No file uploaded' });
65
+ }
66
+
67
+ const { originalname, filename, size, mimetype } = req.file;
68
+ const fileType = path.extname(originalname).toLowerCase().substring(1);
69
+
70
+ // Save to database
71
+ const result = await database.run(
72
+ 'INSERT INTO knowledge_base (tenant_id, name, type, source, status, size, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)',
73
+ [
74
+ tenantId,
75
+ originalname,
76
+ fileType,
77
+ filename,
78
+ 'processing',
79
+ size,
80
+ JSON.stringify({ mimetype, uploadedAt: new Date().toISOString() })
81
+ ]
82
+ );
83
+
84
+ // Log analytics event
85
+ await database.run(
86
+ 'INSERT INTO analytics_events (tenant_id, event_type, event_data) VALUES (?, ?, ?)',
87
+ [tenantId, 'document_uploaded', JSON.stringify({
88
+ documentId: result.lastID,
89
+ type: fileType,
90
+ size
91
+ })]
92
+ );
93
+
94
+ res.status(201).json({
95
+ id: result.lastID,
96
+ name: originalname,
97
+ type: fileType,
98
+ status: 'processing',
99
+ message: 'Document uploaded successfully'
100
+ });
101
+ } catch (error) {
102
+ console.error('Upload document error:', error);
103
+ res.status(500).json({ error: 'Internal server error' });
104
+ }
105
+ });
106
+
107
+ // Add website URL
108
+ router.post('/url', async (req: AuthenticatedRequest, res) => {
109
+ try {
110
+ const { tenantId } = req.user!;
111
+ const { url, name } = req.body;
112
+
113
+ if (!url) {
114
+ return res.status(400).json({ error: 'URL is required' });
115
+ }
116
+
117
+ // Basic URL validation
118
+ try {
119
+ new URL(url);
120
+ } catch {
121
+ return res.status(400).json({ error: 'Invalid URL format' });
122
+ }
123
+
124
+ const displayName = name || new URL(url).hostname;
125
+
126
+ // Save to database
127
+ const result = await database.run(
128
+ 'INSERT INTO knowledge_base (tenant_id, name, type, source, status, metadata) VALUES (?, ?, ?, ?, ?, ?)',
129
+ [
130
+ tenantId,
131
+ displayName,
132
+ 'website',
133
+ url,
134
+ 'processing',
135
+ JSON.stringify({ addedAt: new Date().toISOString() })
136
+ ]
137
+ );
138
+
139
+ // Log analytics event
140
+ await database.run(
141
+ 'INSERT INTO analytics_events (tenant_id, event_type, event_data) VALUES (?, ?, ?)',
142
+ [tenantId, 'website_added', JSON.stringify({
143
+ documentId: result.lastID,
144
+ url
145
+ })]
146
+ );
147
+
148
+ res.status(201).json({
149
+ id: result.lastID,
150
+ name: displayName,
151
+ type: 'website',
152
+ source: url,
153
+ status: 'processing',
154
+ message: 'Website added successfully'
155
+ });
156
+ } catch (error) {
157
+ console.error('Add URL error:', error);
158
+ res.status(500).json({ error: 'Internal server error' });
159
+ }
160
+ });
161
+
162
+ // Delete document
163
+ router.delete('/:documentId', async (req: AuthenticatedRequest, res) => {
164
+ try {
165
+ const { tenantId } = req.user!;
166
+ const { documentId } = req.params;
167
+
168
+ // Get document info
169
+ const document = await database.get(
170
+ 'SELECT * FROM knowledge_base WHERE id = ? AND tenant_id = ?',
171
+ [documentId, tenantId]
172
+ );
173
+
174
+ if (!document) {
175
+ return res.status(404).json({ error: 'Document not found' });
176
+ }
177
+
178
+ // Delete file if it's a uploaded document
179
+ if (document.type !== 'website') {
180
+ const filePath = path.join(process.env.UPLOAD_DIR || './uploads', document.source);
181
+ if (fs.existsSync(filePath)) {
182
+ fs.unlinkSync(filePath);
183
+ }
184
+ }
185
+
186
+ // Delete from database
187
+ await database.run(
188
+ 'DELETE FROM knowledge_base WHERE id = ? AND tenant_id = ?',
189
+ [documentId, tenantId]
190
+ );
191
+
192
+ // Log analytics event
193
+ await database.run(
194
+ 'INSERT INTO analytics_events (tenant_id, event_type, event_data) VALUES (?, ?, ?)',
195
+ [tenantId, 'document_deleted', JSON.stringify({
196
+ documentId,
197
+ name: document.name,
198
+ type: document.type
199
+ })]
200
+ );
201
+
202
+ res.json({ message: 'Document deleted successfully' });
203
+ } catch (error) {
204
+ console.error('Delete document error:', error);
205
+ res.status(500).json({ error: 'Internal server error' });
206
+ }
207
+ });
208
+
209
+ // Update document status (for processing updates)
210
+ router.put('/:documentId/status', async (req: AuthenticatedRequest, res) => {
211
+ try {
212
+ const { tenantId } = req.user!;
213
+ const { documentId } = req.params;
214
+ const { status } = req.body;
215
+
216
+ if (!['processing', 'active', 'error'].includes(status)) {
217
+ return res.status(400).json({ error: 'Invalid status' });
218
+ }
219
+
220
+ await database.run(
221
+ 'UPDATE knowledge_base SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND tenant_id = ?',
222
+ [status, documentId, tenantId]
223
+ );
224
+
225
+ res.json({ message: 'Document status updated successfully' });
226
+ } catch (error) {
227
+ console.error('Update document status error:', error);
228
+ res.status(500).json({ error: 'Internal server error' });
229
+ }
230
+ });
231
+
232
+ // Get document by ID
233
+ router.get('/:documentId', async (req: AuthenticatedRequest, res) => {
234
+ try {
235
+ const { tenantId } = req.user!;
236
+ const { documentId } = req.params;
237
+
238
+ const document = await database.get(
239
+ 'SELECT * FROM knowledge_base WHERE id = ? AND tenant_id = ?',
240
+ [documentId, tenantId]
241
+ );
242
+
243
+ if (!document) {
244
+ return res.status(404).json({ error: 'Document not found' });
245
+ }
246
+
247
+ res.json({
248
+ ...document,
249
+ metadata: JSON.parse(document.metadata || '{}')
250
+ });
251
+ } catch (error) {
252
+ console.error('Get document error:', error);
253
+ res.status(500).json({ error: 'Internal server error' });
254
+ }
255
+ });
256
+
257
+ export default router;
src/routes/tenants.js ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
14
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
15
+ return new (P || (P = Promise))(function (resolve, reject) {
16
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
17
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
18
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
19
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
20
+ });
21
+ };
22
+ var __generator = (this && this.__generator) || function (thisArg, body) {
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
24
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
+ function verb(n) { return function (v) { return step([n, v]); }; }
26
+ function step(op) {
27
+ if (f) throw new TypeError("Generator is already executing.");
28
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
29
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
30
+ if (y = 0, t) op = [op[0] & 2, t.value];
31
+ switch (op[0]) {
32
+ case 0: case 1: t = op; break;
33
+ case 4: _.label++; return { value: op[1], done: false };
34
+ case 5: _.label++; y = op[1]; op = [0]; continue;
35
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
36
+ default:
37
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
38
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
39
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
40
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
41
+ if (t[2]) _.ops.pop();
42
+ _.trys.pop(); continue;
43
+ }
44
+ op = body.call(thisArg, _);
45
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
46
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
+ }
48
+ };
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ var express_1 = require("express");
51
+ var database_1 = require("../db/database");
52
+ var router = express_1.default.Router();
53
+ // Get current tenant info
54
+ router.get('/me', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
55
+ var tenantId, tenant, domains, error_1;
56
+ return __generator(this, function (_a) {
57
+ switch (_a.label) {
58
+ case 0:
59
+ _a.trys.push([0, 3, , 4]);
60
+ tenantId = req.user.tenantId;
61
+ return [4 /*yield*/, database_1.database.get('SELECT id, name, subdomain, plan, settings, created_at FROM tenants WHERE id = ?', [tenantId])];
62
+ case 1:
63
+ tenant = _a.sent();
64
+ if (!tenant) {
65
+ return [2 /*return*/, res.status(404).json({ error: 'Tenant not found' })];
66
+ }
67
+ return [4 /*yield*/, database_1.database.query('SELECT id, domain, verified, created_at FROM domains WHERE tenant_id = ?', [tenantId])];
68
+ case 2:
69
+ domains = _a.sent();
70
+ res.json(__assign(__assign({}, tenant), { settings: JSON.parse(tenant.settings || '{}'), domains: domains }));
71
+ return [3 /*break*/, 4];
72
+ case 3:
73
+ error_1 = _a.sent();
74
+ console.error('Get tenant error:', error_1);
75
+ res.status(500).json({ error: 'Internal server error' });
76
+ return [3 /*break*/, 4];
77
+ case 4: return [2 /*return*/];
78
+ }
79
+ });
80
+ }); });
81
+ // Update tenant settings
82
+ router.put('/me', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
83
+ var tenantId, _a, name_1, settings, updates, params, error_2;
84
+ return __generator(this, function (_b) {
85
+ switch (_b.label) {
86
+ case 0:
87
+ _b.trys.push([0, 2, , 3]);
88
+ tenantId = req.user.tenantId;
89
+ _a = req.body, name_1 = _a.name, settings = _a.settings;
90
+ updates = [];
91
+ params = [];
92
+ if (name_1) {
93
+ updates.push('name = ?');
94
+ params.push(name_1);
95
+ }
96
+ if (settings) {
97
+ updates.push('settings = ?');
98
+ params.push(JSON.stringify(settings));
99
+ }
100
+ if (updates.length === 0) {
101
+ return [2 /*return*/, res.status(400).json({ error: 'No valid fields to update' })];
102
+ }
103
+ updates.push('updated_at = CURRENT_TIMESTAMP');
104
+ params.push(tenantId);
105
+ return [4 /*yield*/, database_1.database.run("UPDATE tenants SET ".concat(updates.join(', '), " WHERE id = ?"), params)];
106
+ case 1:
107
+ _b.sent();
108
+ res.json({ message: 'Tenant updated successfully' });
109
+ return [3 /*break*/, 3];
110
+ case 2:
111
+ error_2 = _b.sent();
112
+ console.error('Update tenant error:', error_2);
113
+ res.status(500).json({ error: 'Internal server error' });
114
+ return [3 /*break*/, 3];
115
+ case 3: return [2 /*return*/];
116
+ }
117
+ });
118
+ }); });
119
+ // Add domain
120
+ router.post('/domains', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
121
+ var tenantId, domain, existingDomain, result, error_3;
122
+ return __generator(this, function (_a) {
123
+ switch (_a.label) {
124
+ case 0:
125
+ _a.trys.push([0, 3, , 4]);
126
+ tenantId = req.user.tenantId;
127
+ domain = req.body.domain;
128
+ if (!domain) {
129
+ return [2 /*return*/, res.status(400).json({ error: 'Domain is required' })];
130
+ }
131
+ return [4 /*yield*/, database_1.database.get('SELECT id FROM domains WHERE domain = ?', [domain])];
132
+ case 1:
133
+ existingDomain = _a.sent();
134
+ if (existingDomain) {
135
+ return [2 /*return*/, res.status(400).json({ error: 'Domain already exists' })];
136
+ }
137
+ return [4 /*yield*/, database_1.database.run('INSERT INTO domains (tenant_id, domain) VALUES (?, ?)', [tenantId, domain])];
138
+ case 2:
139
+ result = _a.sent();
140
+ res.status(201).json({
141
+ id: result.lastID,
142
+ domain: domain,
143
+ verified: false,
144
+ message: 'Domain added successfully'
145
+ });
146
+ return [3 /*break*/, 4];
147
+ case 3:
148
+ error_3 = _a.sent();
149
+ console.error('Add domain error:', error_3);
150
+ res.status(500).json({ error: 'Internal server error' });
151
+ return [3 /*break*/, 4];
152
+ case 4: return [2 /*return*/];
153
+ }
154
+ });
155
+ }); });
156
+ // Remove domain
157
+ router.delete('/domains/:domainId', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
158
+ var tenantId, domainId, error_4;
159
+ return __generator(this, function (_a) {
160
+ switch (_a.label) {
161
+ case 0:
162
+ _a.trys.push([0, 2, , 3]);
163
+ tenantId = req.user.tenantId;
164
+ domainId = req.params.domainId;
165
+ return [4 /*yield*/, database_1.database.run('DELETE FROM domains WHERE id = ? AND tenant_id = ?', [domainId, tenantId])];
166
+ case 1:
167
+ _a.sent();
168
+ res.json({ message: 'Domain removed successfully' });
169
+ return [3 /*break*/, 3];
170
+ case 2:
171
+ error_4 = _a.sent();
172
+ console.error('Remove domain error:', error_4);
173
+ res.status(500).json({ error: 'Internal server error' });
174
+ return [3 /*break*/, 3];
175
+ case 3: return [2 /*return*/];
176
+ }
177
+ });
178
+ }); });
179
+ // Get tenant analytics summary
180
+ router.get('/analytics/summary', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
181
+ var tenantId, totalConversations, totalMessages, avgRating, knowledgeBaseCount, error_5;
182
+ return __generator(this, function (_a) {
183
+ switch (_a.label) {
184
+ case 0:
185
+ _a.trys.push([0, 5, , 6]);
186
+ tenantId = req.user.tenantId;
187
+ return [4 /*yield*/, database_1.database.get('SELECT COUNT(*) as count FROM chat_sessions WHERE tenant_id = ?', [tenantId])];
188
+ case 1:
189
+ totalConversations = _a.sent();
190
+ return [4 /*yield*/, database_1.database.get('SELECT COUNT(*) as count FROM chat_messages cm JOIN chat_sessions cs ON cm.session_id = cs.id WHERE cs.tenant_id = ?', [tenantId])];
191
+ case 2:
192
+ totalMessages = _a.sent();
193
+ return [4 /*yield*/, database_1.database.get('SELECT AVG(rating) as avg FROM chat_sessions WHERE tenant_id = ? AND rating IS NOT NULL', [tenantId])];
194
+ case 3:
195
+ avgRating = _a.sent();
196
+ return [4 /*yield*/, database_1.database.get('SELECT COUNT(*) as count FROM knowledge_base WHERE tenant_id = ?', [tenantId])];
197
+ case 4:
198
+ knowledgeBaseCount = _a.sent();
199
+ res.json({
200
+ totalConversations: totalConversations.count,
201
+ totalMessages: totalMessages.count,
202
+ averageRating: avgRating.avg || 0,
203
+ knowledgeBaseDocuments: knowledgeBaseCount.count
204
+ });
205
+ return [3 /*break*/, 6];
206
+ case 5:
207
+ error_5 = _a.sent();
208
+ console.error('Get analytics summary error:', error_5);
209
+ res.status(500).json({ error: 'Internal server error' });
210
+ return [3 /*break*/, 6];
211
+ case 6: return [2 /*return*/];
212
+ }
213
+ });
214
+ }); });
215
+ exports.default = router;
src/routes/tenants.ts ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { database } from '../db/database';
3
+ import { AuthenticatedRequest } from '../middleware/auth';
4
+
5
+ const router = express.Router();
6
+
7
+ // Get current tenant info
8
+ router.get('/me', async (req: AuthenticatedRequest, res) => {
9
+ try {
10
+ const { tenantId } = req.user!;
11
+
12
+ const tenant = await database.get(
13
+ 'SELECT id, name, subdomain, plan, settings, created_at FROM tenants WHERE id = ?',
14
+ [tenantId]
15
+ );
16
+
17
+ if (!tenant) {
18
+ return res.status(404).json({ error: 'Tenant not found' });
19
+ }
20
+
21
+ // Get domains
22
+ const domains = await database.query(
23
+ 'SELECT id, domain, verified, created_at FROM domains WHERE tenant_id = ?',
24
+ [tenantId]
25
+ );
26
+
27
+ res.json({
28
+ ...tenant,
29
+ settings: JSON.parse(tenant.settings || '{}'),
30
+ domains
31
+ });
32
+ } catch (error) {
33
+ console.error('Get tenant error:', error);
34
+ res.status(500).json({ error: 'Internal server error' });
35
+ }
36
+ });
37
+
38
+ // Update tenant settings
39
+ router.put('/me', async (req: AuthenticatedRequest, res) => {
40
+ try {
41
+ const { tenantId } = req.user!;
42
+ const { name, settings } = req.body;
43
+
44
+ const updates = [];
45
+ const params = [];
46
+
47
+ if (name) {
48
+ updates.push('name = ?');
49
+ params.push(name);
50
+ }
51
+
52
+ if (settings) {
53
+ updates.push('settings = ?');
54
+ params.push(JSON.stringify(settings));
55
+ }
56
+
57
+ if (updates.length === 0) {
58
+ return res.status(400).json({ error: 'No valid fields to update' });
59
+ }
60
+
61
+ updates.push('updated_at = CURRENT_TIMESTAMP');
62
+ params.push(tenantId);
63
+
64
+ await database.run(
65
+ `UPDATE tenants SET ${updates.join(', ')} WHERE id = ?`,
66
+ params
67
+ );
68
+
69
+ res.json({ message: 'Tenant updated successfully' });
70
+ } catch (error) {
71
+ console.error('Update tenant error:', error);
72
+ res.status(500).json({ error: 'Internal server error' });
73
+ }
74
+ });
75
+
76
+ // Add domain
77
+ router.post('/domains', async (req: AuthenticatedRequest, res) => {
78
+ try {
79
+ const { tenantId } = req.user!;
80
+ const { domain } = req.body;
81
+
82
+ if (!domain) {
83
+ return res.status(400).json({ error: 'Domain is required' });
84
+ }
85
+
86
+ // Check if domain already exists
87
+ const existingDomain = await database.get(
88
+ 'SELECT id FROM domains WHERE domain = ?',
89
+ [domain]
90
+ );
91
+
92
+ if (existingDomain) {
93
+ return res.status(400).json({ error: 'Domain already exists' });
94
+ }
95
+
96
+ // Add domain
97
+ const result = await database.run(
98
+ 'INSERT INTO domains (tenant_id, domain) VALUES (?, ?)',
99
+ [tenantId, domain]
100
+ );
101
+
102
+ res.status(201).json({
103
+ id: result.lastID,
104
+ domain,
105
+ verified: false,
106
+ message: 'Domain added successfully'
107
+ });
108
+ } catch (error) {
109
+ console.error('Add domain error:', error);
110
+ res.status(500).json({ error: 'Internal server error' });
111
+ }
112
+ });
113
+
114
+ // Remove domain
115
+ router.delete('/domains/:domainId', async (req: AuthenticatedRequest, res) => {
116
+ try {
117
+ const { tenantId } = req.user!;
118
+ const { domainId } = req.params;
119
+
120
+ await database.run(
121
+ 'DELETE FROM domains WHERE id = ? AND tenant_id = ?',
122
+ [domainId, tenantId]
123
+ );
124
+
125
+ res.json({ message: 'Domain removed successfully' });
126
+ } catch (error) {
127
+ console.error('Remove domain error:', error);
128
+ res.status(500).json({ error: 'Internal server error' });
129
+ }
130
+ });
131
+
132
+ // Get tenant analytics summary
133
+ router.get('/analytics/summary', async (req: AuthenticatedRequest, res) => {
134
+ try {
135
+ const { tenantId } = req.user!;
136
+
137
+ // Get basic stats
138
+ const totalConversations = await database.get(
139
+ 'SELECT COUNT(*) as count FROM chat_sessions WHERE tenant_id = ?',
140
+ [tenantId]
141
+ );
142
+
143
+ const totalMessages = await database.get(
144
+ 'SELECT COUNT(*) as count FROM chat_messages cm JOIN chat_sessions cs ON cm.session_id = cs.id WHERE cs.tenant_id = ?',
145
+ [tenantId]
146
+ );
147
+
148
+ const avgRating = await database.get(
149
+ 'SELECT AVG(rating) as avg FROM chat_sessions WHERE tenant_id = ? AND rating IS NOT NULL',
150
+ [tenantId]
151
+ );
152
+
153
+ const knowledgeBaseCount = await database.get(
154
+ 'SELECT COUNT(*) as count FROM knowledge_base WHERE tenant_id = ?',
155
+ [tenantId]
156
+ );
157
+
158
+ res.json({
159
+ totalConversations: totalConversations.count,
160
+ totalMessages: totalMessages.count,
161
+ averageRating: avgRating.avg || 0,
162
+ knowledgeBaseDocuments: knowledgeBaseCount.count
163
+ });
164
+ } catch (error) {
165
+ console.error('Get analytics summary error:', error);
166
+ res.status(500).json({ error: 'Internal server error' });
167
+ }
168
+ });
169
+
170
+ export default router;
src/routes/widget.js ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
14
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
15
+ return new (P || (P = Promise))(function (resolve, reject) {
16
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
17
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
18
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
19
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
20
+ });
21
+ };
22
+ var __generator = (this && this.__generator) || function (thisArg, body) {
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
24
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
+ function verb(n) { return function (v) { return step([n, v]); }; }
26
+ function step(op) {
27
+ if (f) throw new TypeError("Generator is already executing.");
28
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
29
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
30
+ if (y = 0, t) op = [op[0] & 2, t.value];
31
+ switch (op[0]) {
32
+ case 0: case 1: t = op; break;
33
+ case 4: _.label++; return { value: op[1], done: false };
34
+ case 5: _.label++; y = op[1]; op = [0]; continue;
35
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
36
+ default:
37
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
38
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
39
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
40
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
41
+ if (t[2]) _.ops.pop();
42
+ _.trys.pop(); continue;
43
+ }
44
+ op = body.call(thisArg, _);
45
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
46
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
+ }
48
+ };
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ var express_1 = require("express");
51
+ var database_1 = require("../db/database");
52
+ var router = express_1.default.Router();
53
+ // Get widget configuration
54
+ router.get('/config/:tenantId', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
55
+ var tenantId, tenant, config, defaultConfig, widgetConfig, error_1;
56
+ return __generator(this, function (_a) {
57
+ switch (_a.label) {
58
+ case 0:
59
+ _a.trys.push([0, 3, , 4]);
60
+ tenantId = req.params.tenantId;
61
+ return [4 /*yield*/, database_1.database.get('SELECT id, name, plan FROM tenants WHERE id = ?', [tenantId])];
62
+ case 1:
63
+ tenant = _a.sent();
64
+ if (!tenant) {
65
+ return [2 /*return*/, res.status(404).json({ error: 'Tenant not found' })];
66
+ }
67
+ return [4 /*yield*/, database_1.database.get('SELECT config FROM widget_configs WHERE tenant_id = ?', [tenantId])];
68
+ case 2:
69
+ config = _a.sent();
70
+ defaultConfig = {
71
+ theme: {
72
+ primaryColor: '#6366f1',
73
+ backgroundColor: '#ffffff',
74
+ textColor: '#374151',
75
+ borderRadius: '12px'
76
+ },
77
+ position: {
78
+ bottom: '20px',
79
+ right: '20px'
80
+ },
81
+ size: 'medium',
82
+ welcome: {
83
+ title: "Hi! I'm ".concat(tenant.name, "'s AI assistant"),
84
+ message: 'How can I help you today?',
85
+ showAvatar: true
86
+ },
87
+ features: {
88
+ fileUpload: false,
89
+ typing: true,
90
+ ratings: true
91
+ }
92
+ };
93
+ widgetConfig = config ? __assign(__assign({}, defaultConfig), JSON.parse(config.config)) :
94
+ defaultConfig;
95
+ res.json({
96
+ tenantId: tenantId,
97
+ config: widgetConfig
98
+ });
99
+ return [3 /*break*/, 4];
100
+ case 3:
101
+ error_1 = _a.sent();
102
+ console.error('Get widget config error:', error_1);
103
+ res.status(500).json({ error: 'Internal server error' });
104
+ return [3 /*break*/, 4];
105
+ case 4: return [2 /*return*/];
106
+ }
107
+ });
108
+ }); });
109
+ // Get widget script
110
+ router.get('/script/:tenantId', function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
111
+ var tenantId, tenant, script, error_2;
112
+ return __generator(this, function (_a) {
113
+ switch (_a.label) {
114
+ case 0:
115
+ _a.trys.push([0, 2, , 3]);
116
+ tenantId = req.params.tenantId;
117
+ return [4 /*yield*/, database_1.database.get('SELECT id FROM tenants WHERE id = ?', [tenantId])];
118
+ case 1:
119
+ tenant = _a.sent();
120
+ if (!tenant) {
121
+ return [2 /*return*/, res.status(404).json({ error: 'Tenant not found' })];
122
+ }
123
+ script = "\n(function() {\n // MCP Chat Widget\n const TENANT_ID = '".concat(tenantId, "';\n const API_URL = '").concat(process.env.FRONTEND_URL || 'http://localhost:3001', "/api';\n \n // Create widget container\n const widget = document.createElement('div');\n widget.id = 'mcp-chat-widget';\n widget.style.cssText = `\n position: fixed;\n bottom: 20px;\n right: 20px;\n width: 350px;\n height: 500px;\n background: white;\n border-radius: 12px;\n box-shadow: 0 10px 25px rgba(0,0,0,0.1);\n z-index: 10000;\n display: none;\n flex-direction: column;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n border: 1px solid #e5e7eb;\n `;\n\n // Create chat button\n const button = document.createElement('div');\n button.id = 'mcp-chat-button';\n button.style.cssText = `\n position: fixed;\n bottom: 20px;\n right: 20px;\n width: 60px;\n height: 60px;\n background: linear-gradient(135deg, #6366f1, #8b5cf6);\n border-radius: 50%;\n cursor: pointer;\n z-index: 10001;\n display: flex;\n align-items: center;\n justify-content: center;\n box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);\n transition: transform 0.2s ease;\n `;\n \n button.innerHTML = `\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"white\" stroke-width=\"2\">\n <path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"></path>\n </svg>\n `;\n\n button.onmouseover = () => button.style.transform = 'scale(1.05)';\n button.onmouseout = () => button.style.transform = 'scale(1)';\n\n // Widget header\n const header = document.createElement('div');\n header.style.cssText = `\n padding: 16px;\n background: linear-gradient(135deg, #6366f1, #8b5cf6);\n color: white;\n border-radius: 12px 12px 0 0;\n display: flex;\n justify-content: space-between;\n align-items: center;\n `;\n \n header.innerHTML = `\n <div>\n <h3 style=\"margin: 0; font-size: 16px; font-weight: 600;\">Chat Support</h3>\n <p style=\"margin: 4px 0 0 0; font-size: 12px; opacity: 0.9;\">We're here to help!</p>\n </div>\n <button id=\"mcp-close-btn\" style=\"background: none; border: none; color: white; font-size: 20px; cursor: pointer; padding: 4px;\">\u00D7</button>\n `;\n\n // Messages container\n const messages = document.createElement('div');\n messages.id = 'mcp-messages';\n messages.style.cssText = `\n flex: 1;\n padding: 16px;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 12px;\n `;\n\n // Input container\n const inputContainer = document.createElement('div');\n inputContainer.style.cssText = `\n padding: 16px;\n border-top: 1px solid #e5e7eb;\n display: flex;\n gap: 8px;\n `;\n\n const input = document.createElement('input');\n input.type = 'text';\n input.placeholder = 'Type your message...';\n input.style.cssText = `\n flex: 1;\n padding: 12px;\n border: 1px solid #d1d5db;\n border-radius: 8px;\n outline: none;\n font-size: 14px;\n `;\n\n const sendBtn = document.createElement('button');\n sendBtn.textContent = 'Send';\n sendBtn.style.cssText = `\n padding: 12px 16px;\n background: #6366f1;\n color: white;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n font-size: 14px;\n font-weight: 500;\n `;\n\n // Assemble widget\n inputContainer.appendChild(input);\n inputContainer.appendChild(sendBtn);\n widget.appendChild(header);\n widget.appendChild(messages);\n widget.appendChild(inputContainer);\n\n // Add to page\n document.body.appendChild(button);\n document.body.appendChild(widget);\n\n // Session management\n let sessionToken = null;\n let isVisible = false;\n\n // Toggle widget\n function toggleWidget() {\n isVisible = !isVisible;\n widget.style.display = isVisible ? 'flex' : 'none';\n button.style.display = isVisible ? 'none' : 'flex';\n \n if (isVisible && !sessionToken) {\n initializeSession();\n }\n }\n\n // Initialize chat session\n async function initializeSession() {\n try {\n const response = await fetch(`${API_URL}/chat/sessions`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n tenantId: TENANT_ID,\n domain: window.location.hostname,\n userAgent: navigator.userAgent\n })\n });\n \n const data = await response.json();\n sessionToken = data.sessionToken;\n \n // Add welcome message\n addMessage('ai', 'Hello! How can I help you today?');\n } catch (error) {\n console.error('Failed to initialize session:', error);\n addMessage('ai', 'Sorry, I\\'m having trouble connecting. Please try again later.');\n }\n }\n\n // Send message\n async function sendMessage(message) {\n if (!sessionToken || !message.trim()) return;\n\n addMessage('user', message);\n input.value = '';\n \n // Show typing indicator\n const typingEl = addMessage('ai', 'Typing...', true);\n \n try {\n const response = await fetch(`${API_URL}/chat/messages`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n sessionToken,\n message,\n tenantId: TENANT_ID\n })\n });\n \n const data = await response.json();\n \n // Remove typing indicator\n typingEl.remove();\n \n // Add AI response\n addMessage('ai', data.response);\n \n } catch (error) {\n typingEl.remove();\n addMessage('ai', 'Sorry, I encountered an error. Please try again.');\n }\n }\n\n // Add message to chat\n function addMessage(sender, text, isTyping = false) {\n const messageEl = document.createElement('div');\n messageEl.style.cssText = `\n max-width: 80%;\n padding: 12px 16px;\n border-radius: 18px;\n font-size: 14px;\n line-height: 1.4;\n ${sender === 'user' ? \n 'background: #6366f1; color: white; align-self: flex-end; margin-left: auto;' : \n 'background: #f3f4f6; color: #374151; align-self: flex-start;'\n }\n ${isTyping ? 'opacity: 0.7; font-style: italic;' : ''}\n `;\n \n messageEl.textContent = text;\n messages.appendChild(messageEl);\n messages.scrollTop = messages.scrollHeight;\n \n return messageEl;\n }\n\n // Event listeners\n button.onclick = toggleWidget;\n header.querySelector('#mcp-close-btn').onclick = toggleWidget;\n sendBtn.onclick = () => sendMessage(input.value);\n input.onkeypress = (e) => {\n if (e.key === 'Enter') sendMessage(input.value);\n };\n\n console.log('MCP Chat Widget loaded successfully');\n})();\n");
124
+ res.setHeader('Content-Type', 'application/javascript');
125
+ res.send(script);
126
+ return [3 /*break*/, 3];
127
+ case 2:
128
+ error_2 = _a.sent();
129
+ console.error('Get widget script error:', error_2);
130
+ res.status(500).json({ error: 'Internal server error' });
131
+ return [3 /*break*/, 3];
132
+ case 3: return [2 /*return*/];
133
+ }
134
+ });
135
+ }); });
136
+ exports.default = router;
src/routes/widget.ts ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { database } from '../db/database';
3
+
4
+ const router = express.Router();
5
+
6
+ // Get widget configuration
7
+ router.get('/config/:tenantId', async (req, res) => {
8
+ try {
9
+ const { tenantId } = req.params;
10
+
11
+ // Get tenant info
12
+ const tenant = await database.get(
13
+ 'SELECT id, name, plan FROM tenants WHERE id = ?',
14
+ [tenantId]
15
+ );
16
+
17
+ if (!tenant) {
18
+ return res.status(404).json({ error: 'Tenant not found' });
19
+ }
20
+
21
+ // Get widget configuration
22
+ const config = await database.get(
23
+ 'SELECT config FROM widget_configs WHERE tenant_id = ?',
24
+ [tenantId]
25
+ );
26
+
27
+ const defaultConfig = {
28
+ theme: {
29
+ primaryColor: '#6366f1',
30
+ backgroundColor: '#ffffff',
31
+ textColor: '#374151',
32
+ borderRadius: '12px'
33
+ },
34
+ position: {
35
+ bottom: '20px',
36
+ right: '20px'
37
+ },
38
+ size: 'medium',
39
+ welcome: {
40
+ title: `Hi! I'm ${tenant.name}'s AI assistant`,
41
+ message: 'How can I help you today?',
42
+ showAvatar: true
43
+ },
44
+ features: {
45
+ fileUpload: false,
46
+ typing: true,
47
+ ratings: true
48
+ }
49
+ };
50
+
51
+ const widgetConfig = config ?
52
+ { ...defaultConfig, ...JSON.parse(config.config) } :
53
+ defaultConfig;
54
+
55
+ res.json({
56
+ tenantId,
57
+ config: widgetConfig
58
+ });
59
+ } catch (error) {
60
+ console.error('Get widget config error:', error);
61
+ res.status(500).json({ error: 'Internal server error' });
62
+ }
63
+ });
64
+
65
+ // Get widget script
66
+ router.get('/script/:tenantId', async (req, res) => {
67
+ try {
68
+ const { tenantId } = req.params;
69
+
70
+ // Verify tenant exists
71
+ const tenant = await database.get(
72
+ 'SELECT id FROM tenants WHERE id = ?',
73
+ [tenantId]
74
+ );
75
+
76
+ if (!tenant) {
77
+ return res.status(404).json({ error: 'Tenant not found' });
78
+ }
79
+
80
+ const script = `
81
+ (function() {
82
+ // MCP Chat Widget
83
+ const TENANT_ID = '${tenantId}';
84
+ const API_URL = '${process.env.FRONTEND_URL || 'http://localhost:3001'}/api';
85
+
86
+ // Create widget container
87
+ const widget = document.createElement('div');
88
+ widget.id = 'mcp-chat-widget';
89
+ widget.style.cssText = \`
90
+ position: fixed;
91
+ bottom: 20px;
92
+ right: 20px;
93
+ width: 350px;
94
+ height: 500px;
95
+ background: white;
96
+ border-radius: 12px;
97
+ box-shadow: 0 10px 25px rgba(0,0,0,0.1);
98
+ z-index: 10000;
99
+ display: none;
100
+ flex-direction: column;
101
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
102
+ border: 1px solid #e5e7eb;
103
+ \`;
104
+
105
+ // Create chat button
106
+ const button = document.createElement('div');
107
+ button.id = 'mcp-chat-button';
108
+ button.style.cssText = \`
109
+ position: fixed;
110
+ bottom: 20px;
111
+ right: 20px;
112
+ width: 60px;
113
+ height: 60px;
114
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
115
+ border-radius: 50%;
116
+ cursor: pointer;
117
+ z-index: 10001;
118
+ display: flex;
119
+ align-items: center;
120
+ justify-content: center;
121
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
122
+ transition: transform 0.2s ease;
123
+ \`;
124
+
125
+ button.innerHTML = \`
126
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
127
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
128
+ </svg>
129
+ \`;
130
+
131
+ button.onmouseover = () => button.style.transform = 'scale(1.05)';
132
+ button.onmouseout = () => button.style.transform = 'scale(1)';
133
+
134
+ // Widget header
135
+ const header = document.createElement('div');
136
+ header.style.cssText = \`
137
+ padding: 16px;
138
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
139
+ color: white;
140
+ border-radius: 12px 12px 0 0;
141
+ display: flex;
142
+ justify-content: space-between;
143
+ align-items: center;
144
+ \`;
145
+
146
+ header.innerHTML = \`
147
+ <div>
148
+ <h3 style="margin: 0; font-size: 16px; font-weight: 600;">Chat Support</h3>
149
+ <p style="margin: 4px 0 0 0; font-size: 12px; opacity: 0.9;">We're here to help!</p>
150
+ </div>
151
+ <button id="mcp-close-btn" style="background: none; border: none; color: white; font-size: 20px; cursor: pointer; padding: 4px;">×</button>
152
+ \`;
153
+
154
+ // Messages container
155
+ const messages = document.createElement('div');
156
+ messages.id = 'mcp-messages';
157
+ messages.style.cssText = \`
158
+ flex: 1;
159
+ padding: 16px;
160
+ overflow-y: auto;
161
+ display: flex;
162
+ flex-direction: column;
163
+ gap: 12px;
164
+ \`;
165
+
166
+ // Input container
167
+ const inputContainer = document.createElement('div');
168
+ inputContainer.style.cssText = \`
169
+ padding: 16px;
170
+ border-top: 1px solid #e5e7eb;
171
+ display: flex;
172
+ gap: 8px;
173
+ \`;
174
+
175
+ const input = document.createElement('input');
176
+ input.type = 'text';
177
+ input.placeholder = 'Type your message...';
178
+ input.style.cssText = \`
179
+ flex: 1;
180
+ padding: 12px;
181
+ border: 1px solid #d1d5db;
182
+ border-radius: 8px;
183
+ outline: none;
184
+ font-size: 14px;
185
+ \`;
186
+
187
+ const sendBtn = document.createElement('button');
188
+ sendBtn.textContent = 'Send';
189
+ sendBtn.style.cssText = \`
190
+ padding: 12px 16px;
191
+ background: #6366f1;
192
+ color: white;
193
+ border: none;
194
+ border-radius: 8px;
195
+ cursor: pointer;
196
+ font-size: 14px;
197
+ font-weight: 500;
198
+ \`;
199
+
200
+ // Assemble widget
201
+ inputContainer.appendChild(input);
202
+ inputContainer.appendChild(sendBtn);
203
+ widget.appendChild(header);
204
+ widget.appendChild(messages);
205
+ widget.appendChild(inputContainer);
206
+
207
+ // Add to page
208
+ document.body.appendChild(button);
209
+ document.body.appendChild(widget);
210
+
211
+ // Session management
212
+ let sessionToken = null;
213
+ let isVisible = false;
214
+
215
+ // Toggle widget
216
+ function toggleWidget() {
217
+ isVisible = !isVisible;
218
+ widget.style.display = isVisible ? 'flex' : 'none';
219
+ button.style.display = isVisible ? 'none' : 'flex';
220
+
221
+ if (isVisible && !sessionToken) {
222
+ initializeSession();
223
+ }
224
+ }
225
+
226
+ // Initialize chat session
227
+ async function initializeSession() {
228
+ try {
229
+ const response = await fetch(\`\${API_URL}/chat/sessions\`, {
230
+ method: 'POST',
231
+ headers: { 'Content-Type': 'application/json' },
232
+ body: JSON.stringify({
233
+ tenantId: TENANT_ID,
234
+ domain: window.location.hostname,
235
+ userAgent: navigator.userAgent
236
+ })
237
+ });
238
+
239
+ const data = await response.json();
240
+ sessionToken = data.sessionToken;
241
+
242
+ // Add welcome message
243
+ addMessage('ai', 'Hello! How can I help you today?');
244
+ } catch (error) {
245
+ console.error('Failed to initialize session:', error);
246
+ addMessage('ai', 'Sorry, I\\'m having trouble connecting. Please try again later.');
247
+ }
248
+ }
249
+
250
+ // Send message
251
+ async function sendMessage(message) {
252
+ if (!sessionToken || !message.trim()) return;
253
+
254
+ addMessage('user', message);
255
+ input.value = '';
256
+
257
+ // Show typing indicator
258
+ const typingEl = addMessage('ai', 'Typing...', true);
259
+
260
+ try {
261
+ const response = await fetch(\`\${API_URL}/chat/messages\`, {
262
+ method: 'POST',
263
+ headers: { 'Content-Type': 'application/json' },
264
+ body: JSON.stringify({
265
+ sessionToken,
266
+ message,
267
+ tenantId: TENANT_ID
268
+ })
269
+ });
270
+
271
+ const data = await response.json();
272
+
273
+ // Remove typing indicator
274
+ typingEl.remove();
275
+
276
+ // Add AI response
277
+ addMessage('ai', data.response);
278
+
279
+ } catch (error) {
280
+ typingEl.remove();
281
+ addMessage('ai', 'Sorry, I encountered an error. Please try again.');
282
+ }
283
+ }
284
+
285
+ // Add message to chat
286
+ function addMessage(sender, text, isTyping = false) {
287
+ const messageEl = document.createElement('div');
288
+ messageEl.style.cssText = \`
289
+ max-width: 80%;
290
+ padding: 12px 16px;
291
+ border-radius: 18px;
292
+ font-size: 14px;
293
+ line-height: 1.4;
294
+ \${sender === 'user' ?
295
+ 'background: #6366f1; color: white; align-self: flex-end; margin-left: auto;' :
296
+ 'background: #f3f4f6; color: #374151; align-self: flex-start;'
297
+ }
298
+ \${isTyping ? 'opacity: 0.7; font-style: italic;' : ''}
299
+ \`;
300
+
301
+ messageEl.textContent = text;
302
+ messages.appendChild(messageEl);
303
+ messages.scrollTop = messages.scrollHeight;
304
+
305
+ return messageEl;
306
+ }
307
+
308
+ // Event listeners
309
+ button.onclick = toggleWidget;
310
+ header.querySelector('#mcp-close-btn').onclick = toggleWidget;
311
+ sendBtn.onclick = () => sendMessage(input.value);
312
+ input.onkeypress = (e) => {
313
+ if (e.key === 'Enter') sendMessage(input.value);
314
+ };
315
+
316
+ console.log('MCP Chat Widget loaded successfully');
317
+ })();
318
+ `;
319
+
320
+ res.setHeader('Content-Type', 'application/javascript');
321
+ res.send(script);
322
+ } catch (error) {
323
+ console.error('Get widget script error:', error);
324
+ res.status(500).json({ error: 'Internal server error' });
325
+ }
326
+ });
327
+
328
+ export default router;
src/server.js ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ var express_1 = require("express");
40
+ var cors_1 = require("cors");
41
+ var helmet_1 = require("helmet");
42
+ var dotenv_1 = require("dotenv");
43
+ var express_rate_limit_1 = require("express-rate-limit");
44
+ var http_1 = require("http");
45
+ var ws_1 = require("ws");
46
+ var path_1 = require("path");
47
+ // Import routes
48
+ var auth_1 = require("./routes/auth");
49
+ var tenants_1 = require("./routes/tenants");
50
+ var knowledge_base_1 = require("./routes/knowledge-base");
51
+ var chat_1 = require("./routes/chat");
52
+ var analytics_1 = require("./routes/analytics");
53
+ var widget_1 = require("./routes/widget");
54
+ // Import middleware
55
+ var auth_2 = require("./middleware/auth");
56
+ var errorHandler_1 = require("./middleware/errorHandler");
57
+ // Import database initialization
58
+ var database_1 = require("./db/database");
59
+ // Import WebSocket handler
60
+ var websocket_1 = require("./services/websocket");
61
+ dotenv_1.default.config();
62
+ var app = (0, express_1.default)();
63
+ var PORT = process.env.PORT || 3001;
64
+ // Create HTTP server for WebSocket support
65
+ var server = (0, http_1.createServer)(app);
66
+ // Initialize WebSocket
67
+ var wss = new ws_1.WebSocketServer({ server: server });
68
+ (0, websocket_1.setupWebSocket)(wss);
69
+ // Security middleware
70
+ app.use((0, helmet_1.default)({
71
+ contentSecurityPolicy: false, // Disable for development
72
+ crossOriginEmbedderPolicy: false
73
+ }));
74
+ // Rate limiting
75
+ var limiter = (0, express_rate_limit_1.default)({
76
+ windowMs: 15 * 60 * 1000, // 15 minutes
77
+ max: 100, // limit each IP to 100 requests per windowMs
78
+ message: 'Too many requests from this IP, please try again later.'
79
+ });
80
+ app.use('/api/', limiter);
81
+ // CORS configuration
82
+ app.use((0, cors_1.default)({
83
+ origin: [
84
+ process.env.FRONTEND_URL || 'http://localhost:5173',
85
+ 'http://localhost:3000',
86
+ 'https://your-frontend-domain.com' // Add your production frontend URL
87
+ ],
88
+ credentials: true,
89
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
90
+ allowedHeaders: ['Content-Type', 'Authorization']
91
+ }));
92
+ // Body parsing middleware
93
+ app.use(express_1.default.json({ limit: '50mb' }));
94
+ app.use(express_1.default.urlencoded({ extended: true, limit: '50mb' }));
95
+ // Static file serving for uploads
96
+ app.use('/uploads', express_1.default.static(path_1.default.join(__dirname, '../uploads')));
97
+ // Health check endpoint
98
+ app.get('/health', function (req, res) {
99
+ res.json({
100
+ status: 'healthy',
101
+ timestamp: new Date().toISOString(),
102
+ version: '1.0.0'
103
+ });
104
+ });
105
+ // API Routes
106
+ app.use('/api/auth', auth_1.default);
107
+ app.use('/api/tenants', auth_2.authenticateToken, tenants_1.default);
108
+ app.use('/api/knowledge-base', auth_2.authenticateToken, knowledge_base_1.default);
109
+ app.use('/api/chat', chat_1.default); // No auth required for public chat widget
110
+ app.use('/api/analytics', auth_2.authenticateToken, analytics_1.default);
111
+ app.use('/api/widget', widget_1.default); // No auth required for widget access
112
+ // Error handling middleware
113
+ app.use(errorHandler_1.errorHandler);
114
+ // 404 handler
115
+ app.use('*', function (req, res) {
116
+ res.status(404).json({ error: 'Endpoint not found' });
117
+ });
118
+ // Initialize database and start server
119
+ function startServer() {
120
+ return __awaiter(this, void 0, void 0, function () {
121
+ var error_1;
122
+ return __generator(this, function (_a) {
123
+ switch (_a.label) {
124
+ case 0:
125
+ _a.trys.push([0, 2, , 3]);
126
+ return [4 /*yield*/, (0, database_1.initializeDatabase)()];
127
+ case 1:
128
+ _a.sent();
129
+ console.log('✅ Database initialized successfully');
130
+ server.listen(PORT, function () {
131
+ console.log("\uD83D\uDE80 Server running on port ".concat(PORT));
132
+ console.log("\uD83D\uDCF1 Health check: http://localhost:".concat(PORT, "/health"));
133
+ console.log("\uD83D\uDD0C WebSocket server ready");
134
+ console.log("\uD83C\uDF10 CORS enabled for: ".concat(process.env.FRONTEND_URL));
135
+ });
136
+ return [3 /*break*/, 3];
137
+ case 2:
138
+ error_1 = _a.sent();
139
+ console.error('❌ Failed to start server:', error_1);
140
+ process.exit(1);
141
+ return [3 /*break*/, 3];
142
+ case 3: return [2 /*return*/];
143
+ }
144
+ });
145
+ });
146
+ }
147
+ startServer();
148
+ exports.default = app;
src/server.ts ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import helmet from 'helmet';
4
+ import dotenv from 'dotenv';
5
+ import rateLimit from 'express-rate-limit';
6
+ import { createServer } from 'http';
7
+ import { WebSocketServer } from 'ws';
8
+ import path from 'path';
9
+
10
+ // Import routes
11
+ import authRoutes from './routes/auth';
12
+ import tenantRoutes from './routes/tenants';
13
+ import knowledgeBaseRoutes from './routes/knowledge-base';
14
+ import chatRoutes from './routes/chat';
15
+ import analyticsRoutes from './routes/analytics';
16
+ import widgetRoutes from './routes/widget';
17
+
18
+ // Import middleware
19
+ import { authenticateToken } from './middleware/auth';
20
+ import { errorHandler } from './middleware/errorHandler';
21
+
22
+ // Import database initialization
23
+ import { initializeDatabase } from './db/database';
24
+
25
+ // Import WebSocket handler
26
+ import { setupWebSocket } from './services/websocket';
27
+
28
+ dotenv.config();
29
+
30
+ const app = express();
31
+ const PORT = process.env.PORT || 3001;
32
+
33
+ // Create HTTP server for WebSocket support
34
+ const server = createServer(app);
35
+
36
+ // Initialize WebSocket
37
+ const wss = new WebSocketServer({ server });
38
+ setupWebSocket(wss);
39
+
40
+ // Security middleware
41
+ app.use(helmet({
42
+ contentSecurityPolicy: false, // Disable for development
43
+ crossOriginEmbedderPolicy: false
44
+ }));
45
+
46
+ // Rate limiting
47
+ const limiter = rateLimit({
48
+ windowMs: 15 * 60 * 1000, // 15 minutes
49
+ max: 100, // limit each IP to 100 requests per windowMs
50
+ message: 'Too many requests from this IP, please try again later.'
51
+ });
52
+ app.use('/api/', limiter);
53
+
54
+ // CORS configuration
55
+ app.use(cors({
56
+ origin: [
57
+ process.env.FRONTEND_URL || 'http://localhost:5173',
58
+ 'http://localhost:3000',
59
+ 'https://your-frontend-domain.com' // Add your production frontend URL
60
+ ],
61
+ credentials: true,
62
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
63
+ allowedHeaders: ['Content-Type', 'Authorization']
64
+ }));
65
+
66
+ // Body parsing middleware
67
+ app.use(express.json({ limit: '50mb' }));
68
+ app.use(express.urlencoded({ extended: true, limit: '50mb' }));
69
+
70
+ // Static file serving for uploads
71
+ app.use('/uploads', express.static(path.join(__dirname, '../uploads')));
72
+
73
+ // Health check endpoint
74
+ app.get('/health', (req, res) => {
75
+ res.json({
76
+ status: 'healthy',
77
+ timestamp: new Date().toISOString(),
78
+ version: '1.0.0'
79
+ });
80
+ });
81
+
82
+ // API Routes
83
+ app.use('/api/auth', authRoutes);
84
+ app.use('/api/tenants', authenticateToken, tenantRoutes);
85
+ app.use('/api/knowledge-base', authenticateToken, knowledgeBaseRoutes);
86
+ app.use('/api/chat', chatRoutes); // No auth required for public chat widget
87
+ app.use('/api/analytics', authenticateToken, analyticsRoutes);
88
+ app.use('/api/widget', widgetRoutes); // No auth required for widget access
89
+
90
+ // Error handling middleware
91
+ app.use(errorHandler);
92
+
93
+ // 404 handler
94
+ app.use('*', (req, res) => {
95
+ res.status(404).json({ error: 'Endpoint not found' });
96
+ });
97
+
98
+ // Initialize database and start server
99
+ async function startServer() {
100
+ try {
101
+ await initializeDatabase();
102
+ console.log('✅ Database initialized successfully');
103
+
104
+ server.listen(PORT, () => {
105
+ console.log(`🚀 Server running on port ${PORT}`);
106
+ console.log(`📱 Health check: http://localhost:${PORT}/health`);
107
+ console.log(`🔌 WebSocket server ready`);
108
+ console.log(`🌐 CORS enabled for: ${process.env.FRONTEND_URL}`);
109
+ });
110
+ } catch (error) {
111
+ console.error('❌ Failed to start server:', error);
112
+ process.exit(1);
113
+ }
114
+ }
115
+
116
+ startServer();
117
+
118
+ export default app;
src/services/websocket.js ADDED
File without changes
src/services/websocket.ts ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { WebSocketServer, WebSocket } from 'ws';
2
+ import { IncomingMessage } from 'http';
3
+ import { parse } from 'url';
4
+
5
+ interface ExtendedWebSocket extends WebSocket {
6
+ tenantId?: string;
7
+ sessionToken?: string;
8
+ isAlive?: boolean;
9
+ }
10
+
11
+ export function setupWebSocket(wss: WebSocketServer) {
12
+ const clients = new Map<string, ExtendedWebSocket>();
13
+
14
+ wss.on('connection', (ws: ExtendedWebSocket, req: IncomingMessage) => {
15
+ // Parse query parameters
16
+ const query = parse(req.url || '', true).query;
17
+ const tenantId = query.tenantId as string;
18
+ const sessionToken = query.sessionToken as string;
19
+
20
+ if (!tenantId || !sessionToken) {
21
+ ws.close(1008, 'Missing tenantId or sessionToken');
22
+ return;
23
+ }
24
+
25
+ // Set up connection
26
+ ws.tenantId = tenantId;
27
+ ws.sessionToken = sessionToken;
28
+ ws.isAlive = true;
29
+
30
+ // Store client connection
31
+ const clientKey = `${tenantId}:${sessionToken}`;
32
+ clients.set(clientKey, ws);
33
+
34
+ console.log(`WebSocket client connected: ${clientKey}`);
35
+
36
+ // Handle ping/pong for connection health
37
+ ws.on('pong', () => {
38
+ ws.isAlive = true;
39
+ });
40
+
41
+ ws.on('message', (data: Buffer) => {
42
+ try {
43
+ const message = JSON.parse(data.toString());
44
+ handleMessage(ws, message);
45
+ } catch (error) {
46
+ console.error('Invalid WebSocket message:', error);
47
+ ws.send(JSON.stringify({ error: 'Invalid message format' }));
48
+ }
49
+ });
50
+
51
+ ws.on('close', () => {
52
+ console.log(`WebSocket client disconnected: ${clientKey}`);
53
+ clients.delete(clientKey);
54
+ });
55
+
56
+ ws.on('error', (error) => {
57
+ console.error('WebSocket error:', error);
58
+ clients.delete(clientKey);
59
+ });
60
+
61
+ // Send welcome message
62
+ ws.send(JSON.stringify({
63
+ type: 'connection',
64
+ message: 'Connected to chat server',
65
+ timestamp: new Date().toISOString()
66
+ }));
67
+ });
68
+
69
+ // Health check - ping clients every 30 seconds
70
+ const interval = setInterval(() => {
71
+ wss.clients.forEach((ws: ExtendedWebSocket) => {
72
+ if (ws.isAlive === false) {
73
+ return ws.terminate();
74
+ }
75
+
76
+ ws.isAlive = false;
77
+ ws.ping();
78
+ });
79
+ }, 30000);
80
+
81
+ wss.on('close', () => {
82
+ clearInterval(interval);
83
+ });
84
+
85
+ function handleMessage(ws: ExtendedWebSocket, message: any) {
86
+ switch (message.type) {
87
+ case 'ping':
88
+ ws.send(JSON.stringify({ type: 'pong', timestamp: new Date().toISOString() }));
89
+ break;
90
+
91
+ case 'typing':
92
+ // Broadcast typing indicator to other participants (if multi-user chat)
93
+ broadcastToSession(ws.tenantId!, ws.sessionToken!, {
94
+ type: 'typing',
95
+ sender: 'user',
96
+ timestamp: new Date().toISOString()
97
+ }, ws);
98
+ break;
99
+
100
+ case 'stop_typing':
101
+ broadcastToSession(ws.tenantId!, ws.sessionToken!, {
102
+ type: 'stop_typing',
103
+ sender: 'user',
104
+ timestamp: new Date().toISOString()
105
+ }, ws);
106
+ break;
107
+
108
+ default:
109
+ ws.send(JSON.stringify({ error: 'Unknown message type' }));
110
+ }
111
+ }
112
+
113
+ function broadcastToSession(tenantId: string, sessionToken: string, message: any, sender?: WebSocket) {
114
+ const clientKey = `${tenantId}:${sessionToken}`;
115
+ const client = clients.get(clientKey);
116
+
117
+ if (client && client !== sender && client.readyState === WebSocket.OPEN) {
118
+ client.send(JSON.stringify(message));
119
+ }
120
+ }
121
+
122
+ // Utility function to send message to specific session
123
+ function sendToSession(tenantId: string, sessionToken: string, message: any) {
124
+ const clientKey = `${tenantId}:${sessionToken}`;
125
+ const client = clients.get(clientKey);
126
+
127
+ if (client && client.readyState === WebSocket.OPEN) {
128
+ client.send(JSON.stringify(message));
129
+ return true;
130
+ }
131
+ return false;
132
+ }
133
+
134
+ return {
135
+ broadcastToSession,
136
+ sendToSession,
137
+ getConnectedClients: () => clients.size
138
+ };
139
+ }
src/start.js ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ require("./server");
src/start.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ import './server';
tsconfig.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "allowSyntheticDefaultImports": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": false,
15
+ "sourceMap": true,
16
+ "moduleResolution": "node",
17
+ "allowJs": true,
18
+ "noEmit": false
19
+ },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["node_modules", "dist"],
22
+ "ts-node": {
23
+ "esm": false
24
+ }
25
+ }