@openenthrium/oe-runtime-sdk 1.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/index.js +58 -0
- package/package.json +64 -0
- package/src/data/connectionTypes.json +18587 -0
- package/src/engine/index.js +183 -0
- package/src/engine/llm.js +71 -0
- package/src/engine/promptBuilder.js +38 -0
- package/src/index.js +156 -0
- package/src/middleware/auth.js +63 -0
- package/src/providers/embedding/index.js +69 -0
- package/src/providers/llm/index.js +136 -0
- package/src/routes/admin.js +686 -0
- package/src/routes/agents.js +193 -0
- package/src/routes/apiKeys.js +55 -0
- package/src/routes/audio.js +111 -0
- package/src/routes/auth.js +83 -0
- package/src/routes/chat.js +381 -0
- package/src/routes/connectors.js +435 -0
- package/src/routes/dashboard.js +244 -0
- package/src/routes/documents.js +443 -0
- package/src/routes/embed.js +103 -0
- package/src/routes/marketplace.js +49 -0
- package/src/routes/models.js +81 -0
- package/src/routes/oauth.js +423 -0
- package/src/routes/projects.js +238 -0
- package/src/routes/settings.js +45 -0
- package/src/routes/setup.js +42 -0
- package/src/routes/sso.js +218 -0
- package/src/routes/superadmin.js +51 -0
- package/src/routes/templates.js +75 -0
- package/src/routes/threads.js +53 -0
- package/src/routes/workspaces.js +464 -0
- package/src/telemetry/bootstrap.js +39 -0
- package/src/telemetry/registration.js +16 -0
- package/src/utils/activityLog.js +16 -0
- package/src/utils/agentChain.js +141 -0
- package/src/utils/buildVisualization.js +102 -0
- package/src/utils/dlpScanner.js +57 -0
- package/src/utils/ingestionQueue.js +240 -0
- package/src/utils/prepareConnectors.js +66 -0
- package/src/utils/rag/_settings.js +6 -0
- package/src/utils/rag/chroma.js +82 -0
- package/src/utils/rag/lancedb.js +103 -0
- package/src/utils/rag/milvus.js +116 -0
- package/src/utils/rag/pgvector.js +104 -0
- package/src/utils/rag/pinecone.js +71 -0
- package/src/utils/rag/qdrant.js +106 -0
- package/src/utils/rag/weaviate.js +137 -0
- package/src/utils/rag/zilliz.js +115 -0
- package/src/utils/scheduler.js +168 -0
- package/src/utils/tier.js +106 -0
- package/src/utils/tools/adapters/_template.js +74 -0
- package/src/utils/tools/adapters/box.js +71 -0
- package/src/utils/tools/adapters/confluence.js +80 -0
- package/src/utils/tools/adapters/database.js +215 -0
- package/src/utils/tools/adapters/dropbox.js +74 -0
- package/src/utils/tools/adapters/elasticsearch.js +90 -0
- package/src/utils/tools/adapters/filesystem.js +197 -0
- package/src/utils/tools/adapters/freshdesk.js +87 -0
- package/src/utils/tools/adapters/gdrive.js +326 -0
- package/src/utils/tools/adapters/github.js +169 -0
- package/src/utils/tools/adapters/gmail.js +157 -0
- package/src/utils/tools/adapters/graphql.js +73 -0
- package/src/utils/tools/adapters/hubspot.js +101 -0
- package/src/utils/tools/adapters/image-gen.js +120 -0
- package/src/utils/tools/adapters/jira.js +86 -0
- package/src/utils/tools/adapters/kafka.js +126 -0
- package/src/utils/tools/adapters/ldap.js +118 -0
- package/src/utils/tools/adapters/mcp-client.js +138 -0
- package/src/utils/tools/adapters/mongodb.js +119 -0
- package/src/utils/tools/adapters/mqtt.js +106 -0
- package/src/utils/tools/adapters/music-gen.js +118 -0
- package/src/utils/tools/adapters/notion.js +93 -0
- package/src/utils/tools/adapters/ocr.js +107 -0
- package/src/utils/tools/adapters/onedrive.js +72 -0
- package/src/utils/tools/adapters/redis.js +104 -0
- package/src/utils/tools/adapters/rest-api.js +119 -0
- package/src/utils/tools/adapters/s3.js +142 -0
- package/src/utils/tools/adapters/search.js +80 -0
- package/src/utils/tools/adapters/sftp.js +121 -0
- package/src/utils/tools/adapters/shell.js +97 -0
- package/src/utils/tools/adapters/slack.js +83 -0
- package/src/utils/tools/adapters/speech.js +160 -0
- package/src/utils/tools/adapters/ssh.js +113 -0
- package/src/utils/tools/adapters/video-gen.js +152 -0
- package/src/utils/tools/adapters/web3.js +111 -0
- package/src/utils/tools/adapters/zendesk.js +82 -0
- package/src/utils/tools/adapters/zoho-mail.js +246 -0
- package/src/utils/tools/registry.js +229 -0
- package/src/utils/vectorStore.js +68 -0
- package/src/utils/workflowEngine.js +4 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const { authenticate, requireManagerOrAdmin } = require("../middleware/auth");
|
|
3
|
+
const { getLLMConfig } = require("../providers/llm");
|
|
4
|
+
const engine = require("../engine");
|
|
5
|
+
|
|
6
|
+
router.use(authenticate, requireManagerOrAdmin);
|
|
7
|
+
|
|
8
|
+
// ── List agents for a workspace ───────────────────────────────────────────────
|
|
9
|
+
router.get("/workspaces/:workspaceId/agents", async (req, res) => {
|
|
10
|
+
try {
|
|
11
|
+
const workspaceId = parseInt(req.params.workspaceId);
|
|
12
|
+
const agents = await req.db.agent.findMany({
|
|
13
|
+
where: { workspaceId },
|
|
14
|
+
orderBy: { createdAt: "asc" },
|
|
15
|
+
include: {
|
|
16
|
+
runs: {
|
|
17
|
+
orderBy: { startedAt: "desc" },
|
|
18
|
+
take: 1,
|
|
19
|
+
select: { status: true, startedAt: true, completedAt: true },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
res.json({ agents });
|
|
24
|
+
} catch (err) {
|
|
25
|
+
res.status(500).json({ error: err.message });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ── Create agent ──────────────────────────────────────────────────────────────
|
|
30
|
+
router.post("/workspaces/:workspaceId/agents", async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const workspaceId = parseInt(req.params.workspaceId);
|
|
33
|
+
const { name, description, systemPrompt, workflow, connectorIds } = req.body;
|
|
34
|
+
if (!name?.trim()) return res.status(400).json({ error: "Name required" });
|
|
35
|
+
const agent = await req.db.agent.create({
|
|
36
|
+
data: {
|
|
37
|
+
workspaceId,
|
|
38
|
+
name: name.trim(),
|
|
39
|
+
description: description?.trim() || null,
|
|
40
|
+
systemPrompt: systemPrompt?.trim() || null,
|
|
41
|
+
workflow: typeof workflow === "string" ? workflow : JSON.stringify(workflow || []),
|
|
42
|
+
connectorIds: typeof connectorIds === "string" ? connectorIds : JSON.stringify(connectorIds || []),
|
|
43
|
+
createdByUserId: req.user.id || null,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
res.json({ agent });
|
|
47
|
+
} catch (err) {
|
|
48
|
+
res.status(500).json({ error: err.message });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ── Update agent ──────────────────────────────────────────────────────────────
|
|
53
|
+
router.put("/workspaces/:workspaceId/agents/:id", async (req, res) => {
|
|
54
|
+
try {
|
|
55
|
+
const id = parseInt(req.params.id);
|
|
56
|
+
const { name, description, systemPrompt, workflow, connectorIds, enabled } = req.body;
|
|
57
|
+
const data = {};
|
|
58
|
+
if (name !== undefined) data.name = name.trim();
|
|
59
|
+
if (description !== undefined) data.description = description?.trim() || null;
|
|
60
|
+
if (systemPrompt !== undefined) data.systemPrompt = systemPrompt?.trim() || null;
|
|
61
|
+
if (workflow !== undefined) data.workflow = typeof workflow === "string" ? workflow : JSON.stringify(workflow || []);
|
|
62
|
+
if (connectorIds !== undefined) data.connectorIds = typeof connectorIds === "string" ? connectorIds : JSON.stringify(connectorIds || []);
|
|
63
|
+
if (enabled !== undefined) data.enabled = enabled;
|
|
64
|
+
const agent = await req.db.agent.update({ where: { id }, data });
|
|
65
|
+
res.json({ agent });
|
|
66
|
+
} catch (err) {
|
|
67
|
+
res.status(500).json({ error: err.message });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ── Delete agent ──────────────────────────────────────────────────────────────
|
|
72
|
+
router.delete("/workspaces/:workspaceId/agents/:id", async (req, res) => {
|
|
73
|
+
try {
|
|
74
|
+
const id = parseInt(req.params.id);
|
|
75
|
+
await req.db.agent.delete({ where: { id } });
|
|
76
|
+
res.json({ success: true });
|
|
77
|
+
} catch (err) {
|
|
78
|
+
res.status(500).json({ error: err.message });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ── Run history ───────────────────────────────────────────────────────────────
|
|
83
|
+
router.get("/workspaces/:workspaceId/agents/:id/runs", async (req, res) => {
|
|
84
|
+
try {
|
|
85
|
+
const agentId = parseInt(req.params.id);
|
|
86
|
+
const runs = await req.db.agentRun.findMany({
|
|
87
|
+
where: { agentId },
|
|
88
|
+
orderBy: { startedAt: "desc" },
|
|
89
|
+
take: 10,
|
|
90
|
+
});
|
|
91
|
+
res.json({ runs });
|
|
92
|
+
} catch (err) {
|
|
93
|
+
res.status(500).json({ error: err.message });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── Cancel a run ──────────────────────────────────────────────────────────────
|
|
98
|
+
router.post("/workspaces/:workspaceId/agents/:id/runs/:runId/cancel", async (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
const runId = parseInt(req.params.runId);
|
|
101
|
+
await req.db.$executeRaw`UPDATE AgentRun SET cancelRequested = 1 WHERE id = ${runId}`;
|
|
102
|
+
res.json({ success: true });
|
|
103
|
+
} catch (err) {
|
|
104
|
+
res.status(500).json({ error: err.message });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ── Run agent (SSE) ───────────────────────────────────────────────────────────
|
|
109
|
+
router.post("/workspaces/:workspaceId/agents/:id/run", async (req, res) => {
|
|
110
|
+
const agentId = parseInt(req.params.id);
|
|
111
|
+
const workspaceId = parseInt(req.params.workspaceId);
|
|
112
|
+
const { input } = req.body;
|
|
113
|
+
|
|
114
|
+
const [agent, workspace] = await Promise.all([
|
|
115
|
+
req.db.agent.findUnique({ where: { id: agentId } }),
|
|
116
|
+
req.db.workspace.findUnique({ where: { id: workspaceId } }),
|
|
117
|
+
]);
|
|
118
|
+
if (!agent) return res.status(404).json({ error: "Agent not found" });
|
|
119
|
+
if (!workspace) return res.status(404).json({ error: "Workspace not found" });
|
|
120
|
+
|
|
121
|
+
// Build connector list from workspace agentConfig (inline credentials)
|
|
122
|
+
const agentConfig = (() => { try { return JSON.parse(workspace.agentConfig || "{}"); } catch { return {}; } })();
|
|
123
|
+
const allConnectors = (agentConfig.connectors || []).map((c, i) => ({
|
|
124
|
+
id: i + 1,
|
|
125
|
+
name: c.connection_name,
|
|
126
|
+
type: c.connection_type,
|
|
127
|
+
...c, // spread all credential fields (bearerToken, baseUrl, host, password, etc.)
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
// Filter to connectors the agent declared
|
|
131
|
+
const referencedNames = (() => { try { return JSON.parse(agent.connectorIds || "[]"); } catch { return []; } })();
|
|
132
|
+
const connectors = referencedNames.length
|
|
133
|
+
? allConnectors.filter(c => referencedNames.includes(c.name))
|
|
134
|
+
: allConnectors; // if agent declares none, give it all
|
|
135
|
+
|
|
136
|
+
const run = await req.db.agentRun.create({
|
|
137
|
+
data: {
|
|
138
|
+
agentId,
|
|
139
|
+
triggeredByUserId: req.user.id || null,
|
|
140
|
+
status: "running",
|
|
141
|
+
input: input || null,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
146
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
147
|
+
res.setHeader("Connection", "keep-alive");
|
|
148
|
+
|
|
149
|
+
const { executeTool } = require("../utils/tools/registry");
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const llmConfig = await getLLMConfig();
|
|
153
|
+
const steps = (() => { try { return JSON.parse(agent.workflow || "[]"); } catch { return []; } })();
|
|
154
|
+
|
|
155
|
+
await engine.run(
|
|
156
|
+
{
|
|
157
|
+
systemPrompt: agent.systemPrompt,
|
|
158
|
+
workflow: steps,
|
|
159
|
+
params: [],
|
|
160
|
+
maxRounds: 25,
|
|
161
|
+
input,
|
|
162
|
+
},
|
|
163
|
+
llmConfig,
|
|
164
|
+
connectors,
|
|
165
|
+
{
|
|
166
|
+
toolExecutor: (name, args, conns) => executeTool(name, args, conns, req.db),
|
|
167
|
+
onToolCall: (name) => res.write(`data: ${JSON.stringify({ tool_call: name })}\n\n`),
|
|
168
|
+
checkCancel: async () => {
|
|
169
|
+
const [cr] = await req.db.$queryRaw`SELECT cancelRequested FROM AgentRun WHERE id = ${run.id}`;
|
|
170
|
+
return cr?.cancelRequested;
|
|
171
|
+
},
|
|
172
|
+
onDone: async (output) => {
|
|
173
|
+
await req.db.agentRun.update({ where: { id: run.id }, data: { status: "success", output, completedAt: new Date() } });
|
|
174
|
+
res.write(`data: ${JSON.stringify({ done: true, output, runId: run.id })}\n\n`);
|
|
175
|
+
res.end();
|
|
176
|
+
},
|
|
177
|
+
onError: async (err) => {
|
|
178
|
+
await req.db.agentRun.update({ where: { id: run.id }, data: { status: "error", error: err.message, completedAt: new Date() } });
|
|
179
|
+
res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
|
|
180
|
+
res.end();
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (!res.writableEnded) {
|
|
186
|
+
await req.db.agentRun.update({ where: { id: run.id }, data: { status: "error", error: err.message, completedAt: new Date() } }).catch(() => {});
|
|
187
|
+
res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
|
|
188
|
+
res.end();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
module.exports = router;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const crypto = require("crypto");
|
|
3
|
+
const { authenticate, requireManagerOrAdmin } = require("../middleware/auth");
|
|
4
|
+
const { logActivity } = require("../utils/activityLog");
|
|
5
|
+
|
|
6
|
+
router.use(authenticate, requireManagerOrAdmin);
|
|
7
|
+
|
|
8
|
+
// List all API keys (never returns raw key)
|
|
9
|
+
router.get("/", async (req, res) => {
|
|
10
|
+
const keys = await req.db.apiKey.findMany({
|
|
11
|
+
orderBy: { createdAt: "desc" },
|
|
12
|
+
include: { createdBy: { select: { id: true, name: true, email: true } } }
|
|
13
|
+
});
|
|
14
|
+
res.json({ keys });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Create new API key — returns raw key ONCE
|
|
18
|
+
router.post("/", async (req, res) => {
|
|
19
|
+
const { name } = req.body;
|
|
20
|
+
if (!name?.trim()) return res.status(400).json({ error: "Name required" });
|
|
21
|
+
|
|
22
|
+
const rawKey = "emb_" + crypto.randomBytes(16).toString("hex");
|
|
23
|
+
const keyHash = crypto.createHash("sha256").update(rawKey).digest("hex");
|
|
24
|
+
const keyPrefix = rawKey.slice(0, 12);
|
|
25
|
+
|
|
26
|
+
const apiKey = await req.db.apiKey.create({
|
|
27
|
+
data: { name: name.trim(), keyHash, keyPrefix, createdById: req.user.id || null },
|
|
28
|
+
include: { createdBy: { select: { id: true, name: true, email: true } } }
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await logActivity(req.db, req.user, "apikey.created", { name: name.trim(), prefix: keyPrefix });
|
|
32
|
+
res.json({ apiKey, rawKey });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Revoke an API key (soft)
|
|
36
|
+
router.patch("/:id/revoke", async (req, res) => {
|
|
37
|
+
const apiKey = await req.db.apiKey.findUnique({ where: { id: parseInt(req.params.id) } });
|
|
38
|
+
if (!apiKey) return res.status(404).json({ error: "API key not found" });
|
|
39
|
+
|
|
40
|
+
await req.db.apiKey.update({ where: { id: apiKey.id }, data: { revoked: true } });
|
|
41
|
+
await logActivity(req.db, req.user, "apikey.revoked", { name: apiKey.name, prefix: apiKey.keyPrefix });
|
|
42
|
+
res.json({ success: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Delete an API key (hard)
|
|
46
|
+
router.delete("/:id", async (req, res) => {
|
|
47
|
+
const apiKey = await req.db.apiKey.findUnique({ where: { id: parseInt(req.params.id) } });
|
|
48
|
+
if (!apiKey) return res.status(404).json({ error: "API key not found" });
|
|
49
|
+
|
|
50
|
+
await req.db.apiKey.delete({ where: { id: apiKey.id } });
|
|
51
|
+
await logActivity(req.db, req.user, "apikey.deleted", { name: apiKey.name, prefix: apiKey.keyPrefix });
|
|
52
|
+
res.json({ success: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
module.exports = router;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const multer = require("multer");
|
|
3
|
+
const { authenticate } = require("../middleware/auth");
|
|
4
|
+
const { getSetting } = require("../providers/llm");
|
|
5
|
+
|
|
6
|
+
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024 } });
|
|
7
|
+
|
|
8
|
+
// ── TTS ───────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
router.post("/tts", authenticate, async (req, res) => {
|
|
11
|
+
const { text } = req.body;
|
|
12
|
+
if (!text) return res.status(400).json({ error: "text required" });
|
|
13
|
+
|
|
14
|
+
const provider = (await getSetting("tts_provider")) || "native";
|
|
15
|
+
const safeText = text.slice(0, 4096);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
if (provider === "elevenlabs") {
|
|
19
|
+
const apiKey = await getSetting("tts_elevenlabs_key");
|
|
20
|
+
const voiceId = (await getSetting("tts_elevenlabs_voice_id")) || "21m00Tcm4TlvDq8ikWAM";
|
|
21
|
+
if (!apiKey) return res.status(400).json({ error: "ElevenLabs API key not configured" });
|
|
22
|
+
|
|
23
|
+
const resp = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "xi-api-key": apiKey, "Content-Type": "application/json", Accept: "audio/mpeg" },
|
|
26
|
+
body: JSON.stringify({ text: safeText, model_id: "eleven_monolingual_v1" })
|
|
27
|
+
});
|
|
28
|
+
if (!resp.ok) throw new Error(`ElevenLabs error: ${resp.status}`);
|
|
29
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
30
|
+
res.setHeader("Content-Type", "audio/mpeg");
|
|
31
|
+
return res.send(buf);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// OpenAI or generic-openai
|
|
35
|
+
const { default: OpenAI } = await import("openai");
|
|
36
|
+
let client;
|
|
37
|
+
|
|
38
|
+
if (provider === "generic-openai") {
|
|
39
|
+
const baseURL = await getSetting("tts_generic_base_url");
|
|
40
|
+
const apiKey = (await getSetting("tts_generic_api_key")) || "none";
|
|
41
|
+
if (!baseURL) return res.status(400).json({ error: "Generic TTS base URL not configured" });
|
|
42
|
+
client = new OpenAI({ apiKey, baseURL });
|
|
43
|
+
} else {
|
|
44
|
+
const apiKey = (await getSetting("llm_api_key")) || process.env.OPENAI_API_KEY;
|
|
45
|
+
if (!apiKey) return res.status(400).json({ error: "OpenAI API key not configured" });
|
|
46
|
+
client = new OpenAI({ apiKey });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const voice = (await getSetting("tts_voice")) || "alloy";
|
|
50
|
+
const model = (await getSetting("tts_model")) || "tts-1";
|
|
51
|
+
const gVoice = provider === "generic-openai" ? ((await getSetting("tts_generic_voice")) || "alloy") : voice;
|
|
52
|
+
|
|
53
|
+
const mp3 = await client.audio.speech.create({ model, voice: gVoice, input: safeText });
|
|
54
|
+
const buf = Buffer.from(await mp3.arrayBuffer());
|
|
55
|
+
res.setHeader("Content-Type", "audio/mpeg");
|
|
56
|
+
res.setHeader("Content-Length", buf.length);
|
|
57
|
+
res.send(buf);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error("[TTS]", err.message);
|
|
60
|
+
res.status(500).json({ error: "TTS failed: " + err.message });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ── STT ───────────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
router.post("/stt", authenticate, upload.single("audio"), async (req, res) => {
|
|
67
|
+
if (!req.file) return res.status(400).json({ error: "audio file required" });
|
|
68
|
+
|
|
69
|
+
const provider = (await getSetting("stt_provider")) || "native";
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
if (provider === "deepgram") {
|
|
73
|
+
const apiKey = await getSetting("stt_deepgram_key");
|
|
74
|
+
if (!apiKey) return res.status(400).json({ error: "Deepgram API key not configured" });
|
|
75
|
+
|
|
76
|
+
const resp = await fetch("https://api.deepgram.com/v1/listen?model=nova-2&smart_format=true", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Authorization": `Token ${apiKey}`, "Content-Type": req.file.mimetype || "audio/webm" },
|
|
79
|
+
body: req.file.buffer
|
|
80
|
+
});
|
|
81
|
+
if (!resp.ok) throw new Error(`Deepgram error: ${resp.status}`);
|
|
82
|
+
const data = await resp.json();
|
|
83
|
+
const transcript = data.results?.channels?.[0]?.alternatives?.[0]?.transcript || "";
|
|
84
|
+
return res.json({ transcript });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// OpenAI Whisper or generic-openai
|
|
88
|
+
const { default: OpenAI } = await import("openai");
|
|
89
|
+
let client;
|
|
90
|
+
|
|
91
|
+
if (provider === "generic-openai") {
|
|
92
|
+
const baseURL = await getSetting("stt_generic_base_url");
|
|
93
|
+
const apiKey = (await getSetting("stt_generic_api_key")) || "none";
|
|
94
|
+
if (!baseURL) return res.status(400).json({ error: "Generic STT base URL not configured" });
|
|
95
|
+
client = new OpenAI({ apiKey, baseURL });
|
|
96
|
+
} else {
|
|
97
|
+
const apiKey = (await getSetting("llm_api_key")) || process.env.OPENAI_API_KEY;
|
|
98
|
+
if (!apiKey) return res.status(400).json({ error: "OpenAI API key not configured" });
|
|
99
|
+
client = new OpenAI({ apiKey });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const audioFile = new File([req.file.buffer], "audio.webm", { type: req.file.mimetype || "audio/webm" });
|
|
103
|
+
const result = await client.audio.transcriptions.create({ file: audioFile, model: "whisper-1" });
|
|
104
|
+
res.json({ transcript: result.text });
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error("[STT]", err.message);
|
|
107
|
+
res.status(500).json({ error: "STT failed: " + err.message });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
module.exports = router;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const bcrypt = require("bcryptjs");
|
|
3
|
+
const jwt = require("jsonwebtoken");
|
|
4
|
+
const { authenticate } = require("../middleware/auth");
|
|
5
|
+
|
|
6
|
+
const SUPER_ADMIN = {
|
|
7
|
+
email: process.env.SUPER_ADMIN_EMAIL,
|
|
8
|
+
password: process.env.SUPER_ADMIN_PASSWORD,
|
|
9
|
+
name: process.env.SUPER_ADMIN_NAME || "Super Admin",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
router.post("/login", async (req, res) => {
|
|
13
|
+
const { email, password } = req.body;
|
|
14
|
+
if (!email || !password) return res.status(400).json({ error: "Email and password required" });
|
|
15
|
+
|
|
16
|
+
// Super admin from .env — bypasses DB entirely
|
|
17
|
+
if (SUPER_ADMIN.email && SUPER_ADMIN.password && email.toLowerCase() === SUPER_ADMIN.email.toLowerCase() && password === SUPER_ADMIN.password) {
|
|
18
|
+
const token = jwt.sign(
|
|
19
|
+
{ id: 0, email: SUPER_ADMIN.email, role: "admin", name: SUPER_ADMIN.name },
|
|
20
|
+
process.env.JWT_SECRET,
|
|
21
|
+
{ expiresIn: "7d" }
|
|
22
|
+
);
|
|
23
|
+
return res.json({ token, user: { id: 0, email: SUPER_ADMIN.email, name: SUPER_ADMIN.name, role: "admin" } });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const user = await req.db.user.findUnique({ where: { email: email.toLowerCase() } });
|
|
27
|
+
if (!user || user.suspended) return res.status(401).json({ error: "Invalid credentials" });
|
|
28
|
+
|
|
29
|
+
const valid = await bcrypt.compare(password, user.password);
|
|
30
|
+
if (!valid) return res.status(401).json({ error: "Invalid credentials" });
|
|
31
|
+
|
|
32
|
+
const token = jwt.sign(
|
|
33
|
+
{ id: user.id, email: user.email, role: user.role, name: user.name },
|
|
34
|
+
process.env.JWT_SECRET,
|
|
35
|
+
{ expiresIn: "7d" }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
res.json({ token, user: { id: user.id, email: user.email, name: user.name, role: user.role } });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
router.get("/me", authenticate, async (req, res) => {
|
|
42
|
+
if (req.user.id === 0) {
|
|
43
|
+
return res.json({ user: { id: 0, email: SUPER_ADMIN.email, name: SUPER_ADMIN.name, role: "admin" } });
|
|
44
|
+
}
|
|
45
|
+
const user = await req.db.user.findUnique({
|
|
46
|
+
where: { id: req.user.id },
|
|
47
|
+
select: { id: true, email: true, name: true, role: true }
|
|
48
|
+
});
|
|
49
|
+
res.json({ user });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
router.put("/me", authenticate, async (req, res) => {
|
|
53
|
+
const { name, currentPassword, newPassword } = req.body;
|
|
54
|
+
|
|
55
|
+
// Super admin — only allow name update (password lives in .env)
|
|
56
|
+
if (req.user.id === 0) {
|
|
57
|
+
return res.status(400).json({ error: "Super admin credentials are managed via .env" });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const user = await req.db.user.findUnique({ where: { id: req.user.id } });
|
|
61
|
+
if (!user) return res.status(404).json({ error: "User not found" });
|
|
62
|
+
|
|
63
|
+
const updates = {};
|
|
64
|
+
if (name !== undefined) updates.name = name.trim();
|
|
65
|
+
|
|
66
|
+
if (newPassword) {
|
|
67
|
+
if (!currentPassword) return res.status(400).json({ error: "Current password required" });
|
|
68
|
+
const valid = await bcrypt.compare(currentPassword, user.password);
|
|
69
|
+
if (!valid) return res.status(400).json({ error: "Current password is incorrect" });
|
|
70
|
+
if (newPassword.length < 6) return res.status(400).json({ error: "New password must be at least 6 characters" });
|
|
71
|
+
updates.password = await bcrypt.hash(newPassword, 10);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const updated = await req.db.user.update({
|
|
75
|
+
where: { id: req.user.id },
|
|
76
|
+
data: updates,
|
|
77
|
+
select: { id: true, email: true, name: true, role: true }
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
res.json({ user: updated });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
module.exports = router;
|