Spaces:
Sleeping
Sleeping
Upload 33 files
Browse files- .env +30 -0
- .env.production +2 -0
- .gitattributes +1 -0
- .gitignore +6 -0
- database.sqlite +3 -0
- env.example +30 -0
- package-lock.json +0 -0
- package.json +43 -0
- render.yaml +19 -0
- src/db/database.js +229 -0
- src/db/database.ts +239 -0
- src/middleware/auth.js +23 -0
- src/middleware/auth.ts +29 -0
- src/middleware/errorHandler.js +24 -0
- src/middleware/errorHandler.ts +27 -0
- src/routes/analytics.js +219 -0
- src/routes/analytics.ts +199 -0
- src/routes/auth.js +314 -0
- src/routes/auth.ts +313 -0
- src/routes/chat.js +292 -0
- src/routes/chat.ts +302 -0
- src/routes/knowledge-base.js +314 -0
- src/routes/knowledge-base.ts +257 -0
- src/routes/tenants.js +215 -0
- src/routes/tenants.ts +170 -0
- src/routes/widget.js +136 -0
- src/routes/widget.ts +328 -0
- src/server.js +148 -0
- src/server.ts +118 -0
- src/services/websocket.js +0 -0
- src/services/websocket.ts +139 -0
- src/start.js +3 -0
- src/start.ts +1 -0
- tsconfig.json +25 -0
.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 |
+
}
|