@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,81 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const { authenticate } = require("../middleware/auth");
|
|
3
|
+
const { PrismaClient } = require("@prisma/client");
|
|
4
|
+
const db = new PrismaClient();
|
|
5
|
+
|
|
6
|
+
const STATIC_MODELS = {
|
|
7
|
+
openai: ["gpt-4o","gpt-4o-mini","gpt-4-turbo","gpt-4-turbo-preview","gpt-4","gpt-3.5-turbo","o1","o1-mini","o1-preview","o3-mini"],
|
|
8
|
+
anthropic: ["claude-opus-4-8","claude-sonnet-4-6","claude-3-5-sonnet-20241022","claude-3-5-haiku-20241022","claude-3-opus-20240229","claude-3-sonnet-20240229","claude-3-haiku-20240307"],
|
|
9
|
+
gemini: ["gemini-2.0-flash","gemini-2.0-flash-lite","gemini-2.0-pro-exp","gemini-1.5-pro","gemini-1.5-flash","gemini-1.5-flash-8b"],
|
|
10
|
+
groq: ["llama-3.3-70b-versatile","llama-3.1-70b-versatile","llama-3.1-8b-instant","llama3-70b-8192","llama3-8b-8192","mixtral-8x7b-32768","gemma2-9b-it","gemma-7b-it"],
|
|
11
|
+
mistral: ["mistral-large-latest","mistral-medium-latest","mistral-small-latest","open-mistral-nemo","open-mixtral-8x22b","open-mixtral-8x7b","codestral-latest","ministral-8b-latest","ministral-3b-latest"],
|
|
12
|
+
deepseek: ["deepseek-chat","deepseek-reasoner"],
|
|
13
|
+
perplexity: ["llama-3.1-sonar-huge-128k-online","llama-3.1-sonar-large-128k-online","llama-3.1-sonar-small-128k-online","llama-3.1-8b-instruct","llama-3.1-70b-instruct"],
|
|
14
|
+
xai: ["grok-2-latest","grok-2-vision-latest","grok-beta","grok-vision-beta"],
|
|
15
|
+
togetherai: ["meta-llama/Llama-3.3-70B-Instruct-Turbo","meta-llama/Llama-3.1-405B-Instruct-Turbo","meta-llama/Llama-3.1-70B-Instruct-Turbo","meta-llama/Llama-3.1-8B-Instruct-Turbo","meta-llama/Llama-3-70b-chat-hf","mistralai/Mixtral-8x7B-Instruct-v0.1","mistralai/Mistral-7B-Instruct-v0.3","Qwen/Qwen2.5-72B-Instruct-Turbo","deepseek-ai/DeepSeek-R1","deepseek-ai/DeepSeek-V3"],
|
|
16
|
+
fireworks: ["accounts/fireworks/models/llama-v3p3-70b-instruct","accounts/fireworks/models/llama-v3p1-405b-instruct","accounts/fireworks/models/llama-v3p1-70b-instruct","accounts/fireworks/models/llama-v3p1-8b-instruct","accounts/fireworks/models/mixtral-8x7b-instruct","accounts/fireworks/models/deepseek-r1","accounts/fireworks/models/deepseek-v3","accounts/fireworks/models/qwen2p5-72b-instruct"],
|
|
17
|
+
openrouter: ["openai/gpt-4o","openai/gpt-4o-mini","openai/o1","anthropic/claude-3.5-sonnet","anthropic/claude-3-opus","google/gemini-2.0-flash-exp","google/gemini-pro-1.5","meta-llama/llama-3.3-70b-instruct","deepseek/deepseek-r1","deepseek/deepseek-chat","mistralai/mistral-large-2411","x-ai/grok-2-1212","x-ai/grok-beta"],
|
|
18
|
+
nvidiaNim: ["meta/llama-3.3-70b-instruct","meta/llama-3.1-405b-instruct","meta/llama-3.1-70b-instruct","meta/llama-3.1-8b-instruct","microsoft/phi-3-mini-128k-instruct","mistralai/mistral-large","mistralai/mixtral-8x22b-instruct-v0.1","google/gemma-2-27b-it"],
|
|
19
|
+
sambanova: ["Meta-Llama-3.3-70B-Instruct","Meta-Llama-3.1-405B-Instruct","Meta-Llama-3.1-70B-Instruct","Meta-Llama-3.1-8B-Instruct","Qwen2.5-72B-Instruct","Qwen2.5-Coder-32B-Instruct","DeepSeek-R1","DeepSeek-V3"],
|
|
20
|
+
ollama: ["llama3.2","llama3.1","llama3","mistral","mixtral","phi3","phi4","gemma2","qwen2.5","deepseek-r1","codellama","nomic-embed-text"],
|
|
21
|
+
lmstudio: [],
|
|
22
|
+
"generic-openai": [],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
async function getStoredApiKey() {
|
|
26
|
+
const s = await db.setting.findUnique({ where: { key: "llm_api_key" } });
|
|
27
|
+
return s?.value || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function getStoredBaseUrl() {
|
|
31
|
+
const s = await db.setting.findUnique({ where: { key: "llm_base_url" } });
|
|
32
|
+
return s?.value || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
router.get("/:provider", authenticate, async (req, res) => {
|
|
36
|
+
const { provider } = req.params;
|
|
37
|
+
let { apiKey, baseUrl } = req.query;
|
|
38
|
+
|
|
39
|
+
// Masked value from admin settings — use the DB key instead
|
|
40
|
+
if (apiKey === "********" || !apiKey) apiKey = null;
|
|
41
|
+
if (!baseUrl) baseUrl = null;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// OpenAI — try live fetch with provided or stored key
|
|
45
|
+
if (provider === "openai") {
|
|
46
|
+
const key = apiKey || await getStoredApiKey();
|
|
47
|
+
if (key) {
|
|
48
|
+
try {
|
|
49
|
+
const { default: OpenAI } = await import("openai");
|
|
50
|
+
const client = new OpenAI({ apiKey: key });
|
|
51
|
+
const list = await client.models.list();
|
|
52
|
+
const models = list.data
|
|
53
|
+
.filter(m => m.id.startsWith("gpt-") || m.id.startsWith("o1") || m.id.startsWith("o3"))
|
|
54
|
+
.sort((a, b) => b.created - a.created)
|
|
55
|
+
.map(m => m.id);
|
|
56
|
+
if (models.length) return res.json({ models });
|
|
57
|
+
} catch { /* fall through to static */ }
|
|
58
|
+
}
|
|
59
|
+
return res.json({ models: STATIC_MODELS.openai });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Ollama — try live fetch from local server
|
|
63
|
+
if (provider === "ollama") {
|
|
64
|
+
const url = (baseUrl || await getStoredBaseUrl() || "http://localhost:11434").replace(/\/v1\/?$/, "");
|
|
65
|
+
try {
|
|
66
|
+
const r = await fetch(`${url}/api/tags`, { signal: AbortSignal.timeout(3000) });
|
|
67
|
+
const json = await r.json();
|
|
68
|
+
const models = (json.models || []).map(m => m.name);
|
|
69
|
+
if (models.length) return res.json({ models });
|
|
70
|
+
} catch { /* fall through to static */ }
|
|
71
|
+
return res.json({ models: STATIC_MODELS.ollama });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// All other providers — return static list
|
|
75
|
+
res.json({ models: STATIC_MODELS[provider] || [] });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
res.json({ models: STATIC_MODELS[provider] || [], error: err.message });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
module.exports = router;
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const { authenticate, requireManagerOrAdmin } = require("../middleware/auth");
|
|
3
|
+
const { makeOAuth2Client, getGoogleCredentials } = require("../utils/tools/adapters/gmail");
|
|
4
|
+
|
|
5
|
+
const GMAIL_SCOPES = [
|
|
6
|
+
"https://www.googleapis.com/auth/gmail.send",
|
|
7
|
+
"https://www.googleapis.com/auth/gmail.readonly",
|
|
8
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const GDRIVE_SCOPES = [
|
|
12
|
+
"https://www.googleapis.com/auth/drive",
|
|
13
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000";
|
|
17
|
+
const CALLBACK_BASE = process.env.OAUTH_CALLBACK_BASE || "http://localhost:3001";
|
|
18
|
+
|
|
19
|
+
function generateSlug(name) {
|
|
20
|
+
return (name || "").toLowerCase().replace(/[^a-z0-9]/g, "") || "connector";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function uniqueSlug(db, base) {
|
|
24
|
+
if (!base) base = "connector";
|
|
25
|
+
let slug = base, suffix = 2;
|
|
26
|
+
while (await db.connector.findUnique({ where: { slug } })) slug = `${base}${suffix++}`;
|
|
27
|
+
return slug;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function uniqueName(db, base) {
|
|
31
|
+
if (!base) base = "connector";
|
|
32
|
+
let name = base, suffix = 1;
|
|
33
|
+
while (await db.connector.findFirst({ where: { name } })) name = `${base}-${suffix++}`;
|
|
34
|
+
return name;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function workspaceRedirect(db, workspaceId, type, success) {
|
|
38
|
+
try {
|
|
39
|
+
const ws = await db.workspace.findUnique({ where: { id: workspaceId }, select: { slug: true } });
|
|
40
|
+
if (ws?.slug) return `${FRONTEND_URL}/workspace/${ws.slug}?oauth_${success ? "success" : "error"}=${encodeURIComponent(type)}&ws=${workspaceId}`;
|
|
41
|
+
} catch {}
|
|
42
|
+
return `${FRONTEND_URL}?oauth_${success ? "success" : "error"}=${encodeURIComponent(type)}&ws=${workspaceId}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if Google OAuth is configured for a workspace
|
|
46
|
+
router.get("/gmail/status", authenticate, async (req, res) => {
|
|
47
|
+
const workspaceId = parseInt(req.query.workspaceId);
|
|
48
|
+
const { clientId } = await getGoogleCredentials(req.db, workspaceId || undefined);
|
|
49
|
+
res.json({ configured: !!clientId });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Save per-workspace Google OAuth credentials
|
|
53
|
+
router.post("/gmail/configure", authenticate, requireManagerOrAdmin, async (req, res) => {
|
|
54
|
+
const { clientId, clientSecret, workspaceId } = req.body;
|
|
55
|
+
if (!clientId?.trim() || !clientSecret?.trim() || !workspaceId) {
|
|
56
|
+
return res.status(400).json({ error: "Client ID, Client Secret and workspaceId are required" });
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const wsId = parseInt(workspaceId);
|
|
60
|
+
await req.db.setting.upsert({
|
|
61
|
+
where: { key: `oauth.google.clientId.ws.${wsId}` },
|
|
62
|
+
create: { key: `oauth.google.clientId.ws.${wsId}`, value: clientId.trim() },
|
|
63
|
+
update: { value: clientId.trim() },
|
|
64
|
+
});
|
|
65
|
+
await req.db.setting.upsert({
|
|
66
|
+
where: { key: `oauth.google.clientSecret.ws.${wsId}` },
|
|
67
|
+
create: { key: `oauth.google.clientSecret.ws.${wsId}`, value: clientSecret.trim() },
|
|
68
|
+
update: { value: clientSecret.trim() },
|
|
69
|
+
});
|
|
70
|
+
res.json({ success: true });
|
|
71
|
+
} catch (err) {
|
|
72
|
+
res.status(500).json({ error: err.message });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Delete per-workspace Google OAuth credentials
|
|
77
|
+
router.delete("/gmail/configure", authenticate, requireManagerOrAdmin, async (req, res) => {
|
|
78
|
+
const workspaceId = parseInt(req.query.workspaceId);
|
|
79
|
+
if (!workspaceId) return res.status(400).json({ error: "workspaceId required" });
|
|
80
|
+
try {
|
|
81
|
+
await req.db.setting.deleteMany({
|
|
82
|
+
where: { key: { in: [
|
|
83
|
+
`oauth.google.clientId.ws.${workspaceId}`,
|
|
84
|
+
`oauth.google.clientSecret.ws.${workspaceId}`,
|
|
85
|
+
]}}
|
|
86
|
+
});
|
|
87
|
+
res.json({ success: true });
|
|
88
|
+
} catch (err) {
|
|
89
|
+
res.status(500).json({ error: err.message });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Returns the Google OAuth URL — frontend calls this via axios then does window.location.href
|
|
94
|
+
router.get("/gmail/start", authenticate, requireManagerOrAdmin, async (req, res) => {
|
|
95
|
+
const { workspaceId, connectionName, connectionSlug } = req.query;
|
|
96
|
+
if (!workspaceId) return res.status(400).json({ error: "workspaceId required" });
|
|
97
|
+
|
|
98
|
+
const wsId = parseInt(workspaceId);
|
|
99
|
+
const { clientId } = await getGoogleCredentials(req.db, wsId);
|
|
100
|
+
if (!clientId) return res.status(400).json({ error: "Google Client ID not configured. Add it in the Integrations tab." });
|
|
101
|
+
|
|
102
|
+
const state = Buffer.from(JSON.stringify({ workspaceId: wsId, connectionName: connectionName || "", connectionSlug: connectionSlug || "" })).toString("base64url");
|
|
103
|
+
const oauth2 = await makeOAuth2Client(req.db, wsId);
|
|
104
|
+
const url = oauth2.generateAuthUrl({
|
|
105
|
+
access_type: "offline",
|
|
106
|
+
prompt: "consent",
|
|
107
|
+
scope: GMAIL_SCOPES,
|
|
108
|
+
state,
|
|
109
|
+
});
|
|
110
|
+
res.json({ url });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Gmail OAuth callback — exchange code, store connector, redirect to frontend
|
|
114
|
+
router.get("/gmail/callback", async (req, res) => {
|
|
115
|
+
const { code, state, error } = req.query;
|
|
116
|
+
|
|
117
|
+
if (error) return res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(error)}`);
|
|
118
|
+
if (!code || !state) return res.redirect(`${FRONTEND_URL}?oauth_error=missing_params`);
|
|
119
|
+
|
|
120
|
+
let workspaceId, connectionName, connectionSlug;
|
|
121
|
+
try {
|
|
122
|
+
({ workspaceId, connectionName, connectionSlug } = JSON.parse(Buffer.from(state, "base64url").toString()));
|
|
123
|
+
} catch {
|
|
124
|
+
return res.redirect(`${FRONTEND_URL}?oauth_error=invalid_state`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const oauth2 = await makeOAuth2Client(req.db, workspaceId);
|
|
129
|
+
const { tokens } = await oauth2.getToken(code);
|
|
130
|
+
oauth2.setCredentials(tokens);
|
|
131
|
+
|
|
132
|
+
// Get the user's Gmail address
|
|
133
|
+
const { google } = require("googleapis");
|
|
134
|
+
const oauth2Api = google.oauth2({ version: "v2", auth: oauth2 });
|
|
135
|
+
const { data: userInfo } = await oauth2Api.userinfo.get();
|
|
136
|
+
const email = userInfo.email;
|
|
137
|
+
|
|
138
|
+
const db = req.db || require("../utils/prisma");
|
|
139
|
+
|
|
140
|
+
// Upsert: one Gmail connector per workspace per email
|
|
141
|
+
const existing = await db.connector.findFirst({
|
|
142
|
+
where: { workspaceId, type: "gmail", name: email }
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const authConfig = JSON.stringify({
|
|
146
|
+
accessToken: tokens.access_token,
|
|
147
|
+
refreshToken: tokens.refresh_token,
|
|
148
|
+
expiresAt: tokens.expiry_date,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const connName = connectionName?.trim() || email;
|
|
152
|
+
if (existing) {
|
|
153
|
+
await db.connector.update({
|
|
154
|
+
where: { id: existing.id },
|
|
155
|
+
data: { authConfig, status: "active", lastTestedAt: new Date() }
|
|
156
|
+
});
|
|
157
|
+
} else {
|
|
158
|
+
const finalName = await uniqueName(db, connName);
|
|
159
|
+
await db.connector.create({
|
|
160
|
+
data: {
|
|
161
|
+
workspaceId,
|
|
162
|
+
name: finalName,
|
|
163
|
+
type: "gmail",
|
|
164
|
+
slug: finalName,
|
|
165
|
+
config: JSON.stringify({ email }),
|
|
166
|
+
authConfig,
|
|
167
|
+
status: "active",
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
res.redirect(await workspaceRedirect(req.db, workspaceId, "gmail", true));
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.error("[oauth/gmail] callback error:", err.message);
|
|
175
|
+
res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(err.message)}`);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ── Google Drive OAuth ──────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
router.get("/gdrive/start", authenticate, requireManagerOrAdmin, async (req, res) => {
|
|
182
|
+
const { workspaceId, connectionName, connectionSlug } = req.query;
|
|
183
|
+
if (!workspaceId) return res.status(400).json({ error: "workspaceId required" });
|
|
184
|
+
|
|
185
|
+
const wsId = parseInt(workspaceId);
|
|
186
|
+
const { clientId } = await getGoogleCredentials(req.db, wsId);
|
|
187
|
+
if (!clientId) return res.status(400).json({ error: "Google Client ID not configured. Add it in the Gmail integration tab first." });
|
|
188
|
+
|
|
189
|
+
const state = Buffer.from(JSON.stringify({ workspaceId: wsId, connectionName: connectionName || "", connectionSlug: connectionSlug || "" })).toString("base64url");
|
|
190
|
+
const oauth2 = await makeOAuth2Client(req.db, wsId, "/api/oauth/gdrive/callback");
|
|
191
|
+
const url = oauth2.generateAuthUrl({
|
|
192
|
+
access_type: "offline",
|
|
193
|
+
prompt: "consent",
|
|
194
|
+
scope: GDRIVE_SCOPES,
|
|
195
|
+
state,
|
|
196
|
+
});
|
|
197
|
+
res.json({ url });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
router.get("/gdrive/callback", async (req, res) => {
|
|
201
|
+
const { code, state, error } = req.query;
|
|
202
|
+
|
|
203
|
+
if (error) return res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(error)}`);
|
|
204
|
+
if (!code || !state) return res.redirect(`${FRONTEND_URL}?oauth_error=missing_params`);
|
|
205
|
+
|
|
206
|
+
let workspaceId, connectionName, connectionSlug;
|
|
207
|
+
try {
|
|
208
|
+
({ workspaceId, connectionName, connectionSlug } = JSON.parse(Buffer.from(state, "base64url").toString()));
|
|
209
|
+
} catch {
|
|
210
|
+
return res.redirect(`${FRONTEND_URL}?oauth_error=invalid_state`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const oauth2 = await makeOAuth2Client(req.db, workspaceId, "/api/oauth/gdrive/callback");
|
|
215
|
+
const { tokens } = await oauth2.getToken(code);
|
|
216
|
+
oauth2.setCredentials(tokens);
|
|
217
|
+
|
|
218
|
+
const { google } = require("googleapis");
|
|
219
|
+
const oauth2Api = google.oauth2({ version: "v2", auth: oauth2 });
|
|
220
|
+
const { data: userInfo } = await oauth2Api.userinfo.get();
|
|
221
|
+
const email = userInfo.email;
|
|
222
|
+
|
|
223
|
+
const db = req.db || require("../utils/prisma");
|
|
224
|
+
|
|
225
|
+
const existing = await db.connector.findFirst({
|
|
226
|
+
where: { workspaceId, type: "gdrive", name: email }
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const authConfig = JSON.stringify({
|
|
230
|
+
accessToken: tokens.access_token,
|
|
231
|
+
refreshToken: tokens.refresh_token,
|
|
232
|
+
expiresAt: tokens.expiry_date,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const connName = connectionName?.trim() || email;
|
|
236
|
+
if (existing) {
|
|
237
|
+
await db.connector.update({
|
|
238
|
+
where: { id: existing.id },
|
|
239
|
+
data: { authConfig, status: "active", lastTestedAt: new Date() }
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
const finalName = await uniqueName(db, connName);
|
|
243
|
+
await db.connector.create({
|
|
244
|
+
data: {
|
|
245
|
+
workspaceId,
|
|
246
|
+
name: finalName,
|
|
247
|
+
type: "gdrive",
|
|
248
|
+
slug: finalName,
|
|
249
|
+
slug,
|
|
250
|
+
config: JSON.stringify({ email }),
|
|
251
|
+
authConfig,
|
|
252
|
+
status: "active",
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
res.redirect(await workspaceRedirect(req.db, workspaceId, "gdrive", true));
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.error("[oauth/gdrive] callback error:", err.message);
|
|
260
|
+
res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(err.message)}`);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ── OneDrive OAuth ───────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
router.post("/onedrive/configure", authenticate, requireManagerOrAdmin, async (req, res) => {
|
|
267
|
+
const { clientId, clientSecret, tenantId, workspaceId } = req.body;
|
|
268
|
+
if (!clientId?.trim() || !clientSecret?.trim() || !workspaceId) return res.status(400).json({ error: "clientId, clientSecret, workspaceId required" });
|
|
269
|
+
const wsId = parseInt(workspaceId);
|
|
270
|
+
await req.db.setting.upsert({ where: { key: `oauth.onedrive.clientId.ws.${wsId}` }, create: { key: `oauth.onedrive.clientId.ws.${wsId}`, value: clientId.trim() }, update: { value: clientId.trim() } });
|
|
271
|
+
await req.db.setting.upsert({ where: { key: `oauth.onedrive.clientSecret.ws.${wsId}` }, create: { key: `oauth.onedrive.clientSecret.ws.${wsId}`, value: clientSecret.trim() }, update: { value: clientSecret.trim() } });
|
|
272
|
+
if (tenantId?.trim()) await req.db.setting.upsert({ where: { key: `oauth.onedrive.tenantId.ws.${wsId}` }, create: { key: `oauth.onedrive.tenantId.ws.${wsId}`, value: tenantId.trim() }, update: { value: tenantId.trim() } });
|
|
273
|
+
res.json({ success: true });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
router.get("/onedrive/start", authenticate, requireManagerOrAdmin, async (req, res) => {
|
|
277
|
+
const wsId = parseInt(req.query.workspaceId);
|
|
278
|
+
if (!wsId) return res.status(400).json({ error: "workspaceId required" });
|
|
279
|
+
const get = async key => (await req.db.setting.findUnique({ where: { key } }))?.value;
|
|
280
|
+
const clientId = await get(`oauth.onedrive.clientId.ws.${wsId}`);
|
|
281
|
+
const tenantId = (await get(`oauth.onedrive.tenantId.ws.${wsId}`)) || "common";
|
|
282
|
+
if (!clientId) return res.status(400).json({ error: "OneDrive not configured. Add credentials in Integrations." });
|
|
283
|
+
const { connectionName = "", connectionSlug = "" } = req.query;
|
|
284
|
+
const state = Buffer.from(JSON.stringify({ workspaceId: wsId, connectionName, connectionSlug })).toString("base64url");
|
|
285
|
+
const params = new URLSearchParams({ client_id: clientId, response_type: "code", redirect_uri: `${CALLBACK_BASE}/api/oauth/onedrive/callback`, scope: "Files.Read.All offline_access User.Read", state });
|
|
286
|
+
res.json({ url: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${params}` });
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
router.get("/onedrive/callback", async (req, res) => {
|
|
290
|
+
const { code, state, error } = req.query;
|
|
291
|
+
if (error) return res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(error)}`);
|
|
292
|
+
if (!code || !state) return res.redirect(`${FRONTEND_URL}?oauth_error=missing_params`);
|
|
293
|
+
let workspaceId, connectionName, connectionSlug;
|
|
294
|
+
try { ({ workspaceId, connectionName, connectionSlug } = JSON.parse(Buffer.from(state, "base64url").toString())); } catch { return res.redirect(`${FRONTEND_URL}?oauth_error=invalid_state`); }
|
|
295
|
+
try {
|
|
296
|
+
const db = req.db;
|
|
297
|
+
const get = async key => (await db.setting.findUnique({ where: { key } }))?.value;
|
|
298
|
+
const clientId = await get(`oauth.onedrive.clientId.ws.${workspaceId}`);
|
|
299
|
+
const clientSecret = await get(`oauth.onedrive.clientSecret.ws.${workspaceId}`);
|
|
300
|
+
const tenantId = (await get(`oauth.onedrive.tenantId.ws.${workspaceId}`)) || "common";
|
|
301
|
+
const axios = require("axios");
|
|
302
|
+
const { data: tokens } = await axios.post(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
|
303
|
+
new URLSearchParams({ grant_type: "authorization_code", code, client_id: clientId, client_secret: clientSecret, redirect_uri: `${CALLBACK_BASE}/api/oauth/onedrive/callback`, scope: "Files.Read.All offline_access User.Read" }),
|
|
304
|
+
{ headers: { "Content-Type": "application/x-www-form-urlencoded" } });
|
|
305
|
+
const { data: me } = await axios.get("https://graph.microsoft.com/v1.0/me", { headers: { Authorization: `Bearer ${tokens.access_token}` } });
|
|
306
|
+
const email = me.mail || me.userPrincipalName;
|
|
307
|
+
const authConfig = JSON.stringify({ accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt: Date.now() + tokens.expires_in * 1000 });
|
|
308
|
+
const config = JSON.stringify({ clientId, clientSecret, tenantId, email });
|
|
309
|
+
const existing = await db.connector.findFirst({ where: { workspaceId, type: "onedrive", name: email } });
|
|
310
|
+
if (existing) await db.connector.update({ where: { id: existing.id }, data: { authConfig, config, status: "active", lastTestedAt: new Date() } });
|
|
311
|
+
else {
|
|
312
|
+
const finalName = await uniqueName(db, connectionName?.trim() || email);
|
|
313
|
+
await db.connector.create({ data: { workspaceId, name: finalName, type: "onedrive", slug: finalName, config, authConfig, status: "active" } });
|
|
314
|
+
}
|
|
315
|
+
res.redirect(await workspaceRedirect(req.db, workspaceId, "onedrive", true));
|
|
316
|
+
} catch (err) { res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(err.message)}`); }
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ── Dropbox OAuth ─────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
router.post("/dropbox/configure", authenticate, requireManagerOrAdmin, async (req, res) => {
|
|
322
|
+
const { appKey, appSecret, workspaceId } = req.body;
|
|
323
|
+
if (!appKey?.trim() || !appSecret?.trim() || !workspaceId) return res.status(400).json({ error: "appKey, appSecret, workspaceId required" });
|
|
324
|
+
const wsId = parseInt(workspaceId);
|
|
325
|
+
await req.db.setting.upsert({ where: { key: `oauth.dropbox.appKey.ws.${wsId}` }, create: { key: `oauth.dropbox.appKey.ws.${wsId}`, value: appKey.trim() }, update: { value: appKey.trim() } });
|
|
326
|
+
await req.db.setting.upsert({ where: { key: `oauth.dropbox.appSecret.ws.${wsId}` }, create: { key: `oauth.dropbox.appSecret.ws.${wsId}`, value: appSecret.trim() }, update: { value: appSecret.trim() } });
|
|
327
|
+
res.json({ success: true });
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
router.get("/dropbox/start", authenticate, requireManagerOrAdmin, async (req, res) => {
|
|
331
|
+
const wsId = parseInt(req.query.workspaceId);
|
|
332
|
+
if (!wsId) return res.status(400).json({ error: "workspaceId required" });
|
|
333
|
+
const get = async key => (await req.db.setting.findUnique({ where: { key } }))?.value;
|
|
334
|
+
const appKey = await get(`oauth.dropbox.appKey.ws.${wsId}`);
|
|
335
|
+
if (!appKey) return res.status(400).json({ error: "Dropbox not configured. Add credentials in Integrations." });
|
|
336
|
+
const { connectionName = "", connectionSlug = "" } = req.query;
|
|
337
|
+
const state = Buffer.from(JSON.stringify({ workspaceId: wsId, connectionName, connectionSlug })).toString("base64url");
|
|
338
|
+
const params = new URLSearchParams({ client_id: appKey, response_type: "code", redirect_uri: `${CALLBACK_BASE}/api/oauth/dropbox/callback`, token_access_type: "offline", state });
|
|
339
|
+
res.json({ url: `https://www.dropbox.com/oauth2/authorize?${params}` });
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
router.get("/dropbox/callback", async (req, res) => {
|
|
343
|
+
const { code, state, error } = req.query;
|
|
344
|
+
if (error) return res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(error)}`);
|
|
345
|
+
if (!code || !state) return res.redirect(`${FRONTEND_URL}?oauth_error=missing_params`);
|
|
346
|
+
let workspaceId, connectionName, connectionSlug;
|
|
347
|
+
try { ({ workspaceId, connectionName, connectionSlug } = JSON.parse(Buffer.from(state, "base64url").toString())); } catch { return res.redirect(`${FRONTEND_URL}?oauth_error=invalid_state`); }
|
|
348
|
+
try {
|
|
349
|
+
const db = req.db;
|
|
350
|
+
const get = async key => (await db.setting.findUnique({ where: { key } }))?.value;
|
|
351
|
+
const appKey = await get(`oauth.dropbox.appKey.ws.${workspaceId}`);
|
|
352
|
+
const appSecret = await get(`oauth.dropbox.appSecret.ws.${workspaceId}`);
|
|
353
|
+
const axios = require("axios");
|
|
354
|
+
const { data: tokens } = await axios.post("https://api.dropboxapi.com/oauth2/token",
|
|
355
|
+
new URLSearchParams({ grant_type: "authorization_code", code, client_id: appKey, client_secret: appSecret, redirect_uri: `${CALLBACK_BASE}/api/oauth/dropbox/callback` }),
|
|
356
|
+
{ headers: { "Content-Type": "application/x-www-form-urlencoded" } });
|
|
357
|
+
const { data: me } = await axios.post("https://api.dropboxapi.com/2/users/get_current_account", null, { headers: { Authorization: `Bearer ${tokens.access_token}` } });
|
|
358
|
+
const email = me.email;
|
|
359
|
+
const authConfig = JSON.stringify({ accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : null });
|
|
360
|
+
const config = JSON.stringify({ appKey, appSecret, email });
|
|
361
|
+
const existing = await db.connector.findFirst({ where: { workspaceId, type: "dropbox", name: email } });
|
|
362
|
+
if (existing) await db.connector.update({ where: { id: existing.id }, data: { authConfig, config, status: "active", lastTestedAt: new Date() } });
|
|
363
|
+
else {
|
|
364
|
+
const finalName = await uniqueName(db, connectionName?.trim() || email);
|
|
365
|
+
await db.connector.create({ data: { workspaceId, name: finalName, type: "dropbox", slug: finalName, config, authConfig, status: "active" } });
|
|
366
|
+
}
|
|
367
|
+
res.redirect(await workspaceRedirect(req.db, workspaceId, "dropbox", true));
|
|
368
|
+
} catch (err) { res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(err.message)}`); }
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// ── Box OAuth ─────────────────────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
router.post("/box/configure", authenticate, requireManagerOrAdmin, async (req, res) => {
|
|
374
|
+
const { clientId, clientSecret, workspaceId } = req.body;
|
|
375
|
+
if (!clientId?.trim() || !clientSecret?.trim() || !workspaceId) return res.status(400).json({ error: "clientId, clientSecret, workspaceId required" });
|
|
376
|
+
const wsId = parseInt(workspaceId);
|
|
377
|
+
await req.db.setting.upsert({ where: { key: `oauth.box.clientId.ws.${wsId}` }, create: { key: `oauth.box.clientId.ws.${wsId}`, value: clientId.trim() }, update: { value: clientId.trim() } });
|
|
378
|
+
await req.db.setting.upsert({ where: { key: `oauth.box.clientSecret.ws.${wsId}` }, create: { key: `oauth.box.clientSecret.ws.${wsId}`, value: clientSecret.trim() }, update: { value: clientSecret.trim() } });
|
|
379
|
+
res.json({ success: true });
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
router.get("/box/start", authenticate, requireManagerOrAdmin, async (req, res) => {
|
|
383
|
+
const wsId = parseInt(req.query.workspaceId);
|
|
384
|
+
if (!wsId) return res.status(400).json({ error: "workspaceId required" });
|
|
385
|
+
const get = async key => (await req.db.setting.findUnique({ where: { key } }))?.value;
|
|
386
|
+
const clientId = await get(`oauth.box.clientId.ws.${wsId}`);
|
|
387
|
+
if (!clientId) return res.status(400).json({ error: "Box not configured. Add credentials in Integrations." });
|
|
388
|
+
const { connectionName = "", connectionSlug = "" } = req.query;
|
|
389
|
+
const state = Buffer.from(JSON.stringify({ workspaceId: wsId, connectionName, connectionSlug })).toString("base64url");
|
|
390
|
+
const params = new URLSearchParams({ client_id: clientId, response_type: "code", redirect_uri: `${CALLBACK_BASE}/api/oauth/box/callback`, state });
|
|
391
|
+
res.json({ url: `https://account.box.com/api/oauth2/authorize?${params}` });
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
router.get("/box/callback", async (req, res) => {
|
|
395
|
+
const { code, state, error } = req.query;
|
|
396
|
+
if (error) return res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(error)}`);
|
|
397
|
+
if (!code || !state) return res.redirect(`${FRONTEND_URL}?oauth_error=missing_params`);
|
|
398
|
+
let workspaceId, connectionName, connectionSlug;
|
|
399
|
+
try { ({ workspaceId, connectionName, connectionSlug } = JSON.parse(Buffer.from(state, "base64url").toString())); } catch { return res.redirect(`${FRONTEND_URL}?oauth_error=invalid_state`); }
|
|
400
|
+
try {
|
|
401
|
+
const db = req.db;
|
|
402
|
+
const get = async key => (await db.setting.findUnique({ where: { key } }))?.value;
|
|
403
|
+
const clientId = await get(`oauth.box.clientId.ws.${workspaceId}`);
|
|
404
|
+
const clientSecret = await get(`oauth.box.clientSecret.ws.${workspaceId}`);
|
|
405
|
+
const axios = require("axios");
|
|
406
|
+
const { data: tokens } = await axios.post("https://api.box.com/oauth2/token",
|
|
407
|
+
new URLSearchParams({ grant_type: "authorization_code", code, client_id: clientId, client_secret: clientSecret, redirect_uri: `${CALLBACK_BASE}/api/oauth/box/callback` }),
|
|
408
|
+
{ headers: { "Content-Type": "application/x-www-form-urlencoded" } });
|
|
409
|
+
const { data: me } = await axios.get("https://api.box.com/2.0/users/me", { headers: { Authorization: `Bearer ${tokens.access_token}` } });
|
|
410
|
+
const email = me.login;
|
|
411
|
+
const authConfig = JSON.stringify({ accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt: Date.now() + tokens.expires_in * 1000 });
|
|
412
|
+
const config = JSON.stringify({ clientId, clientSecret, email });
|
|
413
|
+
const existing = await db.connector.findFirst({ where: { workspaceId, type: "box", name: email } });
|
|
414
|
+
if (existing) await db.connector.update({ where: { id: existing.id }, data: { authConfig, config, status: "active", lastTestedAt: new Date() } });
|
|
415
|
+
else {
|
|
416
|
+
const finalName = await uniqueName(db, connectionName?.trim() || email);
|
|
417
|
+
await db.connector.create({ data: { workspaceId, name: finalName, type: "box", slug: finalName, config, authConfig, status: "active" } });
|
|
418
|
+
}
|
|
419
|
+
res.redirect(await workspaceRedirect(req.db, workspaceId, "box", true));
|
|
420
|
+
} catch (err) { res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(err.message)}`); }
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
module.exports = router;
|