@knotpad/app 0.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.
- package/README.md +167 -0
- package/app/(app)/calendar/page.tsx +57 -0
- package/app/(app)/error.tsx +35 -0
- package/app/(app)/graph/page.tsx +32 -0
- package/app/(app)/guide/page.tsx +21 -0
- package/app/(app)/kanban/loading.tsx +24 -0
- package/app/(app)/kanban/page.tsx +59 -0
- package/app/(app)/layout.tsx +122 -0
- package/app/(app)/list/loading.tsx +21 -0
- package/app/(app)/list/page.tsx +137 -0
- package/app/(app)/loading.tsx +18 -0
- package/app/(app)/notes/[noteId]/page.tsx +84 -0
- package/app/(app)/notes/layout.tsx +30 -0
- package/app/(app)/notes/page.tsx +39 -0
- package/app/(app)/page.tsx +5 -0
- package/app/(app)/settings/agent-token/page.tsx +59 -0
- package/app/(app)/settings/backup/page.tsx +49 -0
- package/app/(app)/settings/billing/page.tsx +53 -0
- package/app/(app)/settings/calendar/page.tsx +41 -0
- package/app/(app)/settings/layout.test.tsx +39 -0
- package/app/(app)/settings/layout.tsx +71 -0
- package/app/(app)/settings/page.tsx +4 -0
- package/app/(app)/settings/security/page.tsx +43 -0
- package/app/(app)/settings/team/page.tsx +74 -0
- package/app/(app)/settings/workspace/page.tsx +27 -0
- package/app/(app)/tasks/[taskId]/page.tsx +79 -0
- package/app/(auth)/forgot-password/page.tsx +106 -0
- package/app/(auth)/guest/page.tsx +56 -0
- package/app/(auth)/layout.tsx +13 -0
- package/app/(auth)/login/page.tsx +14 -0
- package/app/(auth)/register/page.tsx +193 -0
- package/app/(auth)/reset-password/page.tsx +138 -0
- package/app/api/account/claim/route.tsx +135 -0
- package/app/api/admin/backfill-encryption/route.tsx +43 -0
- package/app/api/admin/license/route.tsx +42 -0
- package/app/api/auth/2fa/route.tsx +148 -0
- package/app/api/auth/[...nextauth]/route.tsx +3 -0
- package/app/api/auth/change-password/route.tsx +61 -0
- package/app/api/auth/check-2fa/route.tsx +19 -0
- package/app/api/auth/forgot-password/route.tsx +65 -0
- package/app/api/auth/reset-password/route.tsx +52 -0
- package/app/api/auth/verify-2fa/route.tsx +88 -0
- package/app/api/backup/download/db/route.ts +29 -0
- package/app/api/backup/download/notes/route.ts +25 -0
- package/app/api/backup/settings/route.ts +92 -0
- package/app/api/billing/checkout/route.tsx +81 -0
- package/app/api/billing/migrate/route.tsx +163 -0
- package/app/api/billing/portal/route.tsx +24 -0
- package/app/api/billing/setup-intent/route.tsx +55 -0
- package/app/api/billing/status/route.tsx +36 -0
- package/app/api/billing/subscribe/route.tsx +85 -0
- package/app/api/billing/webhook/route.tsx +199 -0
- package/app/api/calendar-feeds/[feedId]/route.tsx +67 -0
- package/app/api/calendar-feeds/[feedId]/sync/route.tsx +37 -0
- package/app/api/calendar-feeds/events/route.tsx +82 -0
- package/app/api/calendar-feeds/route.tsx +52 -0
- package/app/api/calendar-feeds/sync-all/route.tsx +34 -0
- package/app/api/cron/calendar-feeds/route.tsx +31 -0
- package/app/api/cron/stale-tasks/route.tsx +51 -0
- package/app/api/cron/sync/route.tsx +34 -0
- package/app/api/devices/[deviceId]/route.tsx +25 -0
- package/app/api/devices/route.tsx +41 -0
- package/app/api/export/route.tsx +40 -0
- package/app/api/feedback/route.tsx +54 -0
- package/app/api/folders/[folderId]/route.tsx +51 -0
- package/app/api/folders/route.tsx +37 -0
- package/app/api/graph/route.tsx +242 -0
- package/app/api/guest/route.tsx +58 -0
- package/app/api/health/route.tsx +10 -0
- package/app/api/holidays/countries/route.tsx +14 -0
- package/app/api/holidays/route.tsx +49 -0
- package/app/api/holidays/states/route.tsx +21 -0
- package/app/api/invites/[token]/route.tsx +131 -0
- package/app/api/invites/route.tsx +74 -0
- package/app/api/mcp/generate-token/route.tsx +55 -0
- package/app/api/mcp/revoke-token/[tokenId]/route.tsx +30 -0
- package/app/api/mcp/update-alias/[tokenId]/route.tsx +22 -0
- package/app/api/notes/[noteId]/export/route.tsx +45 -0
- package/app/api/notes/[noteId]/route.tsx +360 -0
- package/app/api/notes/route.tsx +112 -0
- package/app/api/notifications/route.tsx +44 -0
- package/app/api/register/route.tsx +67 -0
- package/app/api/restore/route.tsx +148 -0
- package/app/api/sync/conflicts/[conflictId]/route.tsx +134 -0
- package/app/api/sync/conflicts/route.tsx +48 -0
- package/app/api/sync/status/route.tsx +49 -0
- package/app/api/sync/trigger/route.tsx +15 -0
- package/app/api/tasks/[taskId]/detail/route.tsx +68 -0
- package/app/api/tasks/[taskId]/route.tsx +259 -0
- package/app/api/tasks/bulk/route.tsx +133 -0
- package/app/api/tasks/route.tsx +36 -0
- package/app/api/workspace/active/route.tsx +39 -0
- package/app/api/workspace/create-team/route.tsx +42 -0
- package/app/api/workspace/kanban-statuses/route.tsx +71 -0
- package/app/api/workspace/members/[memberId]/route.tsx +69 -0
- package/app/api/workspace/route.tsx +24 -0
- package/app/download/page.tsx +170 -0
- package/app/favicon.ico +0 -0
- package/app/generated/prisma/client.d.ts +1 -0
- package/app/generated/prisma/client.js +5 -0
- package/app/generated/prisma/default.d.ts +1 -0
- package/app/generated/prisma/default.js +5 -0
- package/app/generated/prisma/edge.d.ts +1 -0
- package/app/generated/prisma/edge.js +497 -0
- package/app/generated/prisma/index-browser.js +523 -0
- package/app/generated/prisma/index.d.ts +46376 -0
- package/app/generated/prisma/index.js +497 -0
- package/app/generated/prisma/package.json +144 -0
- package/app/generated/prisma/query_compiler_fast_bg.js +2 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +2 -0
- package/app/generated/prisma/runtime/client.d.ts +3386 -0
- package/app/generated/prisma/runtime/client.js +86 -0
- package/app/generated/prisma/runtime/index-browser.d.ts +90 -0
- package/app/generated/prisma/runtime/index-browser.js +6 -0
- package/app/generated/prisma/runtime/wasm-compiler-edge.js +76 -0
- package/app/generated/prisma/schema.prisma +456 -0
- package/app/generated/prisma/wasm-edge-light-loader.mjs +5 -0
- package/app/generated/prisma/wasm-worker-loader.mjs +5 -0
- package/app/globals.css +54 -0
- package/app/invite/[token]/page.tsx +52 -0
- package/app/layout.tsx +90 -0
- package/app/mcp/route.tsx +430 -0
- package/app/opengraph-image.tsx +120 -0
- package/app/page.tsx +398 -0
- package/app/privacy/page.tsx +69 -0
- package/app/robots.tsx +25 -0
- package/app/sitemap.tsx +36 -0
- package/app/terms/page.tsx +69 -0
- package/app/upgrade/page.tsx +75 -0
- package/auth.config.ts +33 -0
- package/auth.ts +79 -0
- package/bin/brief.js +224 -0
- package/components/auth/login-form.tsx +302 -0
- package/components/auth/password-checklist.tsx +31 -0
- package/components/auth/password-input.tsx +36 -0
- package/components/auth/switch-account-button.test.tsx +22 -0
- package/components/auth/switch-account-button.tsx +19 -0
- package/components/auth/two-factor-input.tsx +116 -0
- package/components/billing/billing-dashboard.tsx +265 -0
- package/components/billing/card-form.tsx +210 -0
- package/components/billing/claim-account-form.tsx +99 -0
- package/components/branding/app-logo.test.tsx +20 -0
- package/components/branding/app-logo.tsx +25 -0
- package/components/calendar/calendar-agenda.tsx +150 -0
- package/components/calendar/calendar-drag.test.tsx +177 -0
- package/components/calendar/calendar-grid.tsx +357 -0
- package/components/calendar/calendar-hooks.test.tsx +27 -0
- package/components/calendar/calendar-hooks.ts +351 -0
- package/components/calendar/calendar-toolbar.test.tsx +68 -0
- package/components/calendar/calendar-toolbar.tsx +291 -0
- package/components/calendar/calendar-types.ts +148 -0
- package/components/calendar/calendar-view.test.tsx +295 -0
- package/components/calendar/calendar-view.tsx +307 -0
- package/components/calendar/day-detail-popover.tsx +174 -0
- package/components/calendar/task-chip.tsx +86 -0
- package/components/command/command-palette.test.tsx +33 -0
- package/components/command/command-palette.tsx +310 -0
- package/components/download-cta.tsx +87 -0
- package/components/feedback/feedback-popup.tsx +207 -0
- package/components/graph/graph-draw.ts +337 -0
- package/components/graph/graph-overlays.tsx +160 -0
- package/components/graph/graph-page.test.tsx +131 -0
- package/components/graph/graph-page.tsx +263 -0
- package/components/graph/graph-types.ts +47 -0
- package/components/graph/graph-view.tsx +322 -0
- package/components/guide/guide-view.tsx +522 -0
- package/components/kanban/kanban-board.test.tsx +128 -0
- package/components/kanban/kanban-board.tsx +361 -0
- package/components/kanban/kanban-card-menu.tsx +102 -0
- package/components/kanban/kanban-card.tsx +227 -0
- package/components/kanban/kanban-column.tsx +49 -0
- package/components/kanban/kanban-status-context.tsx +28 -0
- package/components/landing/calendar-sandbox.test.tsx +15 -0
- package/components/landing/calendar-sandbox.tsx +107 -0
- package/components/landing/graph-sandbox.test.tsx +27 -0
- package/components/landing/graph-sandbox.tsx +80 -0
- package/components/landing/kanban-sandbox.test.tsx +24 -0
- package/components/landing/kanban-sandbox.tsx +101 -0
- package/components/landing/landing-showcase.test.tsx +21 -0
- package/components/landing/landing-showcase.tsx +54 -0
- package/components/landing/list-sandbox.tsx +86 -0
- package/components/landing/mock-workspace.ts +168 -0
- package/components/landing/notes-sandbox.test.tsx +14 -0
- package/components/landing/notes-sandbox.tsx +88 -0
- package/components/layout/app-shell.tsx +83 -0
- package/components/layout/backup-scheduler.tsx +122 -0
- package/components/layout/bottom-nav.tsx +43 -0
- package/components/layout/icon-bar.test.tsx +29 -0
- package/components/layout/icon-bar.tsx +118 -0
- package/components/layout/mobile-top-bar.tsx +68 -0
- package/components/layout/notes-panel-folder.tsx +127 -0
- package/components/layout/notes-panel-note-item.tsx +140 -0
- package/components/layout/notes-panel-task-tab.tsx +63 -0
- package/components/layout/notes-panel-types.ts +44 -0
- package/components/layout/notes-panel.tsx +476 -0
- package/components/layout/notification-bell.tsx +251 -0
- package/components/layout/paywall-screen.tsx +41 -0
- package/components/layout/pro-banner.tsx +76 -0
- package/components/layout/sw-register.tsx +27 -0
- package/components/layout/workspace-switcher.tsx +90 -0
- package/components/notes/mobile-bottom-sheet.tsx +99 -0
- package/components/notes/note-editor-context-menu.tsx +47 -0
- package/components/notes/note-editor-dom.ts +33 -0
- package/components/notes/note-editor-dropdowns.tsx +484 -0
- package/components/notes/note-editor-hooks.ts +692 -0
- package/components/notes/note-editor-keyboard.ts +305 -0
- package/components/notes/note-editor-overlay.tsx +90 -0
- package/components/notes/note-editor.test.tsx +372 -0
- package/components/notes/note-editor.tsx +662 -0
- package/components/notes/note-preview-pane.tsx +156 -0
- package/components/notes/note-tabs.tsx +120 -0
- package/components/notes/note-types.tsx +157 -0
- package/components/settings/accept-invite.tsx +108 -0
- package/components/settings/agent-token-settings.tsx +369 -0
- package/components/settings/backup-restore-settings.test.tsx +25 -0
- package/components/settings/backup-restore-settings.tsx +327 -0
- package/components/settings/calendar-feeds-settings.tsx +489 -0
- package/components/settings/calendar-general-settings.tsx +174 -0
- package/components/settings/confirm-danger-action.test.tsx +215 -0
- package/components/settings/confirm-danger-action.tsx +65 -0
- package/components/settings/security-settings.tsx +252 -0
- package/components/settings/settings-guidance.test.tsx +98 -0
- package/components/settings/team-settings.tsx +319 -0
- package/components/settings/two-factor-auth.tsx +296 -0
- package/components/settings/workspace-settings-client.tsx +363 -0
- package/components/settings/workspace-settings-form.tsx +73 -0
- package/components/sync/conflict-viewer.tsx +247 -0
- package/components/sync/sync-indicator.tsx +171 -0
- package/components/tasks/snippet-thread.tsx +119 -0
- package/components/tasks/status-dot.tsx +47 -0
- package/components/tasks/task-badge.tsx +43 -0
- package/components/tasks/task-detail.test.tsx +187 -0
- package/components/tasks/task-detail.tsx +458 -0
- package/components/tasks/task-list-filters.test.tsx +75 -0
- package/components/tasks/task-list-filters.tsx +163 -0
- package/components/tasks/task-list-types.ts +20 -0
- package/components/tasks/task-list.test.tsx +175 -0
- package/components/tasks/task-list.tsx +481 -0
- package/components/tasks/task-row.tsx +85 -0
- package/components/tasks/task-table-row.tsx +259 -0
- package/components/ui/skeleton.tsx +3 -0
- package/components/ui/toast.test.tsx +42 -0
- package/components/ui/toast.tsx +70 -0
- package/electron/main.ts +251 -0
- package/electron/preload.ts +56 -0
- package/instrumentation.tsx +23 -0
- package/lib/api-error.ts +50 -0
- package/lib/backup/backup-runner.test.ts +32 -0
- package/lib/backup/backup-runner.ts +19 -0
- package/lib/backup/backup-schedule.test.ts +23 -0
- package/lib/backup/backup-schedule.ts +55 -0
- package/lib/backup/backup-settings.test.ts +30 -0
- package/lib/backup/backup-settings.ts +27 -0
- package/lib/backup/export-notes-zip.test.ts +26 -0
- package/lib/backup/export-notes-zip.ts +82 -0
- package/lib/backup/export-workspace-backup.test.ts +17 -0
- package/lib/backup/export-workspace-backup.ts +77 -0
- package/lib/backup/restore-workspace-from-export.test.ts +18 -0
- package/lib/backup/restore-workspace-from-export.ts +183 -0
- package/lib/backup/types.ts +14 -0
- package/lib/brand-icons.ts +1 -0
- package/lib/calendar-feed-crypto.ts +38 -0
- package/lib/calendar-feed.ts +239 -0
- package/lib/client/online-status.ts +47 -0
- package/lib/conflict-resolver.test.ts +57 -0
- package/lib/conflict-resolver.ts +240 -0
- package/lib/db-init.ts +79 -0
- package/lib/email.ts +159 -0
- package/lib/encryption.test.ts +41 -0
- package/lib/encryption.ts +98 -0
- package/lib/extract-snippet.test.ts +123 -0
- package/lib/extract-snippet.ts +69 -0
- package/lib/kanban-status.ts +55 -0
- package/lib/license.ts +21 -0
- package/lib/limits.ts +31 -0
- package/lib/mcp-auth.test.ts +58 -0
- package/lib/mcp-auth.ts +65 -0
- package/lib/mcp-contract.test.ts +25 -0
- package/lib/mcp-contract.ts +210 -0
- package/lib/mcp-handler.ts +31 -0
- package/lib/mcp-url.test.ts +12 -0
- package/lib/mcp-url.ts +7 -0
- package/lib/mentions.test.ts +45 -0
- package/lib/mentions.ts +73 -0
- package/lib/note-crypto.ts +108 -0
- package/lib/note-sync.ts +201 -0
- package/lib/note-title.ts +93 -0
- package/lib/prisma.ts +193 -0
- package/lib/pro-flush.ts +292 -0
- package/lib/rate-limit.ts +57 -0
- package/lib/stripe.ts +38 -0
- package/lib/sync-worker.ts +388 -0
- package/lib/task-parser.test.ts +91 -0
- package/lib/task-parser.ts +81 -0
- package/lib/task-utils.ts +52 -0
- package/lib/use-is-electron.ts +19 -0
- package/lib/use-is-mobile.ts +22 -0
- package/lib/validation/calendar-feed.ts +31 -0
- package/lib/validation/note.ts +27 -0
- package/lib/validation/task.ts +26 -0
- package/lib/view-preferences.test.ts +54 -0
- package/lib/view-preferences.ts +28 -0
- package/lib/workspace.ts +66 -0
- package/next.config.ts +21 -0
- package/package.json +99 -0
- package/postcss.config.mjs +7 -0
- package/prisma/migrations/20260519021916_init/migration.sql +388 -0
- package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +8 -0
- package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +2 -0
- package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +12 -0
- package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +3 -0
- package/prisma/migrations/20260529030000_add_folders/migration.sql +17 -0
- package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +31 -0
- package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +5 -0
- package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +14 -0
- package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +26 -0
- package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +23 -0
- package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +43 -0
- package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +2 -0
- package/prisma/migrations/20260611050000_add_task_priority/migration.sql +14 -0
- package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +2 -0
- package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +25 -0
- package/prisma/migrations/20260614160000_add_feedback/migration.sql +20 -0
- package/prisma/migrations/20260614210000_add_2fa/migration.sql +4 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +457 -0
- package/public/Logo_icon.svg +1 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +4 -0
- package/public/icon_dark.svg +1 -0
- package/public/knotpad_icon.svg +1 -0
- package/public/knotpad_logo_full.svg +1 -0
- package/public/manifest.json +14 -0
- package/public/next.svg +1 -0
- package/public/sw.js +137 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +35 -0
|
@@ -0,0 +1,75 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
}
|
package/bin/brief.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Knotpad CLI — npx @knotpad/app
|
|
4
|
+
*
|
|
5
|
+
* Sets up and starts a local Knotpad instance in under 60 seconds.
|
|
6
|
+
* Requires: Node 18+ only. No external database needed.
|
|
7
|
+
*
|
|
8
|
+
* What it does:
|
|
9
|
+
* 1. Checks Node version
|
|
10
|
+
* 2. Finds or creates a .env file in the current directory
|
|
11
|
+
* 3. Generates NEXTAUTH_SECRET / MCP_SECRET if not present
|
|
12
|
+
* 4. Starts the Next.js server on port 3099
|
|
13
|
+
* → PGlite (embedded local DB) bootstraps automatically at startup
|
|
14
|
+
* via instrumentation.tsx → lib/prisma.ts + lib/db-init.ts
|
|
15
|
+
* 5. Opens an app-mode window (Chrome/Edge — no address bar) or falls back to browser
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
"use strict";
|
|
19
|
+
|
|
20
|
+
const { execSync, spawn } = require("child_process");
|
|
21
|
+
const fs = require("fs");
|
|
22
|
+
const path = require("path");
|
|
23
|
+
const { randomBytes } = require("crypto");
|
|
24
|
+
|
|
25
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function log(msg) { process.stdout.write(`\x1b[36m[knotpad]\x1b[0m ${msg}\n`); }
|
|
28
|
+
function ok(msg) { process.stdout.write(`\x1b[32m[knotpad]\x1b[0m ${msg}\n`); }
|
|
29
|
+
function die(msg) { process.stderr.write(`\x1b[31m[knotpad]\x1b[0m ${msg}\n`); process.exit(1); }
|
|
30
|
+
|
|
31
|
+
function genSecret(bytes = 32) {
|
|
32
|
+
return randomBytes(bytes).toString("base64url");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function openBrowser(url) {
|
|
36
|
+
const cmd =
|
|
37
|
+
process.platform === "win32" ? `start "" "${url}"` :
|
|
38
|
+
process.platform === "darwin" ? `open "${url}"` :
|
|
39
|
+
`xdg-open "${url}"`;
|
|
40
|
+
try { execSync(cmd, { stdio: "ignore" }); } catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Chrome / Edge app-mode detection ──────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Find a Chrome or Edge binary on the system. Returns { bin, name } or null.
|
|
47
|
+
*/
|
|
48
|
+
function findChromeOrEdge() {
|
|
49
|
+
const platform = process.platform;
|
|
50
|
+
|
|
51
|
+
if (platform === "win32") {
|
|
52
|
+
const pf = process.env.ProgramFiles || "";
|
|
53
|
+
const pfx = process.env["ProgramFiles(x86)"] || "";
|
|
54
|
+
const local = process.env.LOCALAPPDATA || "";
|
|
55
|
+
const candidates = [
|
|
56
|
+
{ bin: path.join(pf, "Google", "Chrome", "Application", "chrome.exe"), name: "chrome" },
|
|
57
|
+
{ bin: path.join(pfx, "Google", "Chrome", "Application", "chrome.exe"), name: "chrome" },
|
|
58
|
+
{ bin: path.join(local, "Google", "Chrome", "Application", "chrome.exe"), name: "chrome" },
|
|
59
|
+
{ bin: path.join(pf, "Microsoft", "Edge", "Application", "msedge.exe"), name: "edge" },
|
|
60
|
+
{ bin: path.join(pfx, "Microsoft", "Edge", "Application", "msedge.exe"), name: "edge" },
|
|
61
|
+
];
|
|
62
|
+
for (const c of candidates) {
|
|
63
|
+
if (c.bin && fs.existsSync(c.bin)) return c;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (platform === "darwin") {
|
|
69
|
+
const candidates = [
|
|
70
|
+
{ bin: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", name: "chrome" },
|
|
71
|
+
{ bin: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", name: "edge" },
|
|
72
|
+
];
|
|
73
|
+
for (const c of candidates) {
|
|
74
|
+
if (fs.existsSync(c.bin)) return c;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Linux / other unix-like
|
|
80
|
+
const bins = [
|
|
81
|
+
{ bin: "google-chrome", name: "chrome" },
|
|
82
|
+
{ bin: "chromium", name: "chrome" },
|
|
83
|
+
{ bin: "microsoft-edge", name: "edge" },
|
|
84
|
+
];
|
|
85
|
+
for (const c of bins) {
|
|
86
|
+
try {
|
|
87
|
+
execSync(`which ${c.bin}`, { stdio: "ignore" });
|
|
88
|
+
return { bin: c.bin, name: c.name };
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Open the app URL in a Chrome/Edge app-mode window (no address bar, no tabs).
|
|
96
|
+
* Falls back to the regular browser if no Chrome/Edge is found.
|
|
97
|
+
*/
|
|
98
|
+
function openAppWindow(url) {
|
|
99
|
+
const browser = findChromeOrEdge();
|
|
100
|
+
if (!browser) {
|
|
101
|
+
log("No Chrome/Edge found — falling back to default browser");
|
|
102
|
+
return openBrowser(url);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const profileDir = path.join(WORK_DIR, ".brief", "chrome-profile");
|
|
107
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
108
|
+
|
|
109
|
+
const args = [
|
|
110
|
+
`--app=${url}`,
|
|
111
|
+
`--user-data-dir=${profileDir}`,
|
|
112
|
+
"--window-size=1400,900",
|
|
113
|
+
"--no-first-run",
|
|
114
|
+
"--no-default-browser-check",
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
log(`Launching ${browser.name} app-mode window…`);
|
|
118
|
+
const child = spawn(browser.bin, args, {
|
|
119
|
+
detached: true,
|
|
120
|
+
stdio: "ignore",
|
|
121
|
+
});
|
|
122
|
+
child.unref();
|
|
123
|
+
} catch {
|
|
124
|
+
log("Failed to launch app-mode window — falling back to default browser");
|
|
125
|
+
openBrowser(url);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── 1. Node version ────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
const [major] = process.versions.node.split(".").map(Number);
|
|
132
|
+
if (major < 18) die(`Node 18+ required (you have ${process.versions.node}). Install from https://nodejs.org`);
|
|
133
|
+
|
|
134
|
+
// ── 2. .env setup ──────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
const PKG_DIR = path.resolve(__dirname, "..");
|
|
137
|
+
const WORK_DIR = process.cwd();
|
|
138
|
+
const ENV_PATH = path.join(WORK_DIR, ".env");
|
|
139
|
+
|
|
140
|
+
let env = {};
|
|
141
|
+
if (fs.existsSync(ENV_PATH)) {
|
|
142
|
+
const raw = fs.readFileSync(ENV_PATH, "utf8");
|
|
143
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
144
|
+
const m = /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(line.trim());
|
|
145
|
+
if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
146
|
+
}
|
|
147
|
+
log(".env found, loading existing config");
|
|
148
|
+
} else {
|
|
149
|
+
log("Creating .env with defaults…");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Fill defaults — no DATABASE_URL needed; PGlite handles local storage automatically
|
|
153
|
+
// Port 3099 avoids clashing with dev servers (3000) commonly running alongside.
|
|
154
|
+
const defaults = {
|
|
155
|
+
NEXTAUTH_SECRET: genSecret(),
|
|
156
|
+
NEXTAUTH_URL: "http://localhost:3099",
|
|
157
|
+
MCP_SECRET: genSecret(),
|
|
158
|
+
ENCRYPTION_PEPPER: genSecret(),
|
|
159
|
+
NEXT_PUBLIC_APP_URL: "http://localhost:3099",
|
|
160
|
+
NEXT_PUBLIC_MCP_URL: "http://localhost:3099/mcp",
|
|
161
|
+
IS_CLOUD: "false",
|
|
162
|
+
CRON_SECRET: genSecret(16),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
let changed = false;
|
|
166
|
+
for (const [k, v] of Object.entries(defaults)) {
|
|
167
|
+
if (!env[k]) { env[k] = v; changed = true; }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (changed) {
|
|
171
|
+
const lines = Object.entries(env).map(([k, v]) => `${k}="${v}"`).join("\n");
|
|
172
|
+
fs.writeFileSync(ENV_PATH, lines + "\n", "utf8");
|
|
173
|
+
ok(".env written");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Apply env for this process
|
|
177
|
+
for (const [k, v] of Object.entries(env)) {
|
|
178
|
+
process.env[k] = v;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── 3. Build (if not already built) ───────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
// BUILD_ID is only written after a successful build — a leftover .next/ from a
|
|
184
|
+
// failed build won't have it, so we correctly re-run in that case.
|
|
185
|
+
const BUILD_ID = path.join(PKG_DIR, ".next", "BUILD_ID");
|
|
186
|
+
if (!fs.existsSync(BUILD_ID)) {
|
|
187
|
+
log("Building Knotpad (first run — takes ~30 seconds)…");
|
|
188
|
+
try {
|
|
189
|
+
execSync("npx next build", { cwd: PKG_DIR, stdio: "inherit", env: process.env });
|
|
190
|
+
ok("Build complete");
|
|
191
|
+
} catch {
|
|
192
|
+
die("Build failed. Run `npx @knotpad/app` again after fixing any errors.");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── 4. Start server ────────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
// Pre-create PGlite data directory so the app can open it immediately at startup
|
|
199
|
+
fs.mkdirSync(path.join(WORK_DIR, ".brief", "db"), { recursive: true });
|
|
200
|
+
|
|
201
|
+
const PORT = parseInt(process.env.PORT ?? "3099", 10);
|
|
202
|
+
const URL = `http://localhost:${PORT}`;
|
|
203
|
+
|
|
204
|
+
ok(`\nStarting Knotpad on ${URL}\n`);
|
|
205
|
+
process.stdout.write(` ${"\x1b[2m"}Ctrl+C to stop${"\x1b[0m"}\n\n`);
|
|
206
|
+
|
|
207
|
+
const NEXT_BIN = path.join(PKG_DIR, "node_modules", "next", "dist", "bin", "next");
|
|
208
|
+
const server = spawn(process.execPath, [NEXT_BIN, "start", "--port", String(PORT)], {
|
|
209
|
+
cwd: PKG_DIR,
|
|
210
|
+
stdio: "inherit",
|
|
211
|
+
env: process.env,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
server.on("error", (e) => die(`Failed to start server: ${e.message}`));
|
|
215
|
+
server.on("exit", (code) => {
|
|
216
|
+
if (code !== 0 && code !== null) die(`Server exited with code ${code}`);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Open in app-mode Chrome/Edge window (falls back to browser if not found)
|
|
220
|
+
setTimeout(() => openAppWindow(URL), 1500);
|
|
221
|
+
|
|
222
|
+
// Graceful exit
|
|
223
|
+
process.on("SIGINT", () => { server.kill("SIGINT"); process.exit(0); });
|
|
224
|
+
process.on("SIGTERM", () => { server.kill("SIGTERM"); process.exit(0); });
|