@knotpad/app 0.1.0 → 0.1.2

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 (341) hide show
  1. package/bin/brief.js +165 -78
  2. package/package.json +6 -86
  3. package/app/(app)/calendar/page.tsx +0 -57
  4. package/app/(app)/error.tsx +0 -35
  5. package/app/(app)/graph/page.tsx +0 -32
  6. package/app/(app)/guide/page.tsx +0 -21
  7. package/app/(app)/kanban/loading.tsx +0 -24
  8. package/app/(app)/kanban/page.tsx +0 -59
  9. package/app/(app)/layout.tsx +0 -122
  10. package/app/(app)/list/loading.tsx +0 -21
  11. package/app/(app)/list/page.tsx +0 -137
  12. package/app/(app)/loading.tsx +0 -18
  13. package/app/(app)/notes/[noteId]/page.tsx +0 -84
  14. package/app/(app)/notes/layout.tsx +0 -30
  15. package/app/(app)/notes/page.tsx +0 -39
  16. package/app/(app)/page.tsx +0 -5
  17. package/app/(app)/settings/agent-token/page.tsx +0 -59
  18. package/app/(app)/settings/backup/page.tsx +0 -49
  19. package/app/(app)/settings/billing/page.tsx +0 -53
  20. package/app/(app)/settings/calendar/page.tsx +0 -41
  21. package/app/(app)/settings/layout.test.tsx +0 -39
  22. package/app/(app)/settings/layout.tsx +0 -71
  23. package/app/(app)/settings/page.tsx +0 -4
  24. package/app/(app)/settings/security/page.tsx +0 -43
  25. package/app/(app)/settings/team/page.tsx +0 -74
  26. package/app/(app)/settings/workspace/page.tsx +0 -27
  27. package/app/(app)/tasks/[taskId]/page.tsx +0 -79
  28. package/app/(auth)/forgot-password/page.tsx +0 -106
  29. package/app/(auth)/guest/page.tsx +0 -56
  30. package/app/(auth)/layout.tsx +0 -13
  31. package/app/(auth)/login/page.tsx +0 -14
  32. package/app/(auth)/register/page.tsx +0 -193
  33. package/app/(auth)/reset-password/page.tsx +0 -138
  34. package/app/api/account/claim/route.tsx +0 -135
  35. package/app/api/admin/backfill-encryption/route.tsx +0 -43
  36. package/app/api/admin/license/route.tsx +0 -42
  37. package/app/api/auth/2fa/route.tsx +0 -148
  38. package/app/api/auth/[...nextauth]/route.tsx +0 -3
  39. package/app/api/auth/change-password/route.tsx +0 -61
  40. package/app/api/auth/check-2fa/route.tsx +0 -19
  41. package/app/api/auth/forgot-password/route.tsx +0 -65
  42. package/app/api/auth/reset-password/route.tsx +0 -52
  43. package/app/api/auth/verify-2fa/route.tsx +0 -88
  44. package/app/api/backup/download/db/route.ts +0 -29
  45. package/app/api/backup/download/notes/route.ts +0 -25
  46. package/app/api/backup/settings/route.ts +0 -92
  47. package/app/api/billing/checkout/route.tsx +0 -81
  48. package/app/api/billing/migrate/route.tsx +0 -163
  49. package/app/api/billing/portal/route.tsx +0 -24
  50. package/app/api/billing/setup-intent/route.tsx +0 -55
  51. package/app/api/billing/status/route.tsx +0 -36
  52. package/app/api/billing/subscribe/route.tsx +0 -85
  53. package/app/api/billing/webhook/route.tsx +0 -199
  54. package/app/api/calendar-feeds/[feedId]/route.tsx +0 -67
  55. package/app/api/calendar-feeds/[feedId]/sync/route.tsx +0 -37
  56. package/app/api/calendar-feeds/events/route.tsx +0 -82
  57. package/app/api/calendar-feeds/route.tsx +0 -52
  58. package/app/api/calendar-feeds/sync-all/route.tsx +0 -34
  59. package/app/api/cron/calendar-feeds/route.tsx +0 -31
  60. package/app/api/cron/stale-tasks/route.tsx +0 -51
  61. package/app/api/cron/sync/route.tsx +0 -34
  62. package/app/api/devices/[deviceId]/route.tsx +0 -25
  63. package/app/api/devices/route.tsx +0 -41
  64. package/app/api/export/route.tsx +0 -40
  65. package/app/api/feedback/route.tsx +0 -54
  66. package/app/api/folders/[folderId]/route.tsx +0 -51
  67. package/app/api/folders/route.tsx +0 -37
  68. package/app/api/graph/route.tsx +0 -242
  69. package/app/api/guest/route.tsx +0 -58
  70. package/app/api/health/route.tsx +0 -10
  71. package/app/api/holidays/countries/route.tsx +0 -14
  72. package/app/api/holidays/route.tsx +0 -49
  73. package/app/api/holidays/states/route.tsx +0 -21
  74. package/app/api/invites/[token]/route.tsx +0 -131
  75. package/app/api/invites/route.tsx +0 -74
  76. package/app/api/mcp/generate-token/route.tsx +0 -55
  77. package/app/api/mcp/revoke-token/[tokenId]/route.tsx +0 -30
  78. package/app/api/mcp/update-alias/[tokenId]/route.tsx +0 -22
  79. package/app/api/notes/[noteId]/export/route.tsx +0 -45
  80. package/app/api/notes/[noteId]/route.tsx +0 -360
  81. package/app/api/notes/route.tsx +0 -112
  82. package/app/api/notifications/route.tsx +0 -44
  83. package/app/api/register/route.tsx +0 -67
  84. package/app/api/restore/route.tsx +0 -148
  85. package/app/api/sync/conflicts/[conflictId]/route.tsx +0 -134
  86. package/app/api/sync/conflicts/route.tsx +0 -48
  87. package/app/api/sync/status/route.tsx +0 -49
  88. package/app/api/sync/trigger/route.tsx +0 -15
  89. package/app/api/tasks/[taskId]/detail/route.tsx +0 -68
  90. package/app/api/tasks/[taskId]/route.tsx +0 -259
  91. package/app/api/tasks/bulk/route.tsx +0 -133
  92. package/app/api/tasks/route.tsx +0 -36
  93. package/app/api/workspace/active/route.tsx +0 -39
  94. package/app/api/workspace/create-team/route.tsx +0 -42
  95. package/app/api/workspace/kanban-statuses/route.tsx +0 -71
  96. package/app/api/workspace/members/[memberId]/route.tsx +0 -69
  97. package/app/api/workspace/route.tsx +0 -24
  98. package/app/download/page.tsx +0 -170
  99. package/app/favicon.ico +0 -0
  100. package/app/generated/prisma/client.d.ts +0 -1
  101. package/app/generated/prisma/client.js +0 -5
  102. package/app/generated/prisma/default.d.ts +0 -1
  103. package/app/generated/prisma/default.js +0 -5
  104. package/app/generated/prisma/edge.d.ts +0 -1
  105. package/app/generated/prisma/edge.js +0 -497
  106. package/app/generated/prisma/index-browser.js +0 -523
  107. package/app/generated/prisma/index.d.ts +0 -46376
  108. package/app/generated/prisma/index.js +0 -497
  109. package/app/generated/prisma/package.json +0 -144
  110. package/app/generated/prisma/query_compiler_fast_bg.js +0 -2
  111. package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
  112. package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +0 -2
  113. package/app/generated/prisma/runtime/client.d.ts +0 -3386
  114. package/app/generated/prisma/runtime/client.js +0 -86
  115. package/app/generated/prisma/runtime/index-browser.d.ts +0 -90
  116. package/app/generated/prisma/runtime/index-browser.js +0 -6
  117. package/app/generated/prisma/runtime/wasm-compiler-edge.js +0 -76
  118. package/app/generated/prisma/schema.prisma +0 -456
  119. package/app/generated/prisma/wasm-edge-light-loader.mjs +0 -5
  120. package/app/generated/prisma/wasm-worker-loader.mjs +0 -5
  121. package/app/globals.css +0 -54
  122. package/app/invite/[token]/page.tsx +0 -52
  123. package/app/layout.tsx +0 -90
  124. package/app/mcp/route.tsx +0 -430
  125. package/app/opengraph-image.tsx +0 -120
  126. package/app/page.tsx +0 -398
  127. package/app/privacy/page.tsx +0 -69
  128. package/app/robots.tsx +0 -25
  129. package/app/sitemap.tsx +0 -36
  130. package/app/terms/page.tsx +0 -69
  131. package/app/upgrade/page.tsx +0 -75
  132. package/auth.config.ts +0 -33
  133. package/auth.ts +0 -79
  134. package/components/auth/login-form.tsx +0 -302
  135. package/components/auth/password-checklist.tsx +0 -31
  136. package/components/auth/password-input.tsx +0 -36
  137. package/components/auth/switch-account-button.test.tsx +0 -22
  138. package/components/auth/switch-account-button.tsx +0 -19
  139. package/components/auth/two-factor-input.tsx +0 -116
  140. package/components/billing/billing-dashboard.tsx +0 -265
  141. package/components/billing/card-form.tsx +0 -210
  142. package/components/billing/claim-account-form.tsx +0 -99
  143. package/components/branding/app-logo.test.tsx +0 -20
  144. package/components/branding/app-logo.tsx +0 -25
  145. package/components/calendar/calendar-agenda.tsx +0 -150
  146. package/components/calendar/calendar-drag.test.tsx +0 -177
  147. package/components/calendar/calendar-grid.tsx +0 -357
  148. package/components/calendar/calendar-hooks.test.tsx +0 -27
  149. package/components/calendar/calendar-hooks.ts +0 -351
  150. package/components/calendar/calendar-toolbar.test.tsx +0 -68
  151. package/components/calendar/calendar-toolbar.tsx +0 -291
  152. package/components/calendar/calendar-types.ts +0 -148
  153. package/components/calendar/calendar-view.test.tsx +0 -295
  154. package/components/calendar/calendar-view.tsx +0 -307
  155. package/components/calendar/day-detail-popover.tsx +0 -174
  156. package/components/calendar/task-chip.tsx +0 -86
  157. package/components/command/command-palette.test.tsx +0 -33
  158. package/components/command/command-palette.tsx +0 -310
  159. package/components/download-cta.tsx +0 -87
  160. package/components/feedback/feedback-popup.tsx +0 -207
  161. package/components/graph/graph-draw.ts +0 -337
  162. package/components/graph/graph-overlays.tsx +0 -160
  163. package/components/graph/graph-page.test.tsx +0 -131
  164. package/components/graph/graph-page.tsx +0 -263
  165. package/components/graph/graph-types.ts +0 -47
  166. package/components/graph/graph-view.tsx +0 -322
  167. package/components/guide/guide-view.tsx +0 -522
  168. package/components/kanban/kanban-board.test.tsx +0 -128
  169. package/components/kanban/kanban-board.tsx +0 -361
  170. package/components/kanban/kanban-card-menu.tsx +0 -102
  171. package/components/kanban/kanban-card.tsx +0 -227
  172. package/components/kanban/kanban-column.tsx +0 -49
  173. package/components/kanban/kanban-status-context.tsx +0 -28
  174. package/components/landing/calendar-sandbox.test.tsx +0 -15
  175. package/components/landing/calendar-sandbox.tsx +0 -107
  176. package/components/landing/graph-sandbox.test.tsx +0 -27
  177. package/components/landing/graph-sandbox.tsx +0 -80
  178. package/components/landing/kanban-sandbox.test.tsx +0 -24
  179. package/components/landing/kanban-sandbox.tsx +0 -101
  180. package/components/landing/landing-showcase.test.tsx +0 -21
  181. package/components/landing/landing-showcase.tsx +0 -54
  182. package/components/landing/list-sandbox.tsx +0 -86
  183. package/components/landing/mock-workspace.ts +0 -168
  184. package/components/landing/notes-sandbox.test.tsx +0 -14
  185. package/components/landing/notes-sandbox.tsx +0 -88
  186. package/components/layout/app-shell.tsx +0 -83
  187. package/components/layout/backup-scheduler.tsx +0 -122
  188. package/components/layout/bottom-nav.tsx +0 -43
  189. package/components/layout/icon-bar.test.tsx +0 -29
  190. package/components/layout/icon-bar.tsx +0 -118
  191. package/components/layout/mobile-top-bar.tsx +0 -68
  192. package/components/layout/notes-panel-folder.tsx +0 -127
  193. package/components/layout/notes-panel-note-item.tsx +0 -140
  194. package/components/layout/notes-panel-task-tab.tsx +0 -63
  195. package/components/layout/notes-panel-types.ts +0 -44
  196. package/components/layout/notes-panel.tsx +0 -476
  197. package/components/layout/notification-bell.tsx +0 -251
  198. package/components/layout/paywall-screen.tsx +0 -41
  199. package/components/layout/pro-banner.tsx +0 -76
  200. package/components/layout/sw-register.tsx +0 -27
  201. package/components/layout/workspace-switcher.tsx +0 -90
  202. package/components/notes/mobile-bottom-sheet.tsx +0 -99
  203. package/components/notes/note-editor-context-menu.tsx +0 -47
  204. package/components/notes/note-editor-dom.ts +0 -33
  205. package/components/notes/note-editor-dropdowns.tsx +0 -484
  206. package/components/notes/note-editor-hooks.ts +0 -692
  207. package/components/notes/note-editor-keyboard.ts +0 -305
  208. package/components/notes/note-editor-overlay.tsx +0 -90
  209. package/components/notes/note-editor.test.tsx +0 -372
  210. package/components/notes/note-editor.tsx +0 -662
  211. package/components/notes/note-preview-pane.tsx +0 -156
  212. package/components/notes/note-tabs.tsx +0 -120
  213. package/components/notes/note-types.tsx +0 -157
  214. package/components/settings/accept-invite.tsx +0 -108
  215. package/components/settings/agent-token-settings.tsx +0 -369
  216. package/components/settings/backup-restore-settings.test.tsx +0 -25
  217. package/components/settings/backup-restore-settings.tsx +0 -327
  218. package/components/settings/calendar-feeds-settings.tsx +0 -489
  219. package/components/settings/calendar-general-settings.tsx +0 -174
  220. package/components/settings/confirm-danger-action.test.tsx +0 -215
  221. package/components/settings/confirm-danger-action.tsx +0 -65
  222. package/components/settings/security-settings.tsx +0 -252
  223. package/components/settings/settings-guidance.test.tsx +0 -98
  224. package/components/settings/team-settings.tsx +0 -319
  225. package/components/settings/two-factor-auth.tsx +0 -296
  226. package/components/settings/workspace-settings-client.tsx +0 -363
  227. package/components/settings/workspace-settings-form.tsx +0 -73
  228. package/components/sync/conflict-viewer.tsx +0 -247
  229. package/components/sync/sync-indicator.tsx +0 -171
  230. package/components/tasks/snippet-thread.tsx +0 -119
  231. package/components/tasks/status-dot.tsx +0 -47
  232. package/components/tasks/task-badge.tsx +0 -43
  233. package/components/tasks/task-detail.test.tsx +0 -187
  234. package/components/tasks/task-detail.tsx +0 -458
  235. package/components/tasks/task-list-filters.test.tsx +0 -75
  236. package/components/tasks/task-list-filters.tsx +0 -163
  237. package/components/tasks/task-list-types.ts +0 -20
  238. package/components/tasks/task-list.test.tsx +0 -175
  239. package/components/tasks/task-list.tsx +0 -481
  240. package/components/tasks/task-row.tsx +0 -85
  241. package/components/tasks/task-table-row.tsx +0 -259
  242. package/components/ui/skeleton.tsx +0 -3
  243. package/components/ui/toast.test.tsx +0 -42
  244. package/components/ui/toast.tsx +0 -70
  245. package/electron/main.ts +0 -251
  246. package/electron/preload.ts +0 -56
  247. package/instrumentation.tsx +0 -23
  248. package/lib/api-error.ts +0 -50
  249. package/lib/backup/backup-runner.test.ts +0 -32
  250. package/lib/backup/backup-runner.ts +0 -19
  251. package/lib/backup/backup-schedule.test.ts +0 -23
  252. package/lib/backup/backup-schedule.ts +0 -55
  253. package/lib/backup/backup-settings.test.ts +0 -30
  254. package/lib/backup/backup-settings.ts +0 -27
  255. package/lib/backup/export-notes-zip.test.ts +0 -26
  256. package/lib/backup/export-notes-zip.ts +0 -82
  257. package/lib/backup/export-workspace-backup.test.ts +0 -17
  258. package/lib/backup/export-workspace-backup.ts +0 -77
  259. package/lib/backup/restore-workspace-from-export.test.ts +0 -18
  260. package/lib/backup/restore-workspace-from-export.ts +0 -183
  261. package/lib/backup/types.ts +0 -14
  262. package/lib/brand-icons.ts +0 -1
  263. package/lib/calendar-feed-crypto.ts +0 -38
  264. package/lib/calendar-feed.ts +0 -239
  265. package/lib/client/online-status.ts +0 -47
  266. package/lib/conflict-resolver.test.ts +0 -57
  267. package/lib/conflict-resolver.ts +0 -240
  268. package/lib/db-init.ts +0 -79
  269. package/lib/email.ts +0 -159
  270. package/lib/encryption.test.ts +0 -41
  271. package/lib/encryption.ts +0 -98
  272. package/lib/extract-snippet.test.ts +0 -123
  273. package/lib/extract-snippet.ts +0 -69
  274. package/lib/kanban-status.ts +0 -55
  275. package/lib/license.ts +0 -21
  276. package/lib/limits.ts +0 -31
  277. package/lib/mcp-auth.test.ts +0 -58
  278. package/lib/mcp-auth.ts +0 -65
  279. package/lib/mcp-contract.test.ts +0 -25
  280. package/lib/mcp-contract.ts +0 -210
  281. package/lib/mcp-handler.ts +0 -31
  282. package/lib/mcp-url.test.ts +0 -12
  283. package/lib/mcp-url.ts +0 -7
  284. package/lib/mentions.test.ts +0 -45
  285. package/lib/mentions.ts +0 -73
  286. package/lib/note-crypto.ts +0 -108
  287. package/lib/note-sync.ts +0 -201
  288. package/lib/note-title.ts +0 -93
  289. package/lib/prisma.ts +0 -193
  290. package/lib/pro-flush.ts +0 -292
  291. package/lib/rate-limit.ts +0 -57
  292. package/lib/stripe.ts +0 -38
  293. package/lib/sync-worker.ts +0 -388
  294. package/lib/task-parser.test.ts +0 -91
  295. package/lib/task-parser.ts +0 -81
  296. package/lib/task-utils.ts +0 -52
  297. package/lib/use-is-electron.ts +0 -19
  298. package/lib/use-is-mobile.ts +0 -22
  299. package/lib/validation/calendar-feed.ts +0 -31
  300. package/lib/validation/note.ts +0 -27
  301. package/lib/validation/task.ts +0 -26
  302. package/lib/view-preferences.test.ts +0 -54
  303. package/lib/view-preferences.ts +0 -28
  304. package/lib/workspace.ts +0 -66
  305. package/next.config.ts +0 -21
  306. package/postcss.config.mjs +0 -7
  307. package/prisma/migrations/20260519021916_init/migration.sql +0 -388
  308. package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +0 -8
  309. package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +0 -2
  310. package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +0 -12
  311. package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +0 -3
  312. package/prisma/migrations/20260529030000_add_folders/migration.sql +0 -17
  313. package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +0 -31
  314. package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +0 -5
  315. package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +0 -14
  316. package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +0 -26
  317. package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +0 -23
  318. package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +0 -43
  319. package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +0 -2
  320. package/prisma/migrations/20260611050000_add_task_priority/migration.sql +0 -14
  321. package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +0 -2
  322. package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +0 -25
  323. package/prisma/migrations/20260614160000_add_feedback/migration.sql +0 -20
  324. package/prisma/migrations/20260614210000_add_2fa/migration.sql +0 -4
  325. package/prisma/migrations/migration_lock.toml +0 -3
  326. package/prisma/schema.prisma +0 -457
  327. package/public/Logo_icon.svg +0 -1
  328. package/public/file.svg +0 -1
  329. package/public/globe.svg +0 -1
  330. package/public/icon-192.png +0 -0
  331. package/public/icon-512.png +0 -0
  332. package/public/icon.svg +0 -4
  333. package/public/icon_dark.svg +0 -1
  334. package/public/knotpad_icon.svg +0 -1
  335. package/public/knotpad_logo_full.svg +0 -1
  336. package/public/manifest.json +0 -14
  337. package/public/next.svg +0 -1
  338. package/public/sw.js +0 -137
  339. package/public/vercel.svg +0 -1
  340. package/public/window.svg +0 -1
  341. package/tsconfig.json +0 -35
@@ -1,148 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { auth } from "@/auth";
3
- import { prisma } from "@/lib/prisma";
4
- import { randomBytes, createHmac } from "crypto";
5
-
6
- // TOTP implementation using Node.js crypto
7
- function generateSecret(): string {
8
- return randomBytes(20).toString("base64url").slice(0, 32);
9
- }
10
-
11
- function generateTOTP(secret: string, window = 0): string {
12
- const counter = Math.floor(Date.now() / 1000 / 30) + window;
13
- const counterBuffer = Buffer.alloc(8);
14
- counterBuffer.writeBigUInt64BE(BigInt(counter), 0);
15
-
16
- const secretBuffer = Buffer.from(secret, "base64url");
17
- const hmac = createHmac("sha1", secretBuffer);
18
- hmac.update(counterBuffer);
19
- const hash = hmac.digest();
20
-
21
- const offset = hash[hash.length - 1] & 0x0f;
22
- const code =
23
- ((hash[offset] & 0x7f) << 24) |
24
- ((hash[offset + 1] & 0xff) << 16) |
25
- ((hash[offset + 2] & 0xff) << 8) |
26
- (hash[offset + 3] & 0xff);
27
-
28
- return (code % 1000000).toString().padStart(6, "0");
29
- }
30
-
31
- function verifyTOTP(secret: string, token: string): boolean {
32
- // Check current and adjacent time windows (±1 window = ±30 seconds)
33
- for (let window = -1; window <= 1; window++) {
34
- if (generateTOTP(secret, window) === token) {
35
- return true;
36
- }
37
- }
38
- return false;
39
- }
40
-
41
- function generateQRCodeURL(secret: string, email: string, appName: string): string {
42
- const encodedSecret = secret.replace(/=/g, "");
43
- const encodedApp = encodeURIComponent(appName);
44
- const encodedEmail = encodeURIComponent(email);
45
- return `otpauth://totp/${encodedApp}:${encodedEmail}?secret=${encodedSecret}&issuer=${encodedApp}`;
46
- }
47
-
48
- // GET: Check if 2FA is enabled for current user
49
- export async function GET() {
50
- const session = await auth();
51
- if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
52
-
53
- const user = await prisma.user.findUnique({
54
- where: { id: session.user.id },
55
- select: { twoFactorEnabled: true },
56
- });
57
-
58
- return NextResponse.json({ enabled: user?.twoFactorEnabled ?? false });
59
- }
60
-
61
- // POST: Enable 2FA - generate secret and return QR code
62
- export async function POST(req: NextRequest) {
63
- const session = await auth();
64
- if (!session?.user?.email) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
65
-
66
- const { action } = await req.json();
67
-
68
- if (action === "setup") {
69
- // Generate new secret
70
- const secret = generateSecret();
71
- const qrCodeURL = generateQRCodeURL(secret, session.user.email, "Knotpad");
72
-
73
- // Store secret temporarily (not enabled until verified)
74
- await prisma.user.update({
75
- where: { id: session.user.id },
76
- data: { twoFactorSecret: secret, twoFactorEnabled: false, twoFactorVerified: false },
77
- });
78
-
79
- return NextResponse.json({
80
- secret,
81
- qrCodeURL,
82
- manualEntryKey: secret.replace(/=/g, "").slice(0, 32).toUpperCase().replace(/(.{4})/g, "$1 ").trim(),
83
- });
84
- }
85
-
86
- if (action === "verify") {
87
- const { code } = await req.json();
88
- if (!code || !/^\d{6}$/.test(code)) {
89
- return NextResponse.json({ error: "Invalid code format" }, { status: 400 });
90
- }
91
-
92
- const user = await prisma.user.findUnique({
93
- where: { id: session.user.id },
94
- select: { twoFactorSecret: true, twoFactorEnabled: true },
95
- });
96
-
97
- if (!user?.twoFactorSecret) {
98
- return NextResponse.json({ error: "2FA not set up" }, { status: 400 });
99
- }
100
-
101
- if (verifyTOTP(user.twoFactorSecret, code)) {
102
- await prisma.user.update({
103
- where: { id: session.user.id },
104
- data: { twoFactorEnabled: true, twoFactorVerified: true },
105
- });
106
- return NextResponse.json({ success: true });
107
- }
108
-
109
- return NextResponse.json({ error: "Invalid code" }, { status: 400 });
110
- }
111
-
112
- if (action === "disable") {
113
- const { code, password } = await req.json();
114
-
115
- // Verify password first
116
- const user = await prisma.user.findUnique({
117
- where: { id: session.user.id },
118
- select: { passwordHash: true, twoFactorSecret: true, twoFactorEnabled: true },
119
- });
120
-
121
- if (!user?.passwordHash) {
122
- return NextResponse.json({ error: "Password required" }, { status: 400 });
123
- }
124
-
125
- // Import bcrypt for password verification
126
- const { default: bcrypt } = await import("bcryptjs");
127
- const validPassword = await bcrypt.compare(password, user.passwordHash);
128
- if (!validPassword) {
129
- return NextResponse.json({ error: "Invalid password" }, { status: 400 });
130
- }
131
-
132
- // Verify 2FA code if enabled
133
- if (user.twoFactorEnabled && user.twoFactorSecret) {
134
- if (!code || !verifyTOTP(user.twoFactorSecret, code)) {
135
- return NextResponse.json({ error: "Invalid 2FA code" }, { status: 400 });
136
- }
137
- }
138
-
139
- await prisma.user.update({
140
- where: { id: session.user.id },
141
- data: { twoFactorSecret: null, twoFactorEnabled: false, twoFactorVerified: false },
142
- });
143
-
144
- return NextResponse.json({ success: true });
145
- }
146
-
147
- return NextResponse.json({ error: "Invalid action" }, { status: 400 });
148
- }
@@ -1,3 +0,0 @@
1
- import { handlers } from "@/auth";
2
-
3
- export const { GET, POST } = handlers;
@@ -1,61 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import bcrypt from "bcryptjs";
3
- import { auth } from "@/auth";
4
- import { prisma, getCloudPrisma } from "@/lib/prisma";
5
- import { rateLimit, getClientIp } from "@/lib/rate-limit";
6
-
7
- const RL_MAX = 10;
8
- const RL_WINDOW_MS = 60 * 60_000;
9
-
10
- export async function POST(req: NextRequest) {
11
- const session = await auth();
12
- if (!session?.user?.id) {
13
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
14
- }
15
-
16
- const ip = getClientIp(req);
17
- const rl = rateLimit(`change-password:${ip}`, RL_MAX, RL_WINDOW_MS);
18
- if (rl.limited) {
19
- return NextResponse.json(
20
- { error: "Too many attempts. Try again later." },
21
- { status: 429, headers: { "Retry-After": String(rl.retryAfter) } }
22
- );
23
- }
24
-
25
- const { currentPassword, newPassword } = await req.json();
26
-
27
- if (!currentPassword || typeof currentPassword !== "string") {
28
- return NextResponse.json({ error: "Current password required" }, { status: 400 });
29
- }
30
- if (!newPassword || typeof newPassword !== "string" || newPassword.length < 8) {
31
- return NextResponse.json({ error: "New password must be at least 8 characters" }, { status: 400 });
32
- }
33
-
34
- const db = getCloudPrisma() ?? prisma;
35
-
36
- const user = await db.user.findUnique({
37
- where: { id: session.user.id },
38
- select: { id: true, passwordHash: true },
39
- });
40
-
41
- if (!user || !user.passwordHash) {
42
- return NextResponse.json(
43
- { error: "Password change is not available for this account." },
44
- { status: 400 }
45
- );
46
- }
47
-
48
- const valid = await bcrypt.compare(currentPassword, user.passwordHash);
49
- if (!valid) {
50
- return NextResponse.json({ error: "Current password is incorrect" }, { status: 400 });
51
- }
52
-
53
- const passwordHash = await bcrypt.hash(newPassword, 12);
54
-
55
- await db.user.update({
56
- where: { id: user.id },
57
- data: { passwordHash },
58
- });
59
-
60
- return NextResponse.json({ ok: true });
61
- }
@@ -1,19 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { prisma } from "@/lib/prisma";
3
-
4
- export async function POST(req: NextRequest) {
5
- const { email } = await req.json();
6
-
7
- if (!email) {
8
- return NextResponse.json({ error: "Email required" }, { status: 400 });
9
- }
10
-
11
- const user = await prisma.user.findUnique({
12
- where: { email },
13
- select: { twoFactorEnabled: true },
14
- });
15
-
16
- return NextResponse.json({
17
- requires2FA: user?.twoFactorEnabled ?? false,
18
- });
19
- }
@@ -1,65 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { prisma, getCloudPrisma } from "@/lib/prisma";
3
- import { rateLimit, getClientIp } from "@/lib/rate-limit";
4
- import { sendAuthEmail } from "@/lib/email";
5
-
6
- const RL_MAX = 5;
7
- const RL_WINDOW_MS = 60 * 60_000; // 5 attempts per IP per hour
8
-
9
- export async function POST(req: NextRequest) {
10
- const ip = getClientIp(req);
11
- const rl = rateLimit(`forgot-password:${ip}`, RL_MAX, RL_WINDOW_MS);
12
- if (rl.limited) {
13
- return NextResponse.json(
14
- { error: "Too many attempts. Try again later." },
15
- { status: 429, headers: { "Retry-After": String(rl.retryAfter) } }
16
- );
17
- }
18
-
19
- const { email } = await req.json();
20
- if (!email || typeof email !== "string") {
21
- return NextResponse.json({ error: "Email required" }, { status: 400 });
22
- }
23
-
24
- // Look up user in the same DB auth reads from (cloud if available, local otherwise).
25
- const db = getCloudPrisma() ?? prisma;
26
- const user = await db.user.findUnique({ where: { email: email.toLowerCase().trim() } });
27
-
28
- // Always return 200 to prevent email enumeration.
29
- if (!user || !user.passwordHash) {
30
- return NextResponse.json({ ok: true });
31
- }
32
-
33
- // Invalidate any existing unused tokens for this user.
34
- await db.passwordResetToken.updateMany({
35
- where: { userId: user.id, usedAt: null },
36
- data: { usedAt: new Date() },
37
- });
38
-
39
- const record = await db.passwordResetToken.create({
40
- data: {
41
- userId: user.id,
42
- expiresAt: new Date(Date.now() + 60 * 60_000), // 1 hour
43
- },
44
- });
45
-
46
- const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
47
- const resetUrl = `${appUrl}/reset-password?token=${record.token}`;
48
-
49
- // Send reset email via Brevo. If no email provider is configured the URL is
50
- // returned in the response so admins/devs can copy it manually.
51
- const sent = await sendAuthEmail(
52
- email,
53
- "Reset your Knotpad password",
54
- `
55
- <p>Hi${user.name ? ` ${user.name}` : ""},</p>
56
- <p>Click the link below to reset your password. This link expires in 1 hour.</p>
57
- <p><a href="${resetUrl}">${resetUrl}</a></p>
58
- <p>If you didn't request this, you can ignore this email.</p>
59
- `
60
- );
61
-
62
- // When no email provider is configured, expose the URL for local/dev use.
63
- if (!sent) return NextResponse.json({ ok: true, resetUrl });
64
- return NextResponse.json({ ok: true });
65
- }
@@ -1,52 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import bcrypt from "bcryptjs";
3
- import { prisma, getCloudPrisma } from "@/lib/prisma";
4
- import { rateLimit, getClientIp } from "@/lib/rate-limit";
5
-
6
- const RL_MAX = 10;
7
- const RL_WINDOW_MS = 60 * 60_000;
8
-
9
- export async function POST(req: NextRequest) {
10
- const ip = getClientIp(req);
11
- const rl = rateLimit(`reset-password:${ip}`, RL_MAX, RL_WINDOW_MS);
12
- if (rl.limited) {
13
- return NextResponse.json(
14
- { error: "Too many attempts. Try again later." },
15
- { status: 429, headers: { "Retry-After": String(rl.retryAfter) } }
16
- );
17
- }
18
-
19
- const { token, password } = await req.json();
20
- if (!token || typeof token !== "string") {
21
- return NextResponse.json({ error: "Token required" }, { status: 400 });
22
- }
23
- if (!password || typeof password !== "string" || password.length < 8) {
24
- return NextResponse.json({ error: "Password must be at least 8 characters" }, { status: 400 });
25
- }
26
-
27
- const db = getCloudPrisma() ?? prisma;
28
-
29
- const record = await db.passwordResetToken.findUnique({
30
- where: { token },
31
- include: { user: { select: { id: true, email: true } } },
32
- });
33
-
34
- if (!record || record.usedAt || record.expiresAt < new Date()) {
35
- return NextResponse.json({ error: "Invalid or expired token" }, { status: 400 });
36
- }
37
-
38
- const passwordHash = await bcrypt.hash(password, 12);
39
-
40
- await db.$transaction(async (tx) => {
41
- await tx.user.update({
42
- where: { id: record.userId },
43
- data: { passwordHash },
44
- });
45
- await tx.passwordResetToken.update({
46
- where: { id: record.id },
47
- data: { usedAt: new Date() },
48
- });
49
- });
50
-
51
- return NextResponse.json({ ok: true });
52
- }
@@ -1,88 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { prisma } from "@/lib/prisma";
3
- import { createHmac } from "crypto";
4
- import bcrypt from "bcryptjs";
5
-
6
- function generateTOTP(secret: string, window = 0): string {
7
- const counter = Math.floor(Date.now() / 1000 / 30) + window;
8
- const counterBuffer = Buffer.alloc(8);
9
- counterBuffer.writeBigUInt64BE(BigInt(counter), 0);
10
-
11
- const secretBuffer = Buffer.from(secret, "base64url");
12
- const hmac = createHmac("sha1", secretBuffer);
13
- hmac.update(counterBuffer);
14
- const hash = hmac.digest();
15
-
16
- const offset = hash[hash.length - 1] & 0x0f;
17
- const code =
18
- ((hash[offset] & 0x7f) << 24) |
19
- ((hash[offset + 1] & 0xff) << 16) |
20
- ((hash[offset + 2] & 0xff) << 8) |
21
- (hash[offset + 3] & 0xff);
22
-
23
- return (code % 1000000).toString().padStart(6, "0");
24
- }
25
-
26
- function verifyTOTP(secret: string, token: string): boolean {
27
- for (let window = -1; window <= 1; window++) {
28
- if (generateTOTP(secret, window) === token) {
29
- return true;
30
- }
31
- }
32
- return false;
33
- }
34
-
35
- export async function POST(req: NextRequest) {
36
- const { email, password, code } = await req.json();
37
-
38
- if (!email || !password || !code) {
39
- return NextResponse.json({ error: "Missing credentials" }, { status: 400 });
40
- }
41
-
42
- if (!/^\d{6}$/.test(code)) {
43
- return NextResponse.json({ error: "Invalid 2FA code format" }, { status: 400 });
44
- }
45
-
46
- const db = prisma;
47
- const user = await db.user.findUnique({
48
- where: { email },
49
- select: {
50
- id: true,
51
- email: true,
52
- name: true,
53
- passwordHash: true,
54
- twoFactorSecret: true,
55
- twoFactorEnabled: true,
56
- },
57
- });
58
-
59
- if (!user || !user.passwordHash) {
60
- return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
61
- }
62
-
63
- // Verify password first
64
- const validPassword = await bcrypt.compare(password, user.passwordHash);
65
- if (!validPassword) {
66
- return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
67
- }
68
-
69
- // Check if 2FA is enabled
70
- if (!user.twoFactorEnabled || !user.twoFactorSecret) {
71
- return NextResponse.json({ error: "2FA not enabled" }, { status: 400 });
72
- }
73
-
74
- // Verify TOTP code
75
- if (!verifyTOTP(user.twoFactorSecret, code)) {
76
- return NextResponse.json({ error: "Invalid 2FA code" }, { status: 401 });
77
- }
78
-
79
- // Return user info for client-side session creation
80
- return NextResponse.json({
81
- success: true,
82
- user: {
83
- id: user.id,
84
- email: user.email,
85
- name: user.name ?? "",
86
- },
87
- });
88
- }
@@ -1,29 +0,0 @@
1
- import { NextResponse } from "next/server";
2
- import { auth } from "@/auth";
3
- import { prisma } from "@/lib/prisma";
4
- import { buildWorkspaceBackup } from "@/lib/backup/export-workspace-backup";
5
-
6
- export async function GET() {
7
- const session = await auth();
8
- if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
9
-
10
- const member = await prisma.workspaceMember.findFirst({
11
- where: { userId: session.user.id, revokedAt: null },
12
- include: { workspace: true },
13
- });
14
- if (!member) return NextResponse.json({ error: "No workspace" }, { status: 404 });
15
-
16
- const { filename, payload } = await buildWorkspaceBackup(
17
- member.workspaceId,
18
- member.workspace.name,
19
- member.workspace.slug
20
- );
21
-
22
- return new NextResponse(JSON.stringify(payload, null, 2), {
23
- headers: {
24
- "Content-Type": "application/json",
25
- "Content-Disposition": `attachment; filename="${filename}"`,
26
- "x-brief-filename": filename,
27
- },
28
- });
29
- }
@@ -1,25 +0,0 @@
1
- import { NextResponse } from "next/server";
2
- import { auth } from "@/auth";
3
- import { prisma } from "@/lib/prisma";
4
- import { buildNotesZipExport } from "@/lib/backup/export-notes-zip";
5
-
6
- export async function GET() {
7
- const session = await auth();
8
- if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
9
-
10
- const member = await prisma.workspaceMember.findFirst({
11
- where: { userId: session.user.id, revokedAt: null },
12
- include: { workspace: true },
13
- });
14
- if (!member) return NextResponse.json({ error: "No workspace" }, { status: 404 });
15
-
16
- const { filename, bytes } = await buildNotesZipExport(member.workspaceId);
17
-
18
- return new NextResponse(Buffer.from(bytes), {
19
- headers: {
20
- "Content-Type": "application/zip",
21
- "Content-Disposition": `attachment; filename="${filename}"`,
22
- "x-brief-filename": filename,
23
- },
24
- });
25
- }
@@ -1,92 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { auth } from "@/auth";
3
- import { prisma } from "@/lib/prisma";
4
- import { validateBackupSettingsInput } from "@/lib/backup/backup-settings";
5
-
6
- export async function GET() {
7
- const session = await auth();
8
- if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
9
-
10
- const member = await prisma.workspaceMember.findFirst({
11
- where: {
12
- userId: session.user.id,
13
- role: { in: ["OWNER", "ADMIN"] },
14
- revokedAt: null,
15
- },
16
- include: {
17
- workspace: {
18
- include: {
19
- backupSettings: true,
20
- },
21
- },
22
- },
23
- });
24
- if (!member) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
25
-
26
- return NextResponse.json({
27
- workspace: {
28
- id: member.workspaceId,
29
- isCloud: member.workspace.isCloud,
30
- isPro: member.workspace.isPro,
31
- },
32
- settings: member.workspace.backupSettings,
33
- });
34
- }
35
-
36
- export async function PUT(req: NextRequest) {
37
- const session = await auth();
38
- if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
39
-
40
- const member = await prisma.workspaceMember.findFirst({
41
- where: {
42
- userId: session.user.id,
43
- role: { in: ["OWNER", "ADMIN"] },
44
- revokedAt: null,
45
- },
46
- include: { workspace: true },
47
- });
48
- if (!member) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
49
-
50
- const body = await req.json().catch(() => null);
51
- if (!body) return NextResponse.json({ error: "Invalid body" }, { status: 400 });
52
-
53
- const validation = validateBackupSettingsInput({
54
- scheduleEnabled: Boolean(body.scheduleEnabled),
55
- scheduleCadence: body.scheduleCadence,
56
- destinationPath: typeof body.destinationPath === "string" ? body.destinationPath : "",
57
- includeMarkdownZip: Boolean(body.includeMarkdownZip),
58
- isCloudWorkspace: member.workspace.isCloud,
59
- });
60
- if (!validation.success) {
61
- return NextResponse.json({ error: validation.error }, { status: 422 });
62
- }
63
-
64
- const settings = await prisma.workspaceBackupSettings.upsert({
65
- where: { workspaceId: member.workspaceId },
66
- update: {
67
- scheduleEnabled: Boolean(body.scheduleEnabled),
68
- scheduleCadence: body.scheduleCadence,
69
- destinationPath: body.destinationPath?.trim() ? body.destinationPath.trim() : null,
70
- includeMarkdownZip: Boolean(body.includeMarkdownZip),
71
- lastBackupAt: body.lastBackupAt ? new Date(body.lastBackupAt) : undefined,
72
- lastBackupStatus:
73
- typeof body.lastBackupStatus === "string" ? body.lastBackupStatus : undefined,
74
- lastBackupError:
75
- typeof body.lastBackupError === "string" ? body.lastBackupError : body.lastBackupError === null ? null : undefined,
76
- },
77
- create: {
78
- workspaceId: member.workspaceId,
79
- scheduleEnabled: Boolean(body.scheduleEnabled),
80
- scheduleCadence: body.scheduleCadence,
81
- destinationPath: body.destinationPath?.trim() ? body.destinationPath.trim() : null,
82
- includeMarkdownZip: Boolean(body.includeMarkdownZip),
83
- lastBackupAt: body.lastBackupAt ? new Date(body.lastBackupAt) : null,
84
- lastBackupStatus:
85
- typeof body.lastBackupStatus === "string" ? body.lastBackupStatus : "idle",
86
- lastBackupError:
87
- typeof body.lastBackupError === "string" ? body.lastBackupError : null,
88
- },
89
- });
90
-
91
- return NextResponse.json({ settings });
92
- }
@@ -1,81 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { auth } from "@/auth";
3
- import { prisma } from "@/lib/prisma";
4
- import { stripe, PRICE_ID_PERSONAL_PRO, PRICE_ID_TEAM_PRO, APP_URL } from "@/lib/stripe";
5
-
6
- export async function POST(req: NextRequest) {
7
- const session = await auth();
8
- if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
9
-
10
- const body = await req.json().catch(() => ({}));
11
-
12
- // plan = "personal" (1 seat, PERSONAL_PRO) | "team" (N seats, TEAM_PRO, min 2)
13
- const plan: "personal" | "team" = body.plan === "team" ? "team" : "personal";
14
- const seats: number = plan === "team" ? Math.max(2, parseInt(body.seats ?? "2")) : 1;
15
- const targetWorkspaceId: string | undefined = body.workspaceId;
16
-
17
- // Find the workspace to upgrade. For team checkouts a specific workspaceId is
18
- // required (the newly created team workspace). For personal, use the caller's
19
- // personal workspace.
20
- const member = targetWorkspaceId
21
- ? await prisma.workspaceMember.findFirst({
22
- where: {
23
- userId: session.user.id,
24
- workspaceId: targetWorkspaceId,
25
- role: { in: ["OWNER", "ADMIN"] },
26
- },
27
- include: { workspace: true },
28
- })
29
- : await prisma.workspaceMember.findFirst({
30
- where: {
31
- userId: session.user.id,
32
- role: { in: ["OWNER", "ADMIN"] },
33
- workspace: { type: "PERSONAL" },
34
- },
35
- include: { workspace: true },
36
- });
37
-
38
- if (!member) return NextResponse.json({ error: "Must be owner or admin" }, { status: 403 });
39
-
40
- const { workspace } = member;
41
-
42
- // Guard: prevent creating a second checkout session for an already-active subscription.
43
- if (workspace.isPro && workspace.stripeSubId) {
44
- return NextResponse.json(
45
- { error: "Workspace is already subscribed", stripeSubId: workspace.stripeSubId },
46
- { status: 409 }
47
- );
48
- }
49
-
50
- // Create or reuse Stripe customer
51
- let customerId = workspace.stripeId;
52
- if (!customerId) {
53
- const customer = await stripe.customers.create({
54
- email: session.user.email,
55
- name: session.user.name,
56
- metadata: { workspaceId: workspace.id },
57
- });
58
- customerId = customer.id;
59
- await prisma.workspace.update({
60
- where: { id: workspace.id },
61
- data: { stripeId: customerId },
62
- });
63
- }
64
-
65
- const planType = plan === "team" ? "TEAM_PRO" : "PERSONAL_PRO";
66
- const priceId = plan === "team" ? PRICE_ID_TEAM_PRO : PRICE_ID_PERSONAL_PRO;
67
-
68
- const checkoutSession = await stripe.checkout.sessions.create({
69
- customer: customerId,
70
- mode: "subscription",
71
- line_items: [{ price: priceId, quantity: seats }],
72
- success_url: `${APP_URL}/settings/billing?upgraded=1`,
73
- cancel_url: `${APP_URL}/settings/billing`,
74
- metadata: { workspaceId: workspace.id, userId: session.user.id, planType },
75
- subscription_data: {
76
- metadata: { workspaceId: workspace.id, planType },
77
- },
78
- });
79
-
80
- return NextResponse.json({ url: checkoutSession.url });
81
- }