@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,75 +0,0 @@
1
- import type { Metadata } from "next";
2
- import { auth } from "@/auth";
3
- import { redirect } from "next/navigation";
4
- import { prisma } from "@/lib/prisma";
5
- import { AppLogo } from "@/components/branding/app-logo";
6
- import { SwitchAccountButton } from "@/components/auth/switch-account-button";
7
- import { BillingDashboard } from "@/components/billing/billing-dashboard";
8
-
9
- export const metadata: Metadata = {
10
- title: "Upgrade",
11
- robots: "noindex, nofollow",
12
- };
13
-
14
- /**
15
- * Standalone upgrade page, outside the (app) route group.
16
- *
17
- * app/(app)/layout.tsx paywalls IS_CLOUD users whose workspaces are all FREE —
18
- * which would otherwise also block /settings/billing. This page resolves the
19
- * user's PERSONAL workspace directly (not via getActiveWorkspaceId, which on
20
- * cloud only returns Pro-accessible workspaces) so a FREE user always has a
21
- * path to checkout.
22
- */
23
- export default async function UpgradePage() {
24
- const session = await auth();
25
- if (!session) redirect("/login");
26
-
27
- const member = await prisma.workspaceMember.findFirst({
28
- where: { userId: session.user.id, workspace: { type: "PERSONAL" }, revokedAt: null },
29
- include: {
30
- workspace: {
31
- include: { _count: { select: { members: { where: { revokedAt: null } } } } },
32
- },
33
- },
34
- });
35
- if (!member) redirect("/login");
36
-
37
- const { workspace } = member;
38
-
39
- const me = await prisma.user.findUnique({
40
- where: { id: session.user.id },
41
- select: { email: true, passwordHash: true },
42
- });
43
- const isGuest = !!me?.email?.endsWith("@local.brief") && !me?.passwordHash;
44
-
45
- return (
46
- <div className="flex min-h-full items-center justify-center px-6 py-12">
47
- <div className="w-full max-w-lg space-y-6">
48
- <div className="space-y-4">
49
- <div className="flex justify-center sm:justify-start">
50
- <AppLogo className="h-8 w-auto" />
51
- </div>
52
- <div>
53
- <h1 className="text-xl font-semibold text-zinc-100">Upgrade to Knotpad Pro</h1>
54
- <p className="mt-1 text-sm text-zinc-400">
55
- Unlock cloud sync and access from any device, including the web and PWA.
56
- </p>
57
- </div>
58
- <SwitchAccountButton className="rounded-md border border-zinc-700 px-4 py-2 text-sm text-zinc-300 hover:bg-zinc-900 transition-colors" />
59
- </div>
60
- <BillingDashboard
61
- isPro={workspace.isPro}
62
- isOwner={member.role === "OWNER"}
63
- memberCount={workspace._count.members}
64
- seatCount={workspace.seatCount}
65
- stripeId={workspace.stripeId}
66
- planType={workspace.planType}
67
- licenseType={workspace.licenseType}
68
- workspaceType={workspace.type}
69
- workspaceId={workspace.id}
70
- isGuest={isGuest}
71
- />
72
- </div>
73
- </div>
74
- );
75
- }
package/auth.config.ts DELETED
@@ -1,33 +0,0 @@
1
- import type { NextAuthConfig } from "next-auth";
2
-
3
- // Lightweight config for proxy.ts — no DB adapter, JWT-only verification.
4
- export const authConfig: NextAuthConfig = {
5
- pages: { signIn: "/login" },
6
- callbacks: {
7
- authorized({ auth, request: { nextUrl } }) {
8
- const isLoggedIn = !!auth?.user;
9
- // Invite pages are accessible regardless of login state (invite page handles auth itself).
10
- if (nextUrl.pathname.startsWith("/invite/")) return true;
11
-
12
- const isAuthPage =
13
- nextUrl.pathname === "/login" ||
14
- nextUrl.pathname === "/register" ||
15
- nextUrl.pathname === "/guest" ||
16
- nextUrl.pathname === "/forgot-password" ||
17
- nextUrl.pathname === "/reset-password";
18
-
19
- if (isAuthPage) {
20
- if (!isLoggedIn) return true;
21
- // Logged-in user visiting an auth page — redirect away.
22
- // Preserve ?next= so invite-accept flow isn't broken (e.g. /login?next=/invite/TOKEN).
23
- // Validate next is a relative same-origin path to prevent open redirect.
24
- const next = nextUrl.searchParams.get("next");
25
- const safePath =
26
- next && next.startsWith("/") && !next.startsWith("//") ? next : "/notes";
27
- return Response.redirect(new URL(safePath, nextUrl));
28
- }
29
- return isLoggedIn;
30
- },
31
- },
32
- providers: [],
33
- };
package/auth.ts DELETED
@@ -1,79 +0,0 @@
1
- import NextAuth from "next-auth";
2
- import { PrismaAdapter } from "@auth/prisma-adapter";
3
- import Credentials from "next-auth/providers/credentials";
4
- import Google from "next-auth/providers/google";
5
- import bcrypt from "bcryptjs";
6
- import { prisma, getCloudPrisma } from "@/lib/prisma";
7
- import { authConfig } from "@/auth.config";
8
-
9
- export const { handlers, auth, signIn, signOut } = NextAuth({
10
- ...authConfig,
11
- trustHost: true, // Required for local HTTP (NPX/Electron dev and production)
12
- // Auth (User, Session, Account) lives in Neon — identity is cloud-scoped.
13
- // Free/guest users skip OAuth entirely and use local credentials only.
14
- adapter: getCloudPrisma() ? (PrismaAdapter(getCloudPrisma()!) as any) : undefined,
15
- session: { strategy: "jwt" },
16
- providers: [
17
- Credentials({
18
- credentials: {
19
- email: { label: "Email", type: "email" },
20
- password: { label: "Password", type: "password" },
21
- },
22
- async authorize(credentials) {
23
- if (!credentials?.email) return null;
24
-
25
- const email = credentials.email as string;
26
-
27
- // Guest accounts live in local PGlite (no cloud account needed)
28
- if (email.endsWith("@local.brief")) {
29
- const localUser = await prisma.user.findUnique({ where: { email } });
30
- if (localUser && !localUser.passwordHash) {
31
- return { id: localUser.id, email: localUser.email, name: localUser.name ?? "Guest" };
32
- }
33
- return null;
34
- }
35
-
36
- // Real accounts: prefer cloud (Neon) when available, fall back to local PGlite
37
- // so that registration works in fully-local mode (no CLOUD_DATABASE_URL).
38
- const db = getCloudPrisma() ?? prisma;
39
- const user = await db.user.findUnique({ where: { email } });
40
- if (!user || !credentials.password || !user.passwordHash) return null;
41
-
42
- const valid = await bcrypt.compare(credentials.password as string, user.passwordHash);
43
- if (!valid) return null;
44
-
45
- return { id: user.id, email: user.email, name: user.name ?? "" };
46
- },
47
- }),
48
- ...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
49
- ? [
50
- Google({
51
- clientId: process.env.GOOGLE_CLIENT_ID,
52
- clientSecret: process.env.GOOGLE_CLIENT_SECRET,
53
- }),
54
- ]
55
- : []),
56
- ],
57
- callbacks: {
58
- async jwt({ token, user }) {
59
- if (user) token.id = user.id;
60
- return token;
61
- },
62
- async session({ session, token }) {
63
- if (token?.id) session.user.id = token.id as string;
64
- return session;
65
- },
66
- authorized: authConfig.callbacks!.authorized,
67
- },
68
- });
69
-
70
- declare module "next-auth" {
71
- interface Session {
72
- user: {
73
- id: string;
74
- email: string;
75
- name: string;
76
- image?: string;
77
- };
78
- }
79
- }
@@ -1,302 +0,0 @@
1
- "use client";
2
-
3
- import { useState, Suspense } from "react";
4
- import { signIn } from "next-auth/react";
5
- import { useRouter, useSearchParams } from "next/navigation";
6
- import Link from "next/link";
7
- import { AppLogo } from "@/components/branding/app-logo";
8
- import { PasswordInput } from "@/components/auth/password-input";
9
- import { TwoFactorInput } from "@/components/auth/two-factor-input";
10
- import { Shield, Loader2 } from "lucide-react";
11
-
12
- export function LoginSkeleton() {
13
- return (
14
- <div className="w-full max-w-sm space-y-6">
15
- <div className="h-8 w-48 animate-pulse rounded bg-zinc-800" />
16
- <div className="h-4 w-64 animate-pulse rounded bg-zinc-800" />
17
- <div className="space-y-4 pt-4">
18
- <div className="h-10 animate-pulse rounded bg-zinc-800" />
19
- <div className="h-10 animate-pulse rounded bg-zinc-800" />
20
- <div className="h-10 animate-pulse rounded bg-zinc-800" />
21
- </div>
22
- </div>
23
- );
24
- }
25
-
26
- // `isCloud` reflects the runtime: desktop/NPX (false) offers a local, no-account
27
- // "Start writing" path; the browser/cloud build (true) has no local DB, so it
28
- // only offers Log in / Sign up.
29
- export function LoginForm({ isCloud }: { isCloud: boolean }) {
30
- return (
31
- <Suspense fallback={<LoginSkeleton />}>
32
- <LoginFormInner isCloud={isCloud} />
33
- </Suspense>
34
- );
35
- }
36
-
37
- function LoginFormInner({ isCloud }: { isCloud: boolean }) {
38
- const router = useRouter();
39
- const searchParams = useSearchParams();
40
- const [error, setError] = useState("");
41
- const [loading, setLoading] = useState(false);
42
- const [needs2FA, setNeeds2FA] = useState(false);
43
- const [twoFactorCode, setTwoFactorCode] = useState("");
44
- const [credentials, setCredentials] = useState({ email: "", password: "" });
45
-
46
- // Validate that ?next= is a same-origin relative path to prevent open redirect.
47
- const rawNext = searchParams.get("next") ?? "";
48
- const redirectTo =
49
- rawNext.startsWith("/") && !rawNext.startsWith("//") ? rawNext : "/notes";
50
- const justReset = searchParams.get("reset") === "1";
51
-
52
- async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
53
- e.preventDefault();
54
- setLoading(true);
55
- setError("");
56
-
57
- const form = e.currentTarget;
58
- const email = (form.elements.namedItem("email") as HTMLInputElement).value;
59
- const password = (form.elements.namedItem("password") as HTMLInputElement).value;
60
-
61
- setCredentials({ email, password });
62
-
63
- try {
64
- // Check if 2FA is required first
65
- const checkRes = await fetch("/api/auth/check-2fa", {
66
- method: "POST",
67
- headers: { "Content-Type": "application/json" },
68
- body: JSON.stringify({ email }),
69
- });
70
-
71
- const checkData = await checkRes.json();
72
-
73
- if (checkData.requires2FA) {
74
- setNeeds2FA(true);
75
- setLoading(false);
76
- return;
77
- }
78
-
79
- // No 2FA required, proceed with normal sign in
80
- const result = await signIn("credentials", {
81
- email,
82
- password,
83
- redirect: false,
84
- });
85
-
86
- if (result?.error) {
87
- setError("Invalid email or password");
88
- setLoading(false);
89
- } else {
90
- router.push(redirectTo);
91
- router.refresh();
92
- }
93
- } catch {
94
- setError("Something went wrong. Please try again.");
95
- setLoading(false);
96
- }
97
- }
98
-
99
- async function handle2FASubmit(e: React.FormEvent) {
100
- e.preventDefault();
101
- setLoading(true);
102
- setError("");
103
-
104
- try {
105
- // Verify 2FA code
106
- const res = await fetch("/api/auth/verify-2fa", {
107
- method: "POST",
108
- headers: { "Content-Type": "application/json" },
109
- body: JSON.stringify({
110
- email: credentials.email,
111
- password: credentials.password,
112
- code: twoFactorCode,
113
- }),
114
- });
115
-
116
- const data = await res.json();
117
-
118
- if (!res.ok) {
119
- setError(data.error || "Invalid 2FA code");
120
- setLoading(false);
121
- return;
122
- }
123
-
124
- // 2FA verified, now sign in
125
- const result = await signIn("credentials", {
126
- email: credentials.email,
127
- password: credentials.password,
128
- redirect: false,
129
- });
130
-
131
- if (result?.error) {
132
- setError("Authentication failed");
133
- setLoading(false);
134
- } else {
135
- router.push(redirectTo);
136
- router.refresh();
137
- }
138
- } catch {
139
- setError("Something went wrong. Please try again.");
140
- setLoading(false);
141
- }
142
- }
143
-
144
- return (
145
- <div className="w-full max-w-sm space-y-6">
146
- <div className="flex justify-end">
147
- <Link href="/" className="text-sm text-zinc-500 hover:text-zinc-300 transition-colors">
148
- back to home
149
- </Link>
150
- </div>
151
- <div>
152
- <AppLogo className="mb-4 h-8 w-auto" />
153
- <h1 className="text-2xl font-semibold tracking-tight">Welcome to Knotpad</h1>
154
- <p className="mt-1 text-sm text-zinc-400">
155
- {isCloud
156
- ? "Sign in, or create an account to get started."
157
- : "Start writing instantly — or sign in to your account."}
158
- </p>
159
- </div>
160
-
161
- {/* Desktop/local: the primary, no-account path. */}
162
- {!isCloud && (
163
- <div className="space-y-2">
164
- <Link
165
- href="/guest"
166
- className="block w-full rounded-md bg-zinc-100 px-4 py-2.5 text-center text-sm font-medium text-zinc-900 hover:bg-zinc-200 transition-colors"
167
- >
168
- Start writing
169
- </Link>
170
- <p className="text-center text-xs text-zinc-600">
171
- No account needed. Your notes stay on this device.
172
- </p>
173
- </div>
174
- )}
175
-
176
- {!isCloud && (
177
- <div className="relative">
178
- <div className="absolute inset-0 flex items-center">
179
- <div className="w-full border-t border-zinc-800" />
180
- </div>
181
- <div className="relative flex justify-center text-xs">
182
- <span className="bg-zinc-950 px-3 text-zinc-600">or sign in</span>
183
- </div>
184
- </div>
185
- )}
186
-
187
- {justReset && (
188
- <div className="rounded-md border border-emerald-800/40 bg-emerald-950/30 px-4 py-3">
189
- <p className="text-sm text-emerald-300">Password updated. Sign in with your new password.</p>
190
- </div>
191
- )}
192
-
193
- {needs2FA ? (
194
- <form onSubmit={handle2FASubmit} className="space-y-4">
195
- <div className="flex items-center gap-2 text-zinc-300">
196
- <Shield size={18} />
197
- <h2 className="text-sm font-medium">Two-Factor Authentication</h2>
198
- </div>
199
- <p className="text-xs text-zinc-400">
200
- Enter the 6-digit code from your authenticator app.
201
- </p>
202
-
203
- <div className="space-y-1">
204
- <label className="text-sm font-medium text-zinc-300">
205
- 2FA Code
206
- </label>
207
- <TwoFactorInput
208
- value={twoFactorCode}
209
- onChange={setTwoFactorCode}
210
- length={6}
211
- disabled={loading}
212
- autoFocus
213
- />
214
- </div>
215
-
216
- {error && <p className="text-sm text-red-400">{error}</p>}
217
-
218
- <div className="flex gap-2">
219
- <button
220
- type="button"
221
- onClick={() => {
222
- setNeeds2FA(false);
223
- setTwoFactorCode("");
224
- setError("");
225
- }}
226
- className="flex-1 rounded-md border border-zinc-700 bg-zinc-900 px-4 py-2 text-sm font-medium text-zinc-100 hover:bg-zinc-800"
227
- >
228
- Back
229
- </button>
230
- <button
231
- type="submit"
232
- disabled={loading || twoFactorCode.length !== 6}
233
- className="flex-1 rounded-md bg-zinc-100 px-4 py-2 text-sm font-medium text-zinc-900 hover:bg-zinc-200 disabled:opacity-50"
234
- >
235
- {loading ? (
236
- <span className="flex items-center justify-center gap-1">
237
- <Loader2 size={14} className="animate-spin" />
238
- Verifying…
239
- </span>
240
- ) : (
241
- "Verify"
242
- )}
243
- </button>
244
- </div>
245
- </form>
246
- ) : (
247
- <form onSubmit={handleSubmit} className="space-y-4">
248
- <div className="space-y-1">
249
- <label htmlFor="email" className="text-sm font-medium text-zinc-300">
250
- Email
251
- </label>
252
- <input
253
- id="email"
254
- name="email"
255
- type="email"
256
- required
257
- autoComplete="email"
258
- className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-100 placeholder-zinc-500 focus:border-zinc-500 focus:outline-none"
259
- placeholder="you@example.com"
260
- />
261
- </div>
262
-
263
- <div className="space-y-1">
264
- <label htmlFor="password" className="text-sm font-medium text-zinc-300">
265
- Password
266
- </label>
267
- <PasswordInput
268
- id="password"
269
- name="password"
270
- required
271
- autoComplete="current-password"
272
- placeholder="••••••••"
273
- />
274
- </div>
275
-
276
- {error && <p className="text-sm text-red-400">{error}</p>}
277
-
278
- <div className="flex justify-end">
279
- <Link href="/forgot-password" className="text-xs text-zinc-600 hover:text-zinc-400 transition-colors">
280
- Forgot password?
281
- </Link>
282
- </div>
283
-
284
- <button
285
- type="submit"
286
- disabled={loading}
287
- className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-4 py-2 text-sm font-medium text-zinc-100 hover:bg-zinc-800 disabled:opacity-50"
288
- >
289
- {loading ? "Signing in…" : "Sign in"}
290
- </button>
291
- </form>
292
- )}
293
-
294
- <p className="text-center text-sm text-zinc-500">
295
- {isCloud ? "Want cloud sync and Pro? " : "Need cloud sync across devices? "}
296
- <Link href="/register" className="text-zinc-300 underline underline-offset-2 hover:text-white">
297
- Sign up
298
- </Link>
299
- </p>
300
- </div>
301
- );
302
- }
@@ -1,31 +0,0 @@
1
- "use client";
2
-
3
- import { Check, X } from "lucide-react";
4
-
5
- const RULES: { label: string; test: (pw: string) => boolean }[] = [
6
- { label: "At least 8 characters", test: (pw) => pw.length >= 8 },
7
- { label: "One uppercase letter", test: (pw) => /[A-Z]/.test(pw) },
8
- { label: "One lowercase letter", test: (pw) => /[a-z]/.test(pw) },
9
- { label: "One number", test: (pw) => /[0-9]/.test(pw) },
10
- { label: "One symbol (e.g. !?#$%)", test: (pw) => /[^A-Za-z0-9]/.test(pw) },
11
- ];
12
-
13
- export function PasswordChecklist({ password }: { password: string }) {
14
- return (
15
- <ul className="space-y-1">
16
- {RULES.map((rule) => {
17
- const met = rule.test(password);
18
- return (
19
- <li key={rule.label} className="flex items-center gap-1.5 text-xs">
20
- {met ? (
21
- <Check className="h-3.5 w-3.5 text-emerald-500" />
22
- ) : (
23
- <X className="h-3.5 w-3.5 text-zinc-700" />
24
- )}
25
- <span className={met ? "text-zinc-400" : "text-zinc-600"}>{rule.label}</span>
26
- </li>
27
- );
28
- })}
29
- </ul>
30
- );
31
- }
@@ -1,36 +0,0 @@
1
- "use client";
2
-
3
- import { useState, forwardRef } from "react";
4
- import { Eye, EyeOff } from "lucide-react";
5
-
6
- type Props = Omit<React.InputHTMLAttributes<HTMLInputElement>, "type">;
7
-
8
- export const PasswordInput = forwardRef<HTMLInputElement, Props>(function PasswordInput(
9
- { className, ...props },
10
- ref
11
- ) {
12
- const [visible, setVisible] = useState(false);
13
-
14
- return (
15
- <div className="relative">
16
- <input
17
- {...props}
18
- ref={ref}
19
- type={visible ? "text" : "password"}
20
- className={
21
- className ??
22
- "w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 pr-10 text-sm text-zinc-100 placeholder-zinc-500 focus:border-zinc-500 focus:outline-none ring-focus"
23
- }
24
- />
25
- <button
26
- type="button"
27
- onClick={() => setVisible((v) => !v)}
28
- tabIndex={-1}
29
- aria-label={visible ? "Hide password" : "Show password"}
30
- className="absolute inset-y-0 right-0 flex items-center px-3 text-zinc-500 hover:text-zinc-300 transition-colors"
31
- >
32
- {visible ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
33
- </button>
34
- </div>
35
- );
36
- });
@@ -1,22 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { render, screen } from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
4
- import { describe, expect, it, vi } from "vitest";
5
- import { SwitchAccountButton } from "@/components/auth/switch-account-button";
6
-
7
- const signOut = vi.fn();
8
-
9
- vi.mock("next-auth/react", () => ({
10
- signOut: (...args: unknown[]) => signOut(...args),
11
- }));
12
-
13
- describe("SwitchAccountButton", () => {
14
- it("signs the user out to the login page when clicked", async () => {
15
- const user = userEvent.setup();
16
- render(<SwitchAccountButton />);
17
-
18
- await user.click(screen.getByRole("button", { name: "Switch account" }));
19
-
20
- expect(signOut).toHaveBeenCalledWith({ callbackUrl: "/login" });
21
- });
22
- });
@@ -1,19 +0,0 @@
1
- "use client";
2
-
3
- import { signOut } from "next-auth/react";
4
-
5
- type SwitchAccountButtonProps = {
6
- className?: string;
7
- };
8
-
9
- export function SwitchAccountButton({ className }: SwitchAccountButtonProps) {
10
- return (
11
- <button
12
- type="button"
13
- onClick={() => signOut({ callbackUrl: "/login" })}
14
- className={className}
15
- >
16
- Switch account
17
- </button>
18
- );
19
- }