upfynai-code 3.0.3 → 3.1.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.
Files changed (243) hide show
  1. package/README.md +66 -91
  2. package/bin/cli.js +191 -0
  3. package/{client/dist → dist/client}/api-docs.html +838 -838
  4. package/{client/dist/assets/AppContent-Bvg0CPCO.js → dist/client/assets/AppContent-BofJquUs.js} +43 -43
  5. package/dist/client/assets/BrowserPanel-CSvD4jOX.js +2 -0
  6. package/dist/client/assets/CanvasFullScreen-onRfarpc.js +1 -0
  7. package/dist/client/assets/CanvasWorkspace-DvGKdL-k.js +259 -0
  8. package/dist/client/assets/DashboardPanel-DqAHbXDO.js +1 -0
  9. package/dist/client/assets/FileTree-BE0h-9M9.js +1 -0
  10. package/{client/dist/assets/GitPanel-RtyZUIWS.js → dist/client/assets/GitPanel-DdeJ0bp5.js} +2 -2
  11. package/{client/dist/assets/LoginModal-BWep8a6g.js → dist/client/assets/LoginModal-BP0pCTrH.js} +3 -3
  12. package/{client/dist/assets/MarkdownPreview-DHmk3qzu.js → dist/client/assets/MarkdownPreview-CESjI261.js} +1 -1
  13. package/dist/client/assets/MermaidBlock-D0rfEhrT.js +2 -0
  14. package/dist/client/assets/Onboarding-B2zQy-_6.js +1 -0
  15. package/dist/client/assets/SetupForm-Be7-WBe-.js +1 -0
  16. package/dist/client/assets/WorkflowsPanel-CusLbVJ6.js +1 -0
  17. package/{client/dist/assets/index-C5ptjuTl.js → dist/client/assets/index-BQy15irW.js} +25 -25
  18. package/dist/client/assets/index-CS0fDqEC.js +1 -0
  19. package/dist/client/assets/index-DYLSCCCp.css +1 -0
  20. package/dist/client/assets/vendor-canvas-QWTduIvM.js +23 -0
  21. package/{client/dist/assets/vendor-codemirror-CbtmxxaB.js → dist/client/assets/vendor-codemirror-D2ALgpaX.js} +1 -1
  22. package/{client/dist/assets/vendor-icons-BaD0x9SL.js → dist/client/assets/vendor-icons-kix3Gb31.js} +178 -138
  23. package/{client/dist/assets/vendor-mermaid-CH7SGc99.js → dist/client/assets/vendor-mermaid-CS3J4_Bz.js} +329 -326
  24. package/{client/dist/assets/vendor-syntax-DuHI9Ok6.js → dist/client/assets/vendor-syntax-LS_Nt30I.js} +1 -1
  25. package/{client/dist → dist/client}/clear-cache.html +85 -85
  26. package/dist/client/favicon.png +0 -0
  27. package/dist/client/favicon.svg +15 -0
  28. package/{client/dist → dist/client}/index.html +17 -17
  29. package/{client/dist → dist/client}/manifest.json +15 -15
  30. package/{client/dist → dist/client}/mcp-docs.html +108 -108
  31. package/{client/dist → dist/client}/offline.html +84 -84
  32. package/{client/dist → dist/client}/sw.js +82 -82
  33. package/package.json +55 -104
  34. package/scripts/postinstall.js +9 -0
  35. package/scripts/prepublish.js +77 -0
  36. package/src/animation.js +228 -0
  37. package/src/auth.js +142 -0
  38. package/src/config.js +40 -0
  39. package/src/connect.js +416 -0
  40. package/src/launch.js +81 -0
  41. package/src/mcp.js +57 -0
  42. package/src/permissions.js +140 -0
  43. package/src/persistent-shell.js +261 -0
  44. package/src/server.js +54 -0
  45. package/client/dist/assets/CanvasFullScreen-BdiJ35aq.js +0 -1
  46. package/client/dist/assets/CanvasWorkspace-Bk9R9_e0.js +0 -163
  47. package/client/dist/assets/DashboardPanel-CblJfTGi.js +0 -1
  48. package/client/dist/assets/FileTree-BDUnBheV.js +0 -1
  49. package/client/dist/assets/MermaidBlock-BuBc_G-F.js +0 -2
  50. package/client/dist/assets/Onboarding-Drnlt75a.js +0 -1
  51. package/client/dist/assets/SetupForm-CtCKitZG.js +0 -1
  52. package/client/dist/assets/WorkflowsPanel-B2mIXDvD.js +0 -1
  53. package/client/dist/assets/index-BFuqS0tY.css +0 -1
  54. package/client/dist/assets/vendor-canvas-D39yWul6.js +0 -49
  55. package/client/dist/favicon.png +0 -0
  56. package/client/dist/favicon.svg +0 -5
  57. package/commands/upfynai-connect.md +0 -59
  58. package/commands/upfynai-disconnect.md +0 -31
  59. package/commands/upfynai-doctor.md +0 -99
  60. package/commands/upfynai-export.md +0 -49
  61. package/commands/upfynai-local.md +0 -82
  62. package/commands/upfynai-status.md +0 -75
  63. package/commands/upfynai-stop.md +0 -49
  64. package/commands/upfynai-uninstall.md +0 -58
  65. package/commands/upfynai.md +0 -69
  66. package/scripts/build-client.js +0 -17
  67. package/scripts/fix-node-pty.js +0 -67
  68. package/scripts/install-commands.js +0 -78
  69. package/server/agent-loop.js +0 -242
  70. package/server/auto-compact.js +0 -99
  71. package/server/claude-sdk.js +0 -797
  72. package/server/cli-ui.js +0 -798
  73. package/server/cli.js +0 -751
  74. package/server/constants/config.js +0 -31
  75. package/server/cursor-cli.js +0 -270
  76. package/server/database/auth.db +0 -0
  77. package/server/database/db.js +0 -1451
  78. package/server/database/init.sql +0 -70
  79. package/server/index.js +0 -3814
  80. package/server/load-env.js +0 -26
  81. package/server/mcp-server.js +0 -621
  82. package/server/middleware/auth.js +0 -181
  83. package/server/middleware/relayHelpers.js +0 -44
  84. package/server/middleware/sandboxRouter.js +0 -174
  85. package/server/openai-codex.js +0 -403
  86. package/server/openrouter.js +0 -137
  87. package/server/projects.js +0 -1807
  88. package/server/provider-factory.js +0 -174
  89. package/server/relay-client.js +0 -390
  90. package/server/routes/agent.js +0 -1234
  91. package/server/routes/auth.js +0 -559
  92. package/server/routes/canvas.js +0 -53
  93. package/server/routes/cli-auth.js +0 -263
  94. package/server/routes/codex.js +0 -396
  95. package/server/routes/commands.js +0 -707
  96. package/server/routes/composio.js +0 -176
  97. package/server/routes/cursor.js +0 -770
  98. package/server/routes/dashboard.js +0 -295
  99. package/server/routes/git.js +0 -1208
  100. package/server/routes/keys.js +0 -34
  101. package/server/routes/mcp-utils.js +0 -48
  102. package/server/routes/mcp.js +0 -661
  103. package/server/routes/payments.js +0 -227
  104. package/server/routes/projects.js +0 -655
  105. package/server/routes/sessions.js +0 -146
  106. package/server/routes/settings.js +0 -261
  107. package/server/routes/taskmaster.js +0 -1928
  108. package/server/routes/user.js +0 -106
  109. package/server/routes/vapi-chat.js +0 -624
  110. package/server/routes/voice.js +0 -235
  111. package/server/routes/webhooks.js +0 -166
  112. package/server/routes/workflows.js +0 -312
  113. package/server/sandbox.js +0 -120
  114. package/server/services/composio.js +0 -204
  115. package/server/services/sessionRegistry.js +0 -139
  116. package/server/services/whisperService.js +0 -84
  117. package/server/services/workflowScheduler.js +0 -211
  118. package/server/tests/relay-flow.test.js +0 -570
  119. package/server/tests/sessions.test.js +0 -259
  120. package/server/utils/commandParser.js +0 -303
  121. package/server/utils/email.js +0 -66
  122. package/server/utils/gitConfig.js +0 -24
  123. package/server/utils/mcp-detector.js +0 -198
  124. package/server/utils/taskmaster-websocket.js +0 -129
  125. package/shared/integrationCatalog.d.ts +0 -12
  126. package/shared/integrationCatalog.js +0 -172
  127. package/shared/modelConstants.js +0 -96
  128. /package/{shared → dist}/agents/claude.js +0 -0
  129. /package/{shared → dist}/agents/codex.js +0 -0
  130. /package/{shared → dist}/agents/cursor.js +0 -0
  131. /package/{shared → dist}/agents/detect.js +0 -0
  132. /package/{shared → dist}/agents/exec.js +0 -0
  133. /package/{shared → dist}/agents/files.js +0 -0
  134. /package/{shared → dist}/agents/git.js +0 -0
  135. /package/{shared → dist}/agents/gitagent.js +0 -0
  136. /package/{shared → dist}/agents/index.js +0 -0
  137. /package/{shared → dist}/agents/shell.js +0 -0
  138. /package/{shared → dist}/agents/utils.js +0 -0
  139. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  140. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  141. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  142. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  143. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  144. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  145. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  146. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  147. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  148. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  149. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  150. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  151. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  152. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  153. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  154. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  155. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  156. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  157. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  158. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  159. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  160. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  161. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  162. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  163. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  164. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  165. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  166. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  167. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  168. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  169. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  170. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  171. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  172. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  173. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  174. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  175. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  176. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  177. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  178. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  179. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  180. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  181. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  182. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  183. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  184. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  185. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  186. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  187. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  188. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  189. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  190. /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  191. /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  192. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  193. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  194. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  195. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  196. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  197. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  198. /package/{client/dist → dist/client}/assets/PreviewPanel-CqCa92Tf.js +0 -0
  199. /package/{client/dist → dist/client}/assets/pdf-CE_K4jFx.js +0 -0
  200. /package/{client/dist → dist/client}/assets/vendor-canvas-BZV40eAE.css +0 -0
  201. /package/{client/dist → dist/client}/assets/vendor-diff-DNQpbhrT.js +0 -0
  202. /package/{client/dist → dist/client}/assets/vendor-i18n-DCFGyhQR.js +0 -0
  203. /package/{client/dist → dist/client}/assets/vendor-markdown-CimbIo6Y.js +0 -0
  204. /package/{client/dist → dist/client}/assets/vendor-react-96lCPsRK.js +0 -0
  205. /package/{client/dist → dist/client}/assets/vendor-xterm-CZq1hqo1.js +0 -0
  206. /package/{client/dist → dist/client}/assets/vendor-xterm-qxJ8_QYu.css +0 -0
  207. /package/{client/dist → dist/client}/convert-icons.md +0 -0
  208. /package/{client/dist → dist/client}/generate-icons.js +0 -0
  209. /package/{client/dist → dist/client}/icons/claude-ai-icon.svg +0 -0
  210. /package/{client/dist → dist/client}/icons/codex-white.svg +0 -0
  211. /package/{client/dist → dist/client}/icons/codex.svg +0 -0
  212. /package/{client/dist → dist/client}/icons/cursor-white.svg +0 -0
  213. /package/{client/dist → dist/client}/icons/cursor.svg +0 -0
  214. /package/{client/dist → dist/client}/icons/icon-128x128.png +0 -0
  215. /package/{client/dist → dist/client}/icons/icon-128x128.svg +0 -0
  216. /package/{client/dist → dist/client}/icons/icon-144x144.png +0 -0
  217. /package/{client/dist → dist/client}/icons/icon-144x144.svg +0 -0
  218. /package/{client/dist → dist/client}/icons/icon-152x152.png +0 -0
  219. /package/{client/dist → dist/client}/icons/icon-152x152.svg +0 -0
  220. /package/{client/dist → dist/client}/icons/icon-192x192.png +0 -0
  221. /package/{client/dist → dist/client}/icons/icon-192x192.svg +0 -0
  222. /package/{client/dist → dist/client}/icons/icon-384x384.png +0 -0
  223. /package/{client/dist → dist/client}/icons/icon-384x384.svg +0 -0
  224. /package/{client/dist → dist/client}/icons/icon-512x512.png +0 -0
  225. /package/{client/dist → dist/client}/icons/icon-512x512.svg +0 -0
  226. /package/{client/dist → dist/client}/icons/icon-72x72.png +0 -0
  227. /package/{client/dist → dist/client}/icons/icon-72x72.svg +0 -0
  228. /package/{client/dist → dist/client}/icons/icon-96x96.png +0 -0
  229. /package/{client/dist → dist/client}/icons/icon-96x96.svg +0 -0
  230. /package/{client/dist → dist/client}/icons/icon-template.svg +0 -0
  231. /package/{client/dist → dist/client}/logo-128.png +0 -0
  232. /package/{client/dist → dist/client}/logo-256.png +0 -0
  233. /package/{client/dist → dist/client}/logo-32.png +0 -0
  234. /package/{client/dist → dist/client}/logo-512.png +0 -0
  235. /package/{client/dist → dist/client}/logo-64.png +0 -0
  236. /package/{client/dist → dist/client}/logo.svg +0 -0
  237. /package/{client/dist → dist/client}/screenshots/cli-selection.png +0 -0
  238. /package/{client/dist → dist/client}/screenshots/desktop-main.png +0 -0
  239. /package/{client/dist → dist/client}/screenshots/mobile-chat.png +0 -0
  240. /package/{client/dist → dist/client}/screenshots/tools-modal.png +0 -0
  241. /package/{shared → dist}/gitagent/index.js +0 -0
  242. /package/{shared → dist}/gitagent/parser.js +0 -0
  243. /package/{shared → dist}/gitagent/prompt-builder.js +0 -0
@@ -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;
@@ -1,53 +0,0 @@
1
- import express from 'express';
2
- import { canvasDb } from '../database/db.js';
3
-
4
- const router = express.Router();
5
-
6
- // GET /api/canvas/:projectName — load canvas for a project
7
- router.get('/:projectName', async (req, res) => {
8
- try {
9
- const userId = req.user.id;
10
- const { projectName } = req.params;
11
-
12
- const canvas = await canvasDb.load(userId, decodeURIComponent(projectName));
13
- if (!canvas) {
14
- return res.json({ elements: [], appState: {}, updatedAt: null });
15
- }
16
- res.json(canvas);
17
- } catch (err) {
18
- res.status(500).json({ error: 'Failed to load canvas' });
19
- }
20
- });
21
-
22
- // POST /api/canvas/:projectName — save canvas for a project
23
- router.post('/:projectName', async (req, res) => {
24
- try {
25
- const userId = req.user.id;
26
- const { projectName } = req.params;
27
- const { elements, appState } = req.body;
28
-
29
- if (!Array.isArray(elements)) {
30
- return res.status(400).json({ error: 'elements must be an array' });
31
- }
32
-
33
- await canvasDb.save(userId, decodeURIComponent(projectName), elements, appState || {});
34
- res.json({ ok: true });
35
- } catch (err) {
36
- res.status(500).json({ error: 'Failed to save canvas' });
37
- }
38
- });
39
-
40
- // DELETE /api/canvas/:projectName — delete canvas for a project
41
- router.delete('/:projectName', async (req, res) => {
42
- try {
43
- const userId = req.user.id;
44
- const { projectName } = req.params;
45
-
46
- const deleted = await canvasDb.delete(userId, decodeURIComponent(projectName));
47
- res.json({ ok: true, deleted });
48
- } catch (err) {
49
- res.status(500).json({ error: 'Failed to delete canvas' });
50
- }
51
- });
52
-
53
- export default router;