upfynai-code 3.0.4 → 3.2.0
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 +69 -92
- package/bin/cli.js +191 -0
- package/dist/client/assets/AppContent-M14Au3SB.js +542 -0
- package/{client/dist/assets/BrowserPanel-0TLEl-IC.js → dist/client/assets/BrowserPanel-TFKm2NDJ.js} +2 -2
- package/dist/client/assets/DashboardPanel-C88HjsCh.js +1 -0
- package/dist/client/assets/FileTree-DvO1xnDE.js +1 -0
- package/{client/dist/assets/GitPanel-C_xFM-N2.js → dist/client/assets/GitPanel-D-slVlyy.js} +2 -2
- package/dist/client/assets/LoginModal-Chi4SYcr.js +21 -0
- package/{client/dist/assets/MarkdownPreview-CESjI261.js → dist/client/assets/MarkdownPreview-CuIix2u9.js} +1 -1
- package/dist/client/assets/MermaidBlock-Dq9uFv82.js +2 -0
- package/dist/client/assets/Onboarding-QYXx24dX.js +1 -0
- package/{client/dist/assets/PreviewPanel-CqCa92Tf.js → dist/client/assets/PreviewPanel-Dd8q-jo0.js} +1 -1
- package/dist/client/assets/SetupForm-CrspaUva.js +1 -0
- package/dist/client/assets/WorkflowsPanel-DIlYAdhB.js +1 -0
- package/dist/client/assets/index-CnNNzw9A.css +1 -0
- package/{client/dist/assets/index-HaY-3pK1.js → dist/client/assets/index-rUkK9FDP.js} +26 -26
- package/{client/dist/assets/vendor-codemirror-D2ALgpaX.js → dist/client/assets/vendor-codemirror-jc6nyJQg.js} +1 -1
- package/{client/dist/assets/vendor-diff-DNQpbhrT.js → dist/client/assets/vendor-diff-THJmAcEI.js} +1 -1
- package/{client/dist/assets/vendor-icons-GyYE35HP.js → dist/client/assets/vendor-icons-CfjIpdrD.js} +145 -155
- package/{client/dist/assets/vendor-markdown-CimbIo6Y.js → dist/client/assets/vendor-markdown-Cdm6NEGf.js} +1 -1
- package/dist/client/assets/vendor-mermaid-DTPaBx-U.js +2559 -0
- package/{client/dist/assets/vendor-react-96lCPsRK.js → dist/client/assets/vendor-react-wFkb6mSf.js} +1 -1
- package/{client/dist/assets/vendor-syntax-LS_Nt30I.js → dist/client/assets/vendor-syntax-C_UZR7tc.js} +1 -1
- package/dist/client/favicon.png +0 -0
- package/dist/client/icons/icon-128x128.png +0 -0
- package/dist/client/icons/icon-144x144.png +0 -0
- package/dist/client/icons/icon-152x152.png +0 -0
- package/dist/client/icons/icon-192x192.png +0 -0
- package/dist/client/icons/icon-384x384.png +0 -0
- package/dist/client/icons/icon-512x512.png +0 -0
- package/dist/client/icons/icon-72x72.png +0 -0
- package/dist/client/icons/icon-96x96.png +0 -0
- package/{client/dist → dist/client}/index.html +37 -36
- package/dist/client/logo-128.png +0 -0
- package/dist/client/logo-256.png +0 -0
- package/dist/client/logo-32.png +0 -0
- package/dist/client/logo-512.png +0 -0
- package/dist/client/logo-64.png +0 -0
- package/dist/client/logo.png +0 -0
- package/{client/dist → dist/client}/manifest.json +12 -12
- package/{client/dist → dist/client}/mcp-docs.html +1 -1
- package/{client/dist → dist/client}/sw.js +2 -2
- package/package.json +56 -105
- package/scripts/postinstall.js +9 -0
- package/scripts/prepublish.js +77 -0
- package/src/animation.js +228 -0
- package/src/auth.js +142 -0
- package/src/config.js +40 -0
- package/src/connect.js +416 -0
- package/src/launch.js +81 -0
- package/src/mcp.js +57 -0
- package/src/permissions.js +140 -0
- package/src/persistent-shell.js +261 -0
- package/src/server.js +54 -0
- package/client/dist/assets/AppContent-CwrTP6TW.js +0 -545
- package/client/dist/assets/CanvasFullScreen-D1GWQsGL.js +0 -1
- package/client/dist/assets/CanvasWorkspace-D7ORj358.js +0 -163
- package/client/dist/assets/DashboardPanel-BV7ybUDe.js +0 -1
- package/client/dist/assets/FileTree-5qfhBqdE.js +0 -1
- package/client/dist/assets/LoginModal-CImJHRjX.js +0 -13
- package/client/dist/assets/MermaidBlock-BFM21cwe.js +0 -2
- package/client/dist/assets/Onboarding-B3cteLu2.js +0 -1
- package/client/dist/assets/SetupForm-P6dsYgHO.js +0 -1
- package/client/dist/assets/WorkflowsPanel-CBoN80kc.js +0 -1
- package/client/dist/assets/index-46kkVu2i.css +0 -1
- package/client/dist/assets/pdf-CE_K4jFx.js +0 -12
- package/client/dist/assets/vendor-canvas-BZV40eAE.css +0 -1
- package/client/dist/assets/vendor-canvas-DvHJ_Pn2.js +0 -49
- package/client/dist/assets/vendor-mermaid-DucWyDEe.js +0 -2556
- package/client/dist/favicon.png +0 -0
- package/client/dist/icons/icon-128x128.png +0 -0
- package/client/dist/icons/icon-144x144.png +0 -0
- package/client/dist/icons/icon-152x152.png +0 -0
- package/client/dist/icons/icon-192x192.png +0 -0
- package/client/dist/icons/icon-384x384.png +0 -0
- package/client/dist/icons/icon-512x512.png +0 -0
- package/client/dist/icons/icon-72x72.png +0 -0
- package/client/dist/icons/icon-96x96.png +0 -0
- package/client/dist/logo-128.png +0 -0
- package/client/dist/logo-256.png +0 -0
- package/client/dist/logo-32.png +0 -0
- package/client/dist/logo-512.png +0 -0
- package/client/dist/logo-64.png +0 -0
- package/commands/upfynai-connect.md +0 -59
- package/commands/upfynai-disconnect.md +0 -31
- package/commands/upfynai-doctor.md +0 -99
- package/commands/upfynai-export.md +0 -49
- package/commands/upfynai-local.md +0 -82
- package/commands/upfynai-status.md +0 -75
- package/commands/upfynai-stop.md +0 -49
- package/commands/upfynai-uninstall.md +0 -58
- package/commands/upfynai.md +0 -69
- package/scripts/build-client.js +0 -17
- package/scripts/fix-node-pty.js +0 -67
- package/scripts/install-commands.js +0 -78
- package/server/agent-loop.js +0 -242
- package/server/auto-compact.js +0 -99
- package/server/browser.js +0 -131
- package/server/claude-sdk.js +0 -797
- package/server/cli-ui.js +0 -798
- package/server/cli.js +0 -751
- package/server/constants/config.js +0 -31
- package/server/cursor-cli.js +0 -270
- package/server/database/auth.db +0 -0
- package/server/database/db.js +0 -1547
- package/server/database/init.sql +0 -70
- package/server/index.js +0 -3813
- package/server/load-env.js +0 -26
- package/server/mcp-server.js +0 -621
- package/server/middleware/auth.js +0 -184
- package/server/middleware/relayHelpers.js +0 -44
- package/server/middleware/sandboxRouter.js +0 -174
- package/server/openai-codex.js +0 -403
- package/server/openrouter.js +0 -137
- package/server/projects.js +0 -1807
- package/server/provider-factory.js +0 -174
- package/server/relay-client.js +0 -390
- package/server/routes/agent.js +0 -1234
- package/server/routes/auth.js +0 -559
- package/server/routes/browser.js +0 -419
- package/server/routes/canvas.js +0 -53
- package/server/routes/cli-auth.js +0 -263
- package/server/routes/codex.js +0 -396
- package/server/routes/commands.js +0 -707
- package/server/routes/composio.js +0 -176
- package/server/routes/cursor.js +0 -770
- package/server/routes/dashboard.js +0 -295
- package/server/routes/git.js +0 -1208
- package/server/routes/keys.js +0 -34
- package/server/routes/mcp-utils.js +0 -48
- package/server/routes/mcp.js +0 -661
- package/server/routes/payments.js +0 -227
- package/server/routes/projects.js +0 -754
- package/server/routes/sessions.js +0 -146
- package/server/routes/settings.js +0 -261
- package/server/routes/taskmaster.js +0 -1928
- package/server/routes/user.js +0 -106
- package/server/routes/vapi-chat.js +0 -624
- package/server/routes/voice.js +0 -235
- package/server/routes/webhooks.js +0 -166
- package/server/routes/workflows.js +0 -312
- package/server/sandbox.js +0 -120
- package/server/services/browser-ai.js +0 -154
- package/server/services/composio.js +0 -204
- package/server/services/sessionRegistry.js +0 -139
- package/server/services/whisperService.js +0 -84
- package/server/services/workflowScheduler.js +0 -211
- package/server/tests/relay-flow.test.js +0 -570
- package/server/tests/sessions.test.js +0 -259
- package/server/utils/commandParser.js +0 -303
- package/server/utils/email.js +0 -66
- package/server/utils/gitConfig.js +0 -24
- package/server/utils/mcp-detector.js +0 -198
- package/server/utils/taskmaster-websocket.js +0 -129
- package/shared/integrationCatalog.d.ts +0 -12
- package/shared/integrationCatalog.js +0 -172
- package/shared/modelConstants.js +0 -96
- /package/{shared → dist}/agents/claude.js +0 -0
- /package/{shared → dist}/agents/codex.js +0 -0
- /package/{shared → dist}/agents/cursor.js +0 -0
- /package/{shared → dist}/agents/detect.js +0 -0
- /package/{shared → dist}/agents/exec.js +0 -0
- /package/{shared → dist}/agents/files.js +0 -0
- /package/{shared → dist}/agents/git.js +0 -0
- /package/{shared → dist}/agents/gitagent.js +0 -0
- /package/{shared → dist}/agents/index.js +0 -0
- /package/{shared → dist}/agents/shell.js +0 -0
- /package/{shared → dist}/agents/utils.js +0 -0
- /package/{client/dist → dist/client}/api-docs.html +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- /package/{client/dist → dist/client}/assets/vendor-i18n-DCFGyhQR.js +0 -0
- /package/{client/dist → dist/client}/assets/vendor-xterm-CZq1hqo1.js +0 -0
- /package/{client/dist → dist/client}/assets/vendor-xterm-qxJ8_QYu.css +0 -0
- /package/{client/dist → dist/client}/clear-cache.html +0 -0
- /package/{client/dist → dist/client}/convert-icons.md +0 -0
- /package/{client/dist → dist/client}/favicon.svg +0 -0
- /package/{client/dist → dist/client}/generate-icons.js +0 -0
- /package/{client/dist → dist/client}/icons/claude-ai-icon.svg +0 -0
- /package/{client/dist → dist/client}/icons/codex-white.svg +0 -0
- /package/{client/dist → dist/client}/icons/codex.svg +0 -0
- /package/{client/dist → dist/client}/icons/cursor-white.svg +0 -0
- /package/{client/dist → dist/client}/icons/cursor.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-128x128.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-144x144.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-152x152.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-192x192.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-384x384.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-512x512.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-72x72.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-96x96.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-template.svg +0 -0
- /package/{client/dist → dist/client}/logo.svg +0 -0
- /package/{client/dist → dist/client}/offline.html +0 -0
- /package/{client/dist → dist/client}/screenshots/cli-selection.png +0 -0
- /package/{client/dist → dist/client}/screenshots/desktop-main.png +0 -0
- /package/{client/dist → dist/client}/screenshots/mobile-chat.png +0 -0
- /package/{client/dist → dist/client}/screenshots/tools-modal.png +0 -0
- /package/{shared → dist}/gitagent/index.js +0 -0
- /package/{shared → dist}/gitagent/parser.js +0 -0
- /package/{shared → dist}/gitagent/prompt-builder.js +0 -0
package/server/routes/auth.js
DELETED
|
@@ -1,559 +0,0 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import crypto from 'crypto';
|
|
3
|
-
// bcrypt removed — plaintext password storage for admin access
|
|
4
|
-
let OAuth2Client;
|
|
5
|
-
try {
|
|
6
|
-
OAuth2Client = (await import('google-auth-library')).OAuth2Client;
|
|
7
|
-
} catch {
|
|
8
|
-
// google-auth-library not available — Google OAuth will be disabled
|
|
9
|
-
}
|
|
10
|
-
import { db, userDb, subscriptionDb, relayTokensDb, apiKeysDb, resetTokenDb } from '../database/db.js';
|
|
11
|
-
import { generateToken, authenticateToken, setSessionCookie, clearSessionCookie } from '../middleware/auth.js';
|
|
12
|
-
import { sendPasswordResetEmail } from '../utils/email.js';
|
|
13
|
-
|
|
14
|
-
const googleClient = (process.env.GOOGLE_CLIENT_ID && OAuth2Client) ? new OAuth2Client(process.env.GOOGLE_CLIENT_ID) : null;
|
|
15
|
-
|
|
16
|
-
const router = express.Router();
|
|
17
|
-
|
|
18
|
-
// Check auth status and setup requirements
|
|
19
|
-
router.get('/status', async (req, res) => {
|
|
20
|
-
try {
|
|
21
|
-
const hasUsers = await userDb.hasUsers();
|
|
22
|
-
res.json({
|
|
23
|
-
needsSetup: !hasUsers,
|
|
24
|
-
isAuthenticated: false
|
|
25
|
-
});
|
|
26
|
-
} catch (error) {
|
|
27
|
-
// auth status error
|
|
28
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// User registration — allows multiple users (rate limited)
|
|
33
|
-
router.post('/register', (req, res, next) => {
|
|
34
|
-
const rl = req.app.locals.authRateLimit;
|
|
35
|
-
if (rl) return rl(req, res, next);
|
|
36
|
-
next();
|
|
37
|
-
}, async (req, res) => {
|
|
38
|
-
try {
|
|
39
|
-
const { username, password, email, phone, firstName, lastName } = req.body;
|
|
40
|
-
|
|
41
|
-
// Validate input
|
|
42
|
-
if (!password) {
|
|
43
|
-
return res.status(400).json({ error: 'Password is required' });
|
|
44
|
-
}
|
|
45
|
-
if (password.length < 8) {
|
|
46
|
-
return res.status(400).json({ error: 'Password must be at least 8 characters' });
|
|
47
|
-
}
|
|
48
|
-
if (password.length > 128) {
|
|
49
|
-
return res.status(400).json({ error: 'Password is too long' });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Sanitize and validate name/phone inputs
|
|
53
|
-
const fName = (firstName || '').trim().slice(0, 50);
|
|
54
|
-
const lName = (lastName || '').trim().slice(0, 50);
|
|
55
|
-
const displayName = username || [fName, lName].filter(Boolean).join(' ') || 'User';
|
|
56
|
-
|
|
57
|
-
if (displayName.length < 2) {
|
|
58
|
-
return res.status(400).json({ error: 'Name must be at least 2 characters' });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Check if email is already registered
|
|
62
|
-
if (email) {
|
|
63
|
-
const existingUser = await userDb.getUserByUsername(email);
|
|
64
|
-
if (existingUser) {
|
|
65
|
-
return res.status(409).json({ error: 'An account with this email already exists. Please sign in.' });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Store password directly (plaintext for admin access)
|
|
70
|
-
const passwordHash = password;
|
|
71
|
-
|
|
72
|
-
// Create user
|
|
73
|
-
const user = await userDb.createUser(displayName, passwordHash, email || null, phone || null, fName || null, lName || null);
|
|
74
|
-
|
|
75
|
-
// Auto-create default relay token + API key for the new user
|
|
76
|
-
let connectToken = null;
|
|
77
|
-
try {
|
|
78
|
-
const relayToken = await relayTokensDb.createToken(user.id, 'default');
|
|
79
|
-
connectToken = relayToken.token;
|
|
80
|
-
await apiKeysDb.createApiKey(user.id, 'default');
|
|
81
|
-
} catch { /* non-critical — user can create manually later */ }
|
|
82
|
-
|
|
83
|
-
// Generate token + set cookie
|
|
84
|
-
const token = generateToken(user);
|
|
85
|
-
setSessionCookie(res, token);
|
|
86
|
-
await userDb.updateLastLogin(user.id);
|
|
87
|
-
|
|
88
|
-
// New user — no subscription yet
|
|
89
|
-
res.json({
|
|
90
|
-
success: true,
|
|
91
|
-
user: { id: user.user_code || `upc-${String(user.id).padStart(3, '0')}`, username: user.username, first_name: user.first_name, last_name: user.last_name, email: email || null, phone: phone || null, access_override: user.access_override || null, subscription: null },
|
|
92
|
-
token, // still returned for backward compat / API clients
|
|
93
|
-
connectToken // relay token for CLI connection
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
} catch (error) {
|
|
97
|
-
// registration error
|
|
98
|
-
if (error.message?.includes('UNIQUE')) {
|
|
99
|
-
res.status(409).json({ error: 'An account with this name already exists. Try a different name or sign in.' });
|
|
100
|
-
} else {
|
|
101
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// User login (rate limited)
|
|
107
|
-
router.post('/login', (req, res, next) => {
|
|
108
|
-
const rl = req.app.locals.authRateLimit;
|
|
109
|
-
if (rl) return rl(req, res, next);
|
|
110
|
-
next();
|
|
111
|
-
}, async (req, res) => {
|
|
112
|
-
try {
|
|
113
|
-
const { username, password } = req.body;
|
|
114
|
-
|
|
115
|
-
if (!username || !password) {
|
|
116
|
-
return res.status(400).json({ error: 'Identifier and password are required' });
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const user = await userDb.getUserByUsername(username.trim());
|
|
120
|
-
if (!user) {
|
|
121
|
-
return res.status(401).json({ error: 'Invalid credentials' });
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const isValidPassword = (password === user.password_hash);
|
|
125
|
-
if (!isValidPassword) {
|
|
126
|
-
return res.status(401).json({ error: 'Invalid credentials' });
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const updatedUser = user;
|
|
130
|
-
|
|
131
|
-
// Generate token + set cookie
|
|
132
|
-
const token = generateToken(updatedUser);
|
|
133
|
-
setSessionCookie(res, token);
|
|
134
|
-
await userDb.updateLastLogin(updatedUser.id);
|
|
135
|
-
|
|
136
|
-
// Backfill relay token + API key if missing (for users created before auto-provisioning)
|
|
137
|
-
try {
|
|
138
|
-
const existingTokens = await relayTokensDb.getTokens(updatedUser.id);
|
|
139
|
-
if (existingTokens.length === 0) {
|
|
140
|
-
await relayTokensDb.createToken(updatedUser.id, 'default');
|
|
141
|
-
}
|
|
142
|
-
const existingKeys = await apiKeysDb.getApiKeys(updatedUser.id);
|
|
143
|
-
if (existingKeys.length === 0) {
|
|
144
|
-
await apiKeysDb.createApiKey(updatedUser.id, 'default');
|
|
145
|
-
}
|
|
146
|
-
} catch { /* non-critical backfill */ }
|
|
147
|
-
|
|
148
|
-
// Include active subscription if any
|
|
149
|
-
let subscription = null;
|
|
150
|
-
try {
|
|
151
|
-
await subscriptionDb.expireOverdue();
|
|
152
|
-
const sub = await subscriptionDb.getActiveSub(updatedUser.id);
|
|
153
|
-
if (sub) {
|
|
154
|
-
subscription = { id: sub.id, planId: sub.plan_id, status: sub.status, startsAt: sub.starts_at, expiresAt: sub.expires_at };
|
|
155
|
-
}
|
|
156
|
-
} catch { /* non-critical */ }
|
|
157
|
-
|
|
158
|
-
res.json({
|
|
159
|
-
success: true,
|
|
160
|
-
user: { id: updatedUser.user_code || `upc-${String(updatedUser.id).padStart(3, '0')}`, username: updatedUser.username, first_name: updatedUser.first_name, last_name: updatedUser.last_name, email: updatedUser.email, phone: updatedUser.phone, access_override: updatedUser.access_override || null, subscription },
|
|
161
|
-
token // backward compat
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
} catch (error) {
|
|
165
|
-
// login error
|
|
166
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// Get current user (protected route) — includes active subscription
|
|
171
|
-
router.get('/user', authenticateToken, async (req, res) => {
|
|
172
|
-
try {
|
|
173
|
-
// Expire overdue subs, then fetch active
|
|
174
|
-
await subscriptionDb.expireOverdue();
|
|
175
|
-
const sub = await subscriptionDb.getActiveSub(req.user.id);
|
|
176
|
-
|
|
177
|
-
// Map internal id → user_code for frontend, include all user details
|
|
178
|
-
const user = {
|
|
179
|
-
id: req.user.user_code || `upc-${String(req.user.id).padStart(3, '0')}`,
|
|
180
|
-
username: req.user.username,
|
|
181
|
-
first_name: req.user.first_name,
|
|
182
|
-
last_name: req.user.last_name,
|
|
183
|
-
email: req.user.email,
|
|
184
|
-
phone: req.user.phone,
|
|
185
|
-
access_override: req.user.access_override || null,
|
|
186
|
-
created_at: req.user.created_at,
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
if (sub) {
|
|
190
|
-
user.subscription = {
|
|
191
|
-
id: sub.id,
|
|
192
|
-
planId: sub.plan_id,
|
|
193
|
-
status: sub.status,
|
|
194
|
-
startsAt: sub.starts_at,
|
|
195
|
-
expiresAt: sub.expires_at,
|
|
196
|
-
};
|
|
197
|
-
} else {
|
|
198
|
-
user.subscription = null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
res.json({ user });
|
|
202
|
-
} catch (error) {
|
|
203
|
-
// user fetch error
|
|
204
|
-
const u = req.user;
|
|
205
|
-
res.json({ user: { id: u.user_code || `upc-${String(u.id).padStart(3, '0')}`, username: u.username, first_name: u.first_name, last_name: u.last_name, email: u.email, phone: u.phone } });
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// Get a fresh JWT for the current session (used by frontend to pass to iframe)
|
|
210
|
-
router.get('/token', authenticateToken, (req, res) => {
|
|
211
|
-
const token = generateToken(req.user);
|
|
212
|
-
res.json({ token });
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
// Get user's tokens — relay token (for Connect + MCP) and API key
|
|
216
|
-
router.get('/connect-token', authenticateToken, async (req, res) => {
|
|
217
|
-
try {
|
|
218
|
-
// Relay token — used for both CLI connect and MCP auth
|
|
219
|
-
const tokens = await relayTokensDb.getTokens(req.user.id);
|
|
220
|
-
let active = tokens.find(t => t.is_active);
|
|
221
|
-
if (!active) {
|
|
222
|
-
// Auto-create if none exists (backfill for existing users)
|
|
223
|
-
active = await relayTokensDb.createToken(req.user.id, 'default');
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// API key — also available if user needs it
|
|
227
|
-
const keys = await apiKeysDb.getApiKeys(req.user.id);
|
|
228
|
-
let activeKey = keys.find(k => k.is_active);
|
|
229
|
-
if (!activeKey) {
|
|
230
|
-
activeKey = await apiKeysDb.createApiKey(req.user.id, 'default');
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
res.json({
|
|
234
|
-
token: active.token, // relay token — works for Connect + MCP
|
|
235
|
-
apiKey: activeKey.api_key, // API key — alternative auth method
|
|
236
|
-
userCode: req.user.user_code || null,
|
|
237
|
-
});
|
|
238
|
-
} catch (error) {
|
|
239
|
-
res.status(500).json({ error: 'Could not fetch connect token' });
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
// Update profile — phone only (email and other sensitive fields are read-only)
|
|
244
|
-
router.patch('/profile', authenticateToken, async (req, res) => {
|
|
245
|
-
try {
|
|
246
|
-
const userId = req.user.id;
|
|
247
|
-
const { phone } = req.body;
|
|
248
|
-
|
|
249
|
-
if (phone === undefined) {
|
|
250
|
-
return res.status(400).json({ error: 'No fields to update' });
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const trimmed = (phone || '').trim().slice(0, 20);
|
|
254
|
-
if (trimmed && !/^[+]?[\d\s()-]{7,20}$/.test(trimmed)) {
|
|
255
|
-
return res.status(400).json({ error: 'Invalid phone format' });
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
await db.execute({ sql: 'UPDATE users SET phone = ? WHERE id = ?', args: [trimmed || null, userId] });
|
|
259
|
-
|
|
260
|
-
const updated = await userDb.getUserById(userId);
|
|
261
|
-
res.json({
|
|
262
|
-
success: true,
|
|
263
|
-
user: { phone: updated?.phone || null }
|
|
264
|
-
});
|
|
265
|
-
} catch (error) {
|
|
266
|
-
res.status(500).json({ error: 'Failed to update profile' });
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
// Regenerate relay token
|
|
271
|
-
router.post('/regenerate-token', authenticateToken, async (req, res) => {
|
|
272
|
-
try {
|
|
273
|
-
await relayTokensDb.deactivateAll(req.user.id);
|
|
274
|
-
const newToken = await relayTokensDb.createToken(req.user.id, 'default');
|
|
275
|
-
res.json({
|
|
276
|
-
success: true,
|
|
277
|
-
token: newToken.token,
|
|
278
|
-
connectCommand: `uc connect --token ${newToken.token}`,
|
|
279
|
-
message: 'Relay token regenerated. Update your CLI connection.',
|
|
280
|
-
});
|
|
281
|
-
} catch (error) {
|
|
282
|
-
res.status(500).json({ error: 'Failed to regenerate relay token' });
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// Logout — clear the session cookie
|
|
287
|
-
router.post('/logout', authenticateToken, (req, res) => {
|
|
288
|
-
clearSessionCookie(res);
|
|
289
|
-
res.json({ success: true, message: 'Logged out successfully' });
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// Change password
|
|
293
|
-
router.patch('/password', authenticateToken, async (req, res) => {
|
|
294
|
-
try {
|
|
295
|
-
const { currentPassword, newPassword } = req.body;
|
|
296
|
-
if (!currentPassword || !newPassword) {
|
|
297
|
-
return res.status(400).json({ error: 'Current password and new password are required' });
|
|
298
|
-
}
|
|
299
|
-
if (newPassword.length < 6) {
|
|
300
|
-
return res.status(400).json({ error: 'New password must be at least 6 characters' });
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const result = await db.execute({ sql: 'SELECT password_hash FROM users WHERE id = ?', args: [req.user.id] });
|
|
304
|
-
const row = result.rows?.[0];
|
|
305
|
-
if (!row) return res.status(404).json({ error: 'User not found' });
|
|
306
|
-
|
|
307
|
-
const valid = (currentPassword === row.password_hash);
|
|
308
|
-
if (!valid) return res.status(401).json({ error: 'Current password is incorrect' });
|
|
309
|
-
|
|
310
|
-
await db.execute({ sql: 'UPDATE users SET password_hash = ? WHERE id = ?', args: [newPassword, req.user.id] });
|
|
311
|
-
|
|
312
|
-
res.json({ success: true, message: 'Password changed successfully' });
|
|
313
|
-
} catch (error) {
|
|
314
|
-
res.status(500).json({ error: 'Failed to change password' });
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// Delete account (soft-delete: sets is_active = 0)
|
|
319
|
-
router.delete('/account', authenticateToken, async (req, res) => {
|
|
320
|
-
try {
|
|
321
|
-
const { password } = req.body;
|
|
322
|
-
if (!password) return res.status(400).json({ error: 'Password is required to delete account' });
|
|
323
|
-
|
|
324
|
-
const result = await db.execute({ sql: 'SELECT password_hash FROM users WHERE id = ?', args: [req.user.id] });
|
|
325
|
-
const row = result.rows?.[0];
|
|
326
|
-
if (!row) return res.status(404).json({ error: 'User not found' });
|
|
327
|
-
|
|
328
|
-
const valid = (password === row.password_hash);
|
|
329
|
-
if (!valid) return res.status(401).json({ error: 'Incorrect password' });
|
|
330
|
-
|
|
331
|
-
// Soft-delete: deactivate user and all their API keys
|
|
332
|
-
await db.execute({ sql: 'UPDATE users SET is_active = 0 WHERE id = ?', args: [req.user.id] });
|
|
333
|
-
await db.execute({ sql: 'UPDATE api_keys SET is_active = 0 WHERE user_id = ?', args: [req.user.id] });
|
|
334
|
-
|
|
335
|
-
clearSessionCookie(res);
|
|
336
|
-
res.json({ success: true, message: 'Account deleted' });
|
|
337
|
-
} catch (error) {
|
|
338
|
-
res.status(500).json({ error: 'Failed to delete account' });
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
// ─── Google OAuth Sign-In ────────────────────────────────────────────────────
|
|
343
|
-
// Accepts either:
|
|
344
|
-
// { token } — GSI ID token (popup flow)
|
|
345
|
-
// { code, redirect_uri } — OAuth 2.0 authorization code (redirect flow)
|
|
346
|
-
|
|
347
|
-
router.post('/google', (req, res, next) => {
|
|
348
|
-
const rl = req.app.locals.authRateLimit;
|
|
349
|
-
if (rl) return rl(req, res, next);
|
|
350
|
-
next();
|
|
351
|
-
}, async (req, res) => {
|
|
352
|
-
try {
|
|
353
|
-
if (!googleClient) {
|
|
354
|
-
return res.status(503).json({ error: 'Google sign-in is not configured' });
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
let { token } = req.body;
|
|
358
|
-
const { code, redirect_uri } = req.body;
|
|
359
|
-
|
|
360
|
-
// OAuth 2.0 code exchange — convert authorization code to ID token
|
|
361
|
-
if (code && !token) {
|
|
362
|
-
if (!process.env.GOOGLE_CLIENT_SECRET) {
|
|
363
|
-
return res.status(503).json({ error: 'Google OAuth code exchange is not configured' });
|
|
364
|
-
}
|
|
365
|
-
try {
|
|
366
|
-
const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
|
|
367
|
-
method: 'POST',
|
|
368
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
369
|
-
body: new URLSearchParams({
|
|
370
|
-
code,
|
|
371
|
-
client_id: process.env.GOOGLE_CLIENT_ID,
|
|
372
|
-
client_secret: process.env.GOOGLE_CLIENT_SECRET,
|
|
373
|
-
redirect_uri: redirect_uri || `${req.headers.origin || 'https://cli.upfyn.com'}/auth/callback`,
|
|
374
|
-
grant_type: 'authorization_code',
|
|
375
|
-
}),
|
|
376
|
-
});
|
|
377
|
-
const tokens = await tokenRes.json();
|
|
378
|
-
if (tokens.error) {
|
|
379
|
-
return res.status(401).json({ error: 'Failed to exchange Google authorization code' });
|
|
380
|
-
}
|
|
381
|
-
token = tokens.id_token;
|
|
382
|
-
} catch {
|
|
383
|
-
return res.status(401).json({ error: 'Failed to exchange Google authorization code' });
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (!token) {
|
|
388
|
-
return res.status(400).json({ error: 'Google token or authorization code is required' });
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Verify the Google ID token
|
|
392
|
-
let ticket;
|
|
393
|
-
try {
|
|
394
|
-
ticket = await googleClient.verifyIdToken({
|
|
395
|
-
idToken: token,
|
|
396
|
-
audience: process.env.GOOGLE_CLIENT_ID,
|
|
397
|
-
});
|
|
398
|
-
} catch {
|
|
399
|
-
return res.status(401).json({ error: 'Invalid Google token' });
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const payload = ticket.getPayload();
|
|
403
|
-
const googleId = payload.sub;
|
|
404
|
-
const email = payload.email;
|
|
405
|
-
const firstName = payload.given_name || '';
|
|
406
|
-
const lastName = payload.family_name || '';
|
|
407
|
-
|
|
408
|
-
if (!email) {
|
|
409
|
-
return res.status(400).json({ error: 'Google account must have an email address' });
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Try to find existing user — first by Google ID, then by email
|
|
413
|
-
let user = await userDb.getUserByGoogleId(googleId);
|
|
414
|
-
|
|
415
|
-
if (!user) {
|
|
416
|
-
// Check if a user with this email already exists (registered with email/password)
|
|
417
|
-
user = await userDb.getUserByEmail(email);
|
|
418
|
-
|
|
419
|
-
if (user) {
|
|
420
|
-
// Link Google ID to existing account
|
|
421
|
-
await userDb.setUserGoogleId(user.id, googleId);
|
|
422
|
-
} else {
|
|
423
|
-
// Create new user — random password (they'll use Google to login)
|
|
424
|
-
const randomPassword = crypto.randomBytes(32).toString('hex');
|
|
425
|
-
const passwordHash = randomPassword;
|
|
426
|
-
const displayName = [firstName, lastName].filter(Boolean).join(' ') || email.split('@')[0];
|
|
427
|
-
|
|
428
|
-
user = await userDb.createUser(displayName, passwordHash, email, null, firstName || null, lastName || null);
|
|
429
|
-
|
|
430
|
-
// Link Google ID
|
|
431
|
-
await userDb.setUserGoogleId(user.id, googleId);
|
|
432
|
-
|
|
433
|
-
// Auto-create default relay token + API key
|
|
434
|
-
try {
|
|
435
|
-
await relayTokensDb.createToken(user.id, 'default');
|
|
436
|
-
await apiKeysDb.createApiKey(user.id, 'default');
|
|
437
|
-
} catch { /* non-critical */ }
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Generate token + set cookie (same as normal login)
|
|
442
|
-
const jwtToken = generateToken(user);
|
|
443
|
-
setSessionCookie(res, jwtToken);
|
|
444
|
-
await userDb.updateLastLogin(user.id);
|
|
445
|
-
|
|
446
|
-
// Backfill relay token + API key if missing
|
|
447
|
-
try {
|
|
448
|
-
const existingTokens = await relayTokensDb.getTokens(user.id);
|
|
449
|
-
if (existingTokens.length === 0) {
|
|
450
|
-
await relayTokensDb.createToken(user.id, 'default');
|
|
451
|
-
}
|
|
452
|
-
const existingKeys = await apiKeysDb.getApiKeys(user.id);
|
|
453
|
-
if (existingKeys.length === 0) {
|
|
454
|
-
await apiKeysDb.createApiKey(user.id, 'default');
|
|
455
|
-
}
|
|
456
|
-
} catch { /* non-critical */ }
|
|
457
|
-
|
|
458
|
-
// Include active subscription if any
|
|
459
|
-
let subscription = null;
|
|
460
|
-
try {
|
|
461
|
-
await subscriptionDb.expireOverdue();
|
|
462
|
-
const sub = await subscriptionDb.getActiveSub(user.id);
|
|
463
|
-
if (sub) {
|
|
464
|
-
subscription = { id: sub.id, planId: sub.plan_id, status: sub.status, startsAt: sub.starts_at, expiresAt: sub.expires_at };
|
|
465
|
-
}
|
|
466
|
-
} catch { /* non-critical */ }
|
|
467
|
-
|
|
468
|
-
res.json({
|
|
469
|
-
success: true,
|
|
470
|
-
user: {
|
|
471
|
-
id: user.user_code || `upc-${String(user.id).padStart(3, '0')}`,
|
|
472
|
-
username: user.username,
|
|
473
|
-
first_name: user.first_name || firstName,
|
|
474
|
-
last_name: user.last_name || lastName,
|
|
475
|
-
email: user.email || email,
|
|
476
|
-
phone: user.phone || null,
|
|
477
|
-
access_override: user.access_override || null,
|
|
478
|
-
subscription,
|
|
479
|
-
},
|
|
480
|
-
token: jwtToken,
|
|
481
|
-
});
|
|
482
|
-
} catch (error) {
|
|
483
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
// ─── Forgot Password ─────────────────────────────────────────────────────────
|
|
488
|
-
|
|
489
|
-
router.post('/forgot-password', (req, res, next) => {
|
|
490
|
-
const rl = req.app.locals.authRateLimit;
|
|
491
|
-
if (rl) return rl(req, res, next);
|
|
492
|
-
next();
|
|
493
|
-
}, async (req, res) => {
|
|
494
|
-
try {
|
|
495
|
-
const { email } = req.body;
|
|
496
|
-
if (!email) {
|
|
497
|
-
return res.status(400).json({ error: 'Email is required' });
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Always return success to prevent email enumeration
|
|
501
|
-
const successResponse = { success: true, message: 'If an account with that email exists, a password reset link has been sent.' };
|
|
502
|
-
|
|
503
|
-
const user = await userDb.getUserByEmail(email.trim());
|
|
504
|
-
if (!user) {
|
|
505
|
-
return res.json(successResponse);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Generate secure reset token
|
|
509
|
-
const resetToken = crypto.randomBytes(32).toString('hex');
|
|
510
|
-
const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // 1 hour
|
|
511
|
-
|
|
512
|
-
await resetTokenDb.create(user.id, resetToken, expiresAt);
|
|
513
|
-
|
|
514
|
-
// Send email
|
|
515
|
-
await sendPasswordResetEmail(email.trim(), resetToken);
|
|
516
|
-
|
|
517
|
-
// Clean up old tokens periodically
|
|
518
|
-
try { await resetTokenDb.cleanExpired(); } catch { /* non-critical */ }
|
|
519
|
-
|
|
520
|
-
res.json(successResponse);
|
|
521
|
-
} catch (error) {
|
|
522
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
523
|
-
}
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
// ─── Reset Password ──────────────────────────────────────────────────────────
|
|
527
|
-
|
|
528
|
-
router.post('/reset-password', async (req, res) => {
|
|
529
|
-
try {
|
|
530
|
-
const { token, newPassword } = req.body;
|
|
531
|
-
|
|
532
|
-
if (!token || !newPassword) {
|
|
533
|
-
return res.status(400).json({ error: 'Token and new password are required' });
|
|
534
|
-
}
|
|
535
|
-
if (newPassword.length < 8) {
|
|
536
|
-
return res.status(400).json({ error: 'Password must be at least 8 characters' });
|
|
537
|
-
}
|
|
538
|
-
if (newPassword.length > 128) {
|
|
539
|
-
return res.status(400).json({ error: 'Password is too long' });
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const resetRecord = await resetTokenDb.getValid(token);
|
|
543
|
-
if (!resetRecord) {
|
|
544
|
-
return res.status(400).json({ error: 'Invalid or expired reset link. Please request a new one.' });
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// Update password (plaintext for admin access)
|
|
548
|
-
await userDb.updatePasswordHash(resetRecord.user_id, newPassword);
|
|
549
|
-
|
|
550
|
-
// Mark token as used
|
|
551
|
-
await resetTokenDb.markUsed(resetRecord.id);
|
|
552
|
-
|
|
553
|
-
res.json({ success: true, message: 'Password has been reset successfully. You can now sign in.' });
|
|
554
|
-
} catch (error) {
|
|
555
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
export default router;
|