@yeaft/webchat-agent 1.0.188 → 1.0.190
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/cli.js +12 -0
- package/index.js +10 -2
- package/local-run.js +218 -0
- package/local-runtime/server/.env.example +54 -0
- package/local-runtime/server/api.js +111 -0
- package/local-runtime/server/auth/aad.js +235 -0
- package/local-runtime/server/auth/login.js +156 -0
- package/local-runtime/server/auth/oauth-flow.js +277 -0
- package/local-runtime/server/auth/password-reset.js +134 -0
- package/local-runtime/server/auth/providers/alipay.js +125 -0
- package/local-runtime/server/auth/providers/github.js +82 -0
- package/local-runtime/server/auth/providers/google.js +60 -0
- package/local-runtime/server/auth/providers/microsoft.js +71 -0
- package/local-runtime/server/auth/providers/types.js +57 -0
- package/local-runtime/server/auth/providers/wechat.js +68 -0
- package/local-runtime/server/auth/register.js +91 -0
- package/local-runtime/server/auth/session-store.js +57 -0
- package/local-runtime/server/auth/token.js +85 -0
- package/local-runtime/server/auth/totp-auth.js +133 -0
- package/local-runtime/server/auth/utils.js +42 -0
- package/local-runtime/server/auth.js +8 -0
- package/local-runtime/server/check-node-version.js +74 -0
- package/local-runtime/server/config.js +298 -0
- package/local-runtime/server/context.js +140 -0
- package/local-runtime/server/create-user.js +59 -0
- package/local-runtime/server/database.js +12 -0
- package/local-runtime/server/db/connection.js +963 -0
- package/local-runtime/server/db/expert-db.js +171 -0
- package/local-runtime/server/db/identity-db.js +92 -0
- package/local-runtime/server/db/invitation-db.js +38 -0
- package/local-runtime/server/db/message-db.js +257 -0
- package/local-runtime/server/db/session-db.js +118 -0
- package/local-runtime/server/db/user-db.js +165 -0
- package/local-runtime/server/db/user-stats-db.js +185 -0
- package/local-runtime/server/db/yeaft-session-db.js +258 -0
- package/local-runtime/server/email.js +96 -0
- package/local-runtime/server/encryption.js +105 -0
- package/local-runtime/server/handlers/agent-conversation.js +347 -0
- package/local-runtime/server/handlers/agent-file-terminal.js +99 -0
- package/local-runtime/server/handlers/agent-output.js +854 -0
- package/local-runtime/server/handlers/agent-sync.js +399 -0
- package/local-runtime/server/handlers/agent-work-center.js +27 -0
- package/local-runtime/server/handlers/client-conversation.js +1182 -0
- package/local-runtime/server/handlers/client-misc.js +254 -0
- package/local-runtime/server/handlers/client-work-center.js +269 -0
- package/local-runtime/server/handlers/client-workbench.js +146 -0
- package/local-runtime/server/handlers/session-pin-router.js +61 -0
- package/local-runtime/server/heartbeat-policy.js +46 -0
- package/local-runtime/server/index.js +275 -0
- package/local-runtime/server/package.json +55 -0
- package/local-runtime/server/perf-trace.js +154 -0
- package/local-runtime/server/proxy.js +273 -0
- package/local-runtime/server/routes/admin-routes.js +207 -0
- package/local-runtime/server/routes/auth-routes.js +322 -0
- package/local-runtime/server/routes/expert-routes.js +117 -0
- package/local-runtime/server/routes/invitation-routes.js +60 -0
- package/local-runtime/server/routes/session-routes.js +112 -0
- package/local-runtime/server/routes/upload-routes.js +109 -0
- package/local-runtime/server/routes/user-routes.js +241 -0
- package/local-runtime/server/totp.js +74 -0
- package/local-runtime/server/work-item-attachment-policy.js +56 -0
- package/local-runtime/server/ws-agent.js +319 -0
- package/local-runtime/server/ws-client.js +214 -0
- package/local-runtime/server/ws-utils.js +394 -0
- package/local-runtime/server/yeaft-asset-store.js +339 -0
- package/local-runtime/version.json +1 -0
- package/local-runtime/web/app.bundle.js +7673 -0
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/assets/avatars/README.md +34 -0
- package/local-runtime/web/assets/avatars/ada.svg +1 -0
- package/local-runtime/web/assets/avatars/alan.svg +1 -0
- package/local-runtime/web/assets/avatars/alice.svg +1 -0
- package/local-runtime/web/assets/avatars/anders.svg +1 -0
- package/local-runtime/web/assets/avatars/bezos.svg +1 -0
- package/local-runtime/web/assets/avatars/borges.svg +1 -0
- package/local-runtime/web/assets/avatars/buffett.svg +1 -0
- package/local-runtime/web/assets/avatars/clausewitz.svg +1 -0
- package/local-runtime/web/assets/avatars/dalio.svg +1 -0
- package/local-runtime/web/assets/avatars/dieter.svg +1 -0
- package/local-runtime/web/assets/avatars/drucker.svg +1 -0
- package/local-runtime/web/assets/avatars/einstein.svg +1 -0
- package/local-runtime/web/assets/avatars/grace.svg +1 -0
- package/local-runtime/web/assets/avatars/harari.svg +1 -0
- package/local-runtime/web/assets/avatars/jung.svg +1 -0
- package/local-runtime/web/assets/avatars/kahneman.svg +1 -0
- package/local-runtime/web/assets/avatars/ken.svg +1 -0
- package/local-runtime/web/assets/avatars/kongzi.svg +1 -0
- package/local-runtime/web/assets/avatars/kubrick.svg +1 -0
- package/local-runtime/web/assets/avatars/linus.svg +1 -0
- package/local-runtime/web/assets/avatars/luxun.svg +1 -0
- package/local-runtime/web/assets/avatars/margaret.svg +1 -0
- package/local-runtime/web/assets/avatars/martin.svg +1 -0
- package/local-runtime/web/assets/avatars/miyazaki.svg +1 -0
- package/local-runtime/web/assets/avatars/munger.svg +1 -0
- package/local-runtime/web/assets/avatars/nietzsche.svg +1 -0
- package/local-runtime/web/assets/avatars/norman.svg +1 -0
- package/local-runtime/web/assets/avatars/shannon.svg +1 -0
- package/local-runtime/web/assets/avatars/simaqian.svg +1 -0
- package/local-runtime/web/assets/avatars/socrates.svg +1 -0
- package/local-runtime/web/assets/avatars/steve.svg +1 -0
- package/local-runtime/web/assets/avatars/sudongpo.svg +1 -0
- package/local-runtime/web/assets/avatars/sunzi.svg +1 -0
- package/local-runtime/web/docx-preview.min.js +2 -0
- package/local-runtime/web/docx-preview.min.js.gz +0 -0
- package/local-runtime/web/html-to-image.min.js +2 -0
- package/local-runtime/web/html-to-image.min.js.gz +0 -0
- package/local-runtime/web/index.html +21 -0
- package/local-runtime/web/jszip.min.js +13 -0
- package/local-runtime/web/jszip.min.js.gz +0 -0
- package/local-runtime/web/mermaid.min.js +2843 -0
- package/local-runtime/web/mermaid.min.js.gz +0 -0
- package/local-runtime/web/msal-browser.min.js +69 -0
- package/local-runtime/web/msal-browser.min.js.gz +0 -0
- package/local-runtime/web/style.bundle.css +10 -0
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/local-runtime/web/vendor.bundle.js +1522 -0
- package/local-runtime/web/vendor.bundle.js.gz +0 -0
- package/local-runtime/web/xlsx.min.js +24 -0
- package/local-runtime/web/xlsx.min.js.gz +0 -0
- package/package.json +13 -3
- package/scripts/prepare-local-runtime.js +25 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { CONFIG, isEmailConfigured, isTotpEnabled, isAadEnabled, getEnabledSsoProviders, getUserByUsername } from '../config.js';
|
|
2
|
+
import { loginStep1, loginStep2, logout, verifyTotpStep, completeTotpSetup, register, loginWithAad } from '../auth.js';
|
|
3
|
+
import { buildAuthorizeUrl, handleCallback, peekStateMode, storePendingResult, consumePendingResult } from '../auth/oauth-flow.js';
|
|
4
|
+
import { verifyToken } from '../auth/token.js';
|
|
5
|
+
import { requestPasswordReset, verifyPasswordReset } from '../auth/password-reset.js';
|
|
6
|
+
import { identityDb, userDb } from '../database.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Register authentication-related API routes.
|
|
10
|
+
*/
|
|
11
|
+
export function registerAuthRoutes(app, { requireAuth, checkRateLimit }) {
|
|
12
|
+
app.get('/api/auth/mode', (req, res) => {
|
|
13
|
+
const aadEnabled = isAadEnabled();
|
|
14
|
+
const ssoProviders = getEnabledSsoProviders();
|
|
15
|
+
res.json({
|
|
16
|
+
skipAuth: CONFIG.skipAuth,
|
|
17
|
+
emailVerification: isEmailConfigured(),
|
|
18
|
+
// Password reset shares the email infrastructure — gate the UI on this.
|
|
19
|
+
passwordResetEnabled: isEmailConfigured(),
|
|
20
|
+
totpEnabled: isTotpEnabled(),
|
|
21
|
+
registrationEnabled: !CONFIG.skipAuth,
|
|
22
|
+
aadEnabled,
|
|
23
|
+
...(aadEnabled && {
|
|
24
|
+
aadClientId: CONFIG.aad.clientId,
|
|
25
|
+
aadTenantId: CONFIG.aad.tenantId
|
|
26
|
+
}),
|
|
27
|
+
// Server-driven SSO providers (each one handled via /api/auth/sso/:provider/start).
|
|
28
|
+
ssoProviders
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
app.post('/api/auth/login', async (req, res) => {
|
|
33
|
+
if (!checkRateLimit(req.ip)) {
|
|
34
|
+
return res.status(429).json({ success: false, error: 'Too many login attempts, please try again later' });
|
|
35
|
+
}
|
|
36
|
+
const { username, password } = req.body;
|
|
37
|
+
if (!username || !password) {
|
|
38
|
+
return res.status(400).json({ success: false, error: 'Username and password are required' });
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const result = await loginStep1(username, password);
|
|
42
|
+
res.json(result);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error('Login error:', err);
|
|
45
|
+
res.status(500).json({ success: false, error: 'Internal server error' });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
app.post('/api/auth/verify', (req, res) => {
|
|
50
|
+
if (!checkRateLimit(req.ip)) {
|
|
51
|
+
return res.status(429).json({ success: false, error: 'Too many attempts, please try again later' });
|
|
52
|
+
}
|
|
53
|
+
const { tempToken, code } = req.body;
|
|
54
|
+
if (!tempToken || !code) {
|
|
55
|
+
return res.status(400).json({ success: false, error: 'Token and code are required' });
|
|
56
|
+
}
|
|
57
|
+
const result = loginStep2(tempToken, code);
|
|
58
|
+
res.json(result);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
app.post('/api/auth/logout', (req, res) => {
|
|
62
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
63
|
+
if (token) {
|
|
64
|
+
logout(token);
|
|
65
|
+
}
|
|
66
|
+
res.json({ success: true });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
app.post('/api/auth/verify-totp', async (req, res) => {
|
|
70
|
+
if (!checkRateLimit(req.ip)) {
|
|
71
|
+
return res.status(429).json({ success: false, error: 'Too many attempts, please try again later' });
|
|
72
|
+
}
|
|
73
|
+
const { tempToken, totpCode } = req.body;
|
|
74
|
+
if (!tempToken || !totpCode) {
|
|
75
|
+
return res.status(400).json({ success: false, error: 'Token and TOTP code are required' });
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const result = await verifyTotpStep(tempToken, totpCode);
|
|
79
|
+
res.json(result);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error('TOTP verification error:', err);
|
|
82
|
+
res.status(500).json({ success: false, error: 'Internal server error' });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
app.post('/api/auth/setup-totp', async (req, res) => {
|
|
87
|
+
const { setupToken, totpCode } = req.body;
|
|
88
|
+
if (!setupToken || !totpCode) {
|
|
89
|
+
return res.status(400).json({ success: false, error: 'Setup token and TOTP code are required' });
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const result = await completeTotpSetup(setupToken, totpCode);
|
|
93
|
+
res.json(result);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error('TOTP setup error:', err);
|
|
96
|
+
res.status(500).json({ success: false, error: 'Internal server error' });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Registration (public - requires invitation code)
|
|
101
|
+
app.post('/api/auth/register', async (req, res) => {
|
|
102
|
+
if (!checkRateLimit(req.ip)) {
|
|
103
|
+
return res.status(429).json({ success: false, error: 'Too many attempts, please try again later' });
|
|
104
|
+
}
|
|
105
|
+
const { username, password, email, invitationCode } = req.body;
|
|
106
|
+
try {
|
|
107
|
+
const result = await register(username, password, email, invitationCode);
|
|
108
|
+
if (!result.success) {
|
|
109
|
+
return res.status(400).json(result);
|
|
110
|
+
}
|
|
111
|
+
res.json(result);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error('Registration error:', err);
|
|
114
|
+
res.status(500).json({ success: false, error: 'Internal server error' });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Password reset — step 1: email me a code
|
|
119
|
+
app.post('/api/auth/password-reset/request', async (req, res) => {
|
|
120
|
+
if (!checkRateLimit(req.ip)) {
|
|
121
|
+
return res.status(429).json({ success: false, error: 'Too many attempts, please try again later' });
|
|
122
|
+
}
|
|
123
|
+
const { email } = req.body || {};
|
|
124
|
+
try {
|
|
125
|
+
const result = await requestPasswordReset(email);
|
|
126
|
+
res.json(result);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error('Password reset request error:', err);
|
|
129
|
+
res.status(500).json({ success: false, error: 'Internal server error' });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Password reset — step 2: verify code, set new password
|
|
134
|
+
app.post('/api/auth/password-reset/verify', async (req, res) => {
|
|
135
|
+
if (!checkRateLimit(req.ip)) {
|
|
136
|
+
return res.status(429).json({ success: false, error: 'Too many attempts, please try again later' });
|
|
137
|
+
}
|
|
138
|
+
const { resetToken, code, newPassword } = req.body || {};
|
|
139
|
+
try {
|
|
140
|
+
const result = await verifyPasswordReset(resetToken, code, newPassword);
|
|
141
|
+
res.json(result);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error('Password reset verify error:', err);
|
|
144
|
+
res.status(500).json({ success: false, error: 'Internal server error' });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Azure AD (Microsoft Entra ID) SSO login
|
|
149
|
+
app.post('/api/auth/aad', async (req, res) => {
|
|
150
|
+
if (!checkRateLimit(req.ip)) {
|
|
151
|
+
return res.status(429).json({ success: false, error: 'Too many attempts, please try again later' });
|
|
152
|
+
}
|
|
153
|
+
const { idToken } = req.body;
|
|
154
|
+
if (!idToken) {
|
|
155
|
+
return res.status(400).json({ success: false, error: 'id_token is required' });
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const result = await loginWithAad(idToken);
|
|
159
|
+
res.json(result);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error('AAD login error:', err);
|
|
162
|
+
res.status(500).json({ success: false, error: 'Internal server error' });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ─── Server-driven SSO (GitHub / Google / WeChat / Alipay / Microsoft) ────
|
|
167
|
+
// Login flow: /start → 302 to provider → user consents → provider 302 to /callback
|
|
168
|
+
// Bind flow: same, but /start requires Authorization header so callback knows which user
|
|
169
|
+
|
|
170
|
+
app.get('/api/auth/sso/:provider/start', (req, res) => {
|
|
171
|
+
const { provider } = req.params;
|
|
172
|
+
const intent = req.query.intent === 'bind' ? 'bind' : 'login';
|
|
173
|
+
let userId = null;
|
|
174
|
+
|
|
175
|
+
if (intent === 'bind') {
|
|
176
|
+
// Bind requires the caller to be already logged in. The browser navigates
|
|
177
|
+
// here directly so we read the token from the `Authorization` query
|
|
178
|
+
// parameter (frontend hands it over explicitly because top-level navigation
|
|
179
|
+
// can't set headers).
|
|
180
|
+
const token = String(req.query.token || '');
|
|
181
|
+
const ver = verifyToken(token);
|
|
182
|
+
if (!ver.valid) {
|
|
183
|
+
return res.status(401).send('Authentication required to bind an identity');
|
|
184
|
+
}
|
|
185
|
+
const u = userDb.getByUsername(ver.username);
|
|
186
|
+
if (!u) return res.status(401).send('User not found');
|
|
187
|
+
userId = u.id;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const { url } = buildAuthorizeUrl({ provider, intent, userId });
|
|
192
|
+
res.redirect(url);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error(`[SSO ${provider}] start error:`, err.message);
|
|
195
|
+
res.status(400).send(`SSO start failed: ${err.message}`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// QR-flow start: returns the authorize URL + state in JSON instead of
|
|
200
|
+
// redirecting. The PC frontend renders the URL as a QR code, the user scans
|
|
201
|
+
// with their phone, and the PC polls /poll/:state for the result.
|
|
202
|
+
app.get('/api/auth/sso/:provider/start-qr', (req, res) => {
|
|
203
|
+
const { provider } = req.params;
|
|
204
|
+
const intent = req.query.intent === 'bind' ? 'bind' : 'login';
|
|
205
|
+
let userId = null;
|
|
206
|
+
if (intent === 'bind') {
|
|
207
|
+
const token = String(req.query.token || '');
|
|
208
|
+
const ver = verifyToken(token);
|
|
209
|
+
if (!ver.valid) return res.status(401).json({ success: false, error: 'auth required' });
|
|
210
|
+
const u = userDb.getByUsername(ver.username);
|
|
211
|
+
if (!u) return res.status(401).json({ success: false, error: 'user not found' });
|
|
212
|
+
userId = u.id;
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
const { url, state } = buildAuthorizeUrl({ provider, intent, userId, mode: 'qr' });
|
|
216
|
+
res.json({ success: true, authorizeUrl: url, state });
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error(`[SSO ${provider}] start-qr error:`, err.message);
|
|
219
|
+
res.status(400).json({ success: false, error: err.message });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// QR-flow poll: PC frontend hits this every ~2s with the state issued by
|
|
224
|
+
// /start-qr. Returns { status: 'pending' | 'login' | 'bind' | 'error', ... }.
|
|
225
|
+
app.get('/api/auth/sso/poll/:state', (req, res) => {
|
|
226
|
+
const r = consumePendingResult(req.params.state);
|
|
227
|
+
if (!r) return res.json({ status: 'pending' });
|
|
228
|
+
if (r.kind === 'login') {
|
|
229
|
+
return res.json({ status: 'login', token: r.token, sessionKey: r.sessionKey, role: r.role });
|
|
230
|
+
}
|
|
231
|
+
if (r.kind === 'bind') {
|
|
232
|
+
return res.json({ status: 'bind', provider: r.provider });
|
|
233
|
+
}
|
|
234
|
+
return res.json({ status: 'error', error: r.error || 'SSO failed', code: r.status || 400 });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
app.get('/api/auth/sso/:provider/callback', async (req, res) => {
|
|
238
|
+
const { provider } = req.params;
|
|
239
|
+
// Alipay's web auth returns the code as `auth_code` (not the OAuth-standard
|
|
240
|
+
// `code`). Accept either to keep one route handling all providers.
|
|
241
|
+
const code = req.query.code || req.query.auth_code;
|
|
242
|
+
const { state } = req.query;
|
|
243
|
+
if (!code || !state) {
|
|
244
|
+
return res.status(400).send('Missing code or state');
|
|
245
|
+
}
|
|
246
|
+
// Was this state issued in QR mode? If so, the PC browser is polling for
|
|
247
|
+
// the result — we park it under the state and show a tiny success page on
|
|
248
|
+
// the device that scanned (typically the user's phone).
|
|
249
|
+
const stateMode = peekStateMode(String(state));
|
|
250
|
+
try {
|
|
251
|
+
const result = await handleCallback({ provider, code: String(code), state: String(state) });
|
|
252
|
+
|
|
253
|
+
if (stateMode === 'qr') {
|
|
254
|
+
storePendingResult(String(state), result);
|
|
255
|
+
if (result.kind === 'error') {
|
|
256
|
+
return res.status(result.status || 400).send(`扫码登录失败: ${result.error}`);
|
|
257
|
+
}
|
|
258
|
+
// Minimal success page for the scanning device. The real session lands
|
|
259
|
+
// on the original PC tab once it finishes polling.
|
|
260
|
+
return res.send(`<!doctype html><meta charset="utf-8"><title>登录成功</title><style>body{font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;color:#222}.box{text-align:center;padding:0 24px}h1{font-size:20px;margin:0 0 8px}p{color:#666;margin:0;line-height:1.5}</style><div class=box><h1>✓ 登录成功</h1><p>请返回原先的浏览器页面,几秒内会自动登录。<br/>本页面可关闭。</p></div>`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (result.kind === 'login') {
|
|
264
|
+
const params = new URLSearchParams({
|
|
265
|
+
token: result.token,
|
|
266
|
+
sessionKey: result.sessionKey,
|
|
267
|
+
role: result.role
|
|
268
|
+
});
|
|
269
|
+
return res.redirect(`/#/sso-complete?${params.toString()}`);
|
|
270
|
+
}
|
|
271
|
+
if (result.kind === 'bind') {
|
|
272
|
+
return res.redirect(`/#/settings?ssoBound=${encodeURIComponent(provider)}`);
|
|
273
|
+
}
|
|
274
|
+
const status = result.status || 400;
|
|
275
|
+
if (status === 409) {
|
|
276
|
+
return res.redirect(`/#/settings?ssoError=conflict&provider=${encodeURIComponent(provider)}`);
|
|
277
|
+
}
|
|
278
|
+
return res.status(status).send(`SSO error: ${result.error}`);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.error(`[SSO ${provider}] callback error:`, err);
|
|
281
|
+
res.status(500).send('Internal server error');
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// List the current user's bound identities.
|
|
286
|
+
app.get('/api/auth/identities', requireAuth, (req, res) => {
|
|
287
|
+
const u = userDb.getByUsername(req.user.username);
|
|
288
|
+
if (!u) return res.status(404).json({ success: false, error: 'User not found' });
|
|
289
|
+
const rows = identityDb.listForUser(u.id).map(r => ({
|
|
290
|
+
provider: r.provider,
|
|
291
|
+
email: r.email,
|
|
292
|
+
displayName: r.display_name,
|
|
293
|
+
createdAt: Number(r.created_at),
|
|
294
|
+
lastLoginAt: r.last_login_at ? Number(r.last_login_at) : null
|
|
295
|
+
}));
|
|
296
|
+
// Whether the user has a password (i.e. can log in without any SSO identity).
|
|
297
|
+
const hasPassword = !!u.password_hash;
|
|
298
|
+
res.json({ success: true, identities: rows, hasPassword });
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Unbind an identity. Refuse if it would leave the user with no login method.
|
|
302
|
+
app.delete('/api/auth/identities/:provider', requireAuth, (req, res) => {
|
|
303
|
+
const { provider } = req.params;
|
|
304
|
+
const u = userDb.getByUsername(req.user.username);
|
|
305
|
+
if (!u) return res.status(404).json({ success: false, error: 'User not found' });
|
|
306
|
+
const total = identityDb.countForUser(u.id);
|
|
307
|
+
const hasPassword = !!u.password_hash;
|
|
308
|
+
if (!hasPassword && total <= 1) {
|
|
309
|
+
return res.status(400).json({
|
|
310
|
+
success: false,
|
|
311
|
+
error: 'Cannot remove the only login method. Set a password first.'
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const removed = identityDb.removeForUser(u.id, provider);
|
|
315
|
+
if (!removed) return res.status(404).json({ success: false, error: 'No such linked identity' });
|
|
316
|
+
// Keep legacy users.aad_oid in sync if microsoft is unbound.
|
|
317
|
+
if (provider === 'microsoft' && u.aad_oid) {
|
|
318
|
+
try { userDb.updateAadOid(u.id, null); } catch { /* ignore */ }
|
|
319
|
+
}
|
|
320
|
+
res.json({ success: true });
|
|
321
|
+
});
|
|
322
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { CONFIG } from '../config.js';
|
|
2
|
+
import { expertDb, userDb } from '../database.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the effective userId from the request.
|
|
6
|
+
* In skipAuth mode, uses a default user id; otherwise resolves from JWT.
|
|
7
|
+
*/
|
|
8
|
+
function resolveUserId(req) {
|
|
9
|
+
if (CONFIG.skipAuth) {
|
|
10
|
+
// In skipAuth mode, use a fixed default user ID
|
|
11
|
+
return req.query.userId || 'default-user';
|
|
12
|
+
}
|
|
13
|
+
const user = userDb.getByUsername(req.user.username);
|
|
14
|
+
return user?.id;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Register custom expert role REST API routes.
|
|
19
|
+
*
|
|
20
|
+
* GET /api/expert-roles/custom — list custom roles for current user
|
|
21
|
+
* POST /api/expert-roles/custom — create a custom role
|
|
22
|
+
* PUT /api/expert-roles/custom/:roleId — update a custom role
|
|
23
|
+
* DELETE /api/expert-roles/custom/:roleId — delete a custom role
|
|
24
|
+
*/
|
|
25
|
+
export function registerExpertRoutes(app, { requireAuth }) {
|
|
26
|
+
// GET — list all custom roles for the current user
|
|
27
|
+
app.get('/api/expert-roles/custom', requireAuth, (req, res) => {
|
|
28
|
+
try {
|
|
29
|
+
const userId = resolveUserId(req);
|
|
30
|
+
if (!userId) return res.status(401).json({ error: 'User not found' });
|
|
31
|
+
|
|
32
|
+
const roles = expertDb.getCustomRolesByUser(userId);
|
|
33
|
+
res.json({ roles });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('[ExpertRoutes] GET /api/expert-roles/custom error:', error);
|
|
36
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// POST — create a custom role
|
|
41
|
+
app.post('/api/expert-roles/custom', requireAuth, (req, res) => {
|
|
42
|
+
try {
|
|
43
|
+
const userId = resolveUserId(req);
|
|
44
|
+
if (!userId) return res.status(401).json({ error: 'User not found' });
|
|
45
|
+
|
|
46
|
+
const roleData = req.body;
|
|
47
|
+
if (!roleData.name || !roleData.title) {
|
|
48
|
+
return res.status(400).json({ error: 'name and title are required' });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check for duplicate role_id if provided
|
|
52
|
+
if (roleData.roleId && expertDb.exists(userId, roleData.roleId)) {
|
|
53
|
+
return res.status(409).json({ error: `Role ${roleData.roleId} already exists` });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { rowId, roleId } = expertDb.createCustomRole(userId, roleData);
|
|
57
|
+
const roles = expertDb.getCustomRolesByUser(userId);
|
|
58
|
+
const created = roles.find(r => r.id === roleId);
|
|
59
|
+
|
|
60
|
+
res.status(201).json({ role: created });
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('[ExpertRoutes] POST /api/expert-roles/custom error:', error);
|
|
63
|
+
if (error.message?.includes('UNIQUE constraint')) {
|
|
64
|
+
return res.status(409).json({ error: 'Role with this ID already exists' });
|
|
65
|
+
}
|
|
66
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// PUT — update a custom role
|
|
71
|
+
app.put('/api/expert-roles/custom/:roleId', requireAuth, (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
const userId = resolveUserId(req);
|
|
74
|
+
if (!userId) return res.status(401).json({ error: 'User not found' });
|
|
75
|
+
|
|
76
|
+
const { roleId } = req.params;
|
|
77
|
+
const roleData = req.body;
|
|
78
|
+
|
|
79
|
+
if (!expertDb.exists(userId, roleId)) {
|
|
80
|
+
return res.status(404).json({ error: `Role ${roleId} not found` });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!roleData.name || !roleData.title) {
|
|
84
|
+
return res.status(400).json({ error: 'name and title are required' });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
expertDb.updateCustomRole(userId, roleId, roleData);
|
|
88
|
+
const roles = expertDb.getCustomRolesByUser(userId);
|
|
89
|
+
const updated = roles.find(r => r.id === roleId);
|
|
90
|
+
|
|
91
|
+
res.json({ role: updated });
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('[ExpertRoutes] PUT /api/expert-roles/custom error:', error);
|
|
94
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// DELETE — delete a custom role
|
|
99
|
+
app.delete('/api/expert-roles/custom/:roleId', requireAuth, (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const userId = resolveUserId(req);
|
|
102
|
+
if (!userId) return res.status(401).json({ error: 'User not found' });
|
|
103
|
+
|
|
104
|
+
const { roleId } = req.params;
|
|
105
|
+
const deleted = expertDb.deleteCustomRole(userId, roleId);
|
|
106
|
+
|
|
107
|
+
if (!deleted) {
|
|
108
|
+
return res.status(404).json({ error: `Role ${roleId} not found` });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
res.json({ deleted: true, roleId });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('[ExpertRoutes] DELETE /api/expert-roles/custom error:', error);
|
|
114
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { userDb, invitationDb } from '../database.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Register invitation-related API routes.
|
|
5
|
+
*/
|
|
6
|
+
export function registerInvitationRoutes(app, { requireAuth, requireAdmin }) {
|
|
7
|
+
// Create invitation code
|
|
8
|
+
app.post('/api/invitations', requireAuth, requireAdmin, (req, res) => {
|
|
9
|
+
try {
|
|
10
|
+
const user = userDb.getByUsername(req.user.username);
|
|
11
|
+
if (!user) {
|
|
12
|
+
return res.status(400).json({ error: 'User not found' });
|
|
13
|
+
}
|
|
14
|
+
const role = req.body.role || 'pro';
|
|
15
|
+
if (!['pro'].includes(role)) {
|
|
16
|
+
return res.status(400).json({ error: 'Invalid role. Must be "pro"' });
|
|
17
|
+
}
|
|
18
|
+
const expiresInDays = parseInt(req.body.expiresInDays, 10) || 7;
|
|
19
|
+
const expiresInMs = expiresInDays * 24 * 60 * 60 * 1000;
|
|
20
|
+
const invitation = invitationDb.create(user.id, role, expiresInMs);
|
|
21
|
+
res.json(invitation);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error('Create invitation error:', err);
|
|
24
|
+
res.status(500).json({ error: 'Failed to create invitation' });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// List my invitations
|
|
29
|
+
app.get('/api/invitations', requireAuth, requireAdmin, (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
const user = userDb.getByUsername(req.user.username);
|
|
32
|
+
if (!user) {
|
|
33
|
+
return res.status(400).json({ error: 'User not found' });
|
|
34
|
+
}
|
|
35
|
+
const invitations = invitationDb.getByUser(user.id);
|
|
36
|
+
res.json({ invitations });
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error('List invitations error:', err);
|
|
39
|
+
res.status(500).json({ error: 'Failed to list invitations' });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Delete unused invitation
|
|
44
|
+
app.delete('/api/invitations/:code', requireAuth, requireAdmin, (req, res) => {
|
|
45
|
+
try {
|
|
46
|
+
const user = userDb.getByUsername(req.user.username);
|
|
47
|
+
if (!user) {
|
|
48
|
+
return res.status(400).json({ error: 'User not found' });
|
|
49
|
+
}
|
|
50
|
+
const deleted = invitationDb.delete(req.params.code, user.id);
|
|
51
|
+
if (!deleted) {
|
|
52
|
+
return res.status(404).json({ error: 'Invitation not found or already used' });
|
|
53
|
+
}
|
|
54
|
+
res.json({ success: true });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error('Delete invitation error:', err);
|
|
57
|
+
res.status(500).json({ error: 'Failed to delete invitation' });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { CONFIG } from '../config.js';
|
|
2
|
+
import { sessionDb, messageDb, userDb } from '../database.js';
|
|
3
|
+
|
|
4
|
+
// 转换数据库会话记录为前端期望的格式
|
|
5
|
+
function transformSession(session) {
|
|
6
|
+
return {
|
|
7
|
+
id: session.id,
|
|
8
|
+
agentId: session.agent_id,
|
|
9
|
+
agentName: session.agent_name,
|
|
10
|
+
claudeSessionId: session.claude_session_id,
|
|
11
|
+
workDir: session.work_dir,
|
|
12
|
+
title: session.title,
|
|
13
|
+
createdAt: session.created_at,
|
|
14
|
+
updatedAt: session.updated_at,
|
|
15
|
+
isActive: !!session.is_active,
|
|
16
|
+
userId: session.user_id
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register session history API routes.
|
|
22
|
+
*/
|
|
23
|
+
export function registerSessionRoutes(app, { requireAuth }) {
|
|
24
|
+
app.get('/api/sessions', requireAuth, (req, res) => {
|
|
25
|
+
const limit = parseInt(req.query.limit, 10) || 100;
|
|
26
|
+
const agentId = req.query.agentId;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
let effectiveUserId;
|
|
30
|
+
if (CONFIG.skipAuth) {
|
|
31
|
+
effectiveUserId = req.query.userId;
|
|
32
|
+
} else if (req.user.role === 'admin' && req.query.userId) {
|
|
33
|
+
effectiveUserId = req.query.userId;
|
|
34
|
+
} else {
|
|
35
|
+
const user = userDb.getByUsername(req.user.username);
|
|
36
|
+
effectiveUserId = user?.id;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let sessions;
|
|
40
|
+
if (effectiveUserId && agentId) {
|
|
41
|
+
sessions = sessionDb.getByUserAndAgent(effectiveUserId, agentId, limit);
|
|
42
|
+
} else if (effectiveUserId) {
|
|
43
|
+
sessions = sessionDb.getByUser(effectiveUserId, limit);
|
|
44
|
+
} else if (agentId) {
|
|
45
|
+
sessions = sessionDb.getByAgent(agentId, limit);
|
|
46
|
+
} else {
|
|
47
|
+
sessions = sessionDb.getAll(limit);
|
|
48
|
+
}
|
|
49
|
+
res.json({ sessions: sessions.map(transformSession) });
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.error('Failed to get sessions:', e.message);
|
|
52
|
+
res.status(500).json({ error: 'Failed to get sessions' });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
app.get('/api/sessions/:id', requireAuth, (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
const session = sessionDb.get(req.params.id);
|
|
59
|
+
if (!session) {
|
|
60
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
61
|
+
}
|
|
62
|
+
if (!CONFIG.skipAuth) {
|
|
63
|
+
const user = userDb.getByUsername(req.user.username);
|
|
64
|
+
if (session.user_id && session.user_id !== user?.id) {
|
|
65
|
+
return res.status(403).json({ error: 'Permission denied' });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
res.json({ session: transformSession(session) });
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error('Failed to get session:', e.message);
|
|
71
|
+
res.status(500).json({ error: 'Failed to get session' });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
app.get('/api/sessions/:id/messages', requireAuth, (req, res) => {
|
|
76
|
+
const limit = parseInt(req.query.limit, 10) || 100;
|
|
77
|
+
try {
|
|
78
|
+
if (!CONFIG.skipAuth) {
|
|
79
|
+
const session = sessionDb.get(req.params.id);
|
|
80
|
+
const user = userDb.getByUsername(req.user.username);
|
|
81
|
+
if (session && session.user_id && session.user_id !== user?.id) {
|
|
82
|
+
return res.status(403).json({ error: 'Permission denied' });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const messages = limit
|
|
86
|
+
? messageDb.getRecent(req.params.id, limit)
|
|
87
|
+
: messageDb.getBySession(req.params.id);
|
|
88
|
+
res.json({ messages });
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.error('Failed to get messages:', e.message);
|
|
91
|
+
res.status(500).json({ error: 'Failed to get messages' });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
app.delete('/api/sessions/:id', requireAuth, (req, res) => {
|
|
96
|
+
try {
|
|
97
|
+
if (!CONFIG.skipAuth) {
|
|
98
|
+
const session = sessionDb.get(req.params.id);
|
|
99
|
+
const user = userDb.getByUsername(req.user.username);
|
|
100
|
+
if (session && session.user_id && session.user_id !== user?.id) {
|
|
101
|
+
return res.status(403).json({ error: 'Permission denied' });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
messageDb.deleteBySession(req.params.id);
|
|
105
|
+
sessionDb.delete(req.params.id);
|
|
106
|
+
res.json({ success: true });
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.error('Failed to delete session:', e.message);
|
|
109
|
+
res.status(500).json({ error: 'Failed to delete session' });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|