@knotpad/app 0.1.4 → 0.1.6
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/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/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 +49 -3
- 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
- package/brief.js +0 -311
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restore flow — two paths:
|
|
3
|
+
*
|
|
4
|
+
* 1. POST { type: "cloud" } — paid users: trigger cloud pull to repopulate local DB
|
|
5
|
+
* 2. POST { type: "file-replace", data: <JSON export> } — free/paid: validate
|
|
6
|
+
* and replace the current workspace state with a Knotpad JSON export.
|
|
7
|
+
*
|
|
8
|
+
* .db file copy-paste (copy brief.db between machines) is handled at the CLI level
|
|
9
|
+
* in the npx distribution (Phase 9). The web app restore accepts JSON exports.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
13
|
+
import { auth } from "@/auth";
|
|
14
|
+
import { prisma } from "@/lib/prisma";
|
|
15
|
+
import { runSync } from "@/lib/sync-worker";
|
|
16
|
+
import { writeTombstone } from "@/lib/conflict-resolver";
|
|
17
|
+
import {
|
|
18
|
+
restoreWorkspaceFromExport,
|
|
19
|
+
type BriefExport,
|
|
20
|
+
} from "@/lib/backup/restore-workspace-from-export";
|
|
21
|
+
|
|
22
|
+
const SUPPORTED_EXPORT_VERSION = 1;
|
|
23
|
+
|
|
24
|
+
function validateExport(data: unknown): { valid: boolean; error?: string } {
|
|
25
|
+
if (!data || typeof data !== "object") {
|
|
26
|
+
return { valid: false, error: "Invalid file: not a JSON object" };
|
|
27
|
+
}
|
|
28
|
+
const d = data as Record<string, unknown>;
|
|
29
|
+
if (d.version !== SUPPORTED_EXPORT_VERSION) {
|
|
30
|
+
return {
|
|
31
|
+
valid: false,
|
|
32
|
+
error: `Unsupported export version ${d.version}. Expected ${SUPPORTED_EXPORT_VERSION}.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (!Array.isArray(d.notes)) {
|
|
36
|
+
return { valid: false, error: "Invalid file: missing notes array" };
|
|
37
|
+
}
|
|
38
|
+
if (!d.workspace || typeof d.workspace !== "object") {
|
|
39
|
+
return { valid: false, error: "Invalid file: missing workspace info" };
|
|
40
|
+
}
|
|
41
|
+
return { valid: true };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function POST(req: NextRequest) {
|
|
45
|
+
const session = await auth();
|
|
46
|
+
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
47
|
+
|
|
48
|
+
const body = await req.json().catch(() => null);
|
|
49
|
+
if (!body) return NextResponse.json({ error: "Invalid body" }, { status: 400 });
|
|
50
|
+
|
|
51
|
+
const { type } = body as { type: string };
|
|
52
|
+
|
|
53
|
+
const member = await prisma.workspaceMember.findFirst({
|
|
54
|
+
where: { userId: session.user.id, role: { in: ["OWNER", "ADMIN"] } },
|
|
55
|
+
include: { workspace: true },
|
|
56
|
+
});
|
|
57
|
+
if (!member) {
|
|
58
|
+
return NextResponse.json({ error: "Must be workspace owner or admin" }, { status: 403 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- Cloud restore ---
|
|
62
|
+
if (type === "cloud") {
|
|
63
|
+
if (!member.workspace.isPro) {
|
|
64
|
+
return NextResponse.json(
|
|
65
|
+
{ error: "Cloud restore requires an active Pro subscription" },
|
|
66
|
+
{ status: 403 }
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const result = await runSync(member.workspaceId);
|
|
70
|
+
return NextResponse.json({
|
|
71
|
+
restored: true,
|
|
72
|
+
source: "cloud",
|
|
73
|
+
...result,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- File replace restore ---
|
|
78
|
+
if (type === "file-replace") {
|
|
79
|
+
const { data } = body as { data: unknown };
|
|
80
|
+
const { valid, error } = validateExport(data);
|
|
81
|
+
if (!valid) {
|
|
82
|
+
return NextResponse.json({ error }, { status: 422 });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const exportData = data as BriefExport;
|
|
86
|
+
const pushedToCloud = member.workspace.isPro && member.workspace.isCloud;
|
|
87
|
+
|
|
88
|
+
await restoreWorkspaceFromExport({
|
|
89
|
+
workspaceId: member.workspaceId,
|
|
90
|
+
exportData,
|
|
91
|
+
pushToCloud: pushedToCloud,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return NextResponse.json({
|
|
95
|
+
restored: true,
|
|
96
|
+
source: "file-replace",
|
|
97
|
+
notesRestored: exportData.notes.length,
|
|
98
|
+
tasksRestored: exportData.notes.reduce((total, note) => total + note.tasks.length, 0),
|
|
99
|
+
pushedToCloud,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return NextResponse.json(
|
|
104
|
+
{ error: "type must be 'cloud' or 'file-replace'" },
|
|
105
|
+
{ status: 400 }
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- DELETE: wipe workspace data before a restore ---
|
|
110
|
+
export async function DELETE() {
|
|
111
|
+
const session = await auth();
|
|
112
|
+
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
113
|
+
|
|
114
|
+
const member = await prisma.workspaceMember.findFirst({
|
|
115
|
+
where: { userId: session.user.id, role: "OWNER" },
|
|
116
|
+
});
|
|
117
|
+
if (!member) {
|
|
118
|
+
return NextResponse.json({ error: "Only the workspace owner can wipe data" }, { status: 403 });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const workspaceId = member.workspaceId;
|
|
122
|
+
|
|
123
|
+
// Tombstone everything before wiping — prevents ghost resurrection on sync
|
|
124
|
+
const [notes, tasks] = await Promise.all([
|
|
125
|
+
prisma.note.findMany({ where: { workspaceId }, select: { id: true } }),
|
|
126
|
+
prisma.task.findMany({ where: { workspaceId }, select: { id: true } }),
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
await Promise.all([
|
|
130
|
+
...notes.map((n) => writeTombstone(workspaceId, "note", n.id)),
|
|
131
|
+
...tasks.map((t) => writeTombstone(workspaceId, "task", t.id)),
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
// Delete notes and tasks (cascade handles references, audit logs)
|
|
135
|
+
await prisma.$transaction([
|
|
136
|
+
prisma.task.deleteMany({ where: { workspaceId } }),
|
|
137
|
+
prisma.note.deleteMany({ where: { workspaceId } }),
|
|
138
|
+
prisma.conflictLog.deleteMany({ where: { workspaceId } }),
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
await prisma.syncState.upsert({
|
|
142
|
+
where: { workspaceId },
|
|
143
|
+
update: { lastSyncedAt: null, localVersion: 0, cloudVersion: 0, noteSnapshots: null },
|
|
144
|
+
create: { workspaceId },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return NextResponse.json({ wiped: true });
|
|
148
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { auth } from "@/auth";
|
|
3
|
+
import { prisma, getPrimaryDb } from "@/lib/prisma";
|
|
4
|
+
import { saveSnapshot } from "@/lib/note-sync";
|
|
5
|
+
import { encryptContent, decryptContent } from "@/lib/note-crypto";
|
|
6
|
+
import { getActiveWorkspaceId } from "@/lib/workspace";
|
|
7
|
+
|
|
8
|
+
const VALID_TASK_STATUSES = ["OPEN", "CLAIMED", "IN_PROGRESS", "REVIEW", "DONE"];
|
|
9
|
+
|
|
10
|
+
export async function PATCH(
|
|
11
|
+
req: NextRequest,
|
|
12
|
+
{ params }: { params: Promise<{ conflictId: string }> }
|
|
13
|
+
) {
|
|
14
|
+
const session = await auth();
|
|
15
|
+
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
16
|
+
|
|
17
|
+
const { conflictId } = await params;
|
|
18
|
+
const body = await req.json();
|
|
19
|
+
|
|
20
|
+
// resolution: "local" | "cloud" | "custom"
|
|
21
|
+
// value: required when resolution === "custom"
|
|
22
|
+
const { resolution, value } = body as {
|
|
23
|
+
resolution: "local" | "cloud" | "custom";
|
|
24
|
+
value?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (!["local", "cloud", "custom"].includes(resolution)) {
|
|
28
|
+
return NextResponse.json({ error: "Invalid resolution" }, { status: 400 });
|
|
29
|
+
}
|
|
30
|
+
if (resolution === "custom" && !value) {
|
|
31
|
+
return NextResponse.json({ error: "value required for custom resolution" }, { status: 400 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const workspaceId = await getActiveWorkspaceId(session.user.id);
|
|
35
|
+
if (!workspaceId) return NextResponse.json({ error: "No workspace" }, { status: 404 });
|
|
36
|
+
|
|
37
|
+
const db = await getPrimaryDb(workspaceId);
|
|
38
|
+
const conflict = await db.conflictLog.findUnique({ where: { id: conflictId } });
|
|
39
|
+
if (!conflict || conflict.workspaceId !== workspaceId) {
|
|
40
|
+
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
41
|
+
}
|
|
42
|
+
if (conflict.resolvedAt) {
|
|
43
|
+
return NextResponse.json({ error: "Already resolved" }, { status: 409 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// localValue/cloudValue are ciphertext (notes) or plain status (tasks);
|
|
47
|
+
// a custom value arrives as plaintext from the editor.
|
|
48
|
+
const resolvedRaw =
|
|
49
|
+
resolution === "local"
|
|
50
|
+
? conflict.localValue
|
|
51
|
+
: resolution === "cloud"
|
|
52
|
+
? conflict.cloudValue
|
|
53
|
+
: value!;
|
|
54
|
+
|
|
55
|
+
if (conflict.entityType === "task" && !VALID_TASK_STATUSES.includes(resolvedRaw)) {
|
|
56
|
+
return NextResponse.json({ error: "Invalid task status" }, { status: 400 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// For notes: store ciphertext, but keep the plaintext as the merge-base snapshot.
|
|
60
|
+
let storedContent = resolvedRaw;
|
|
61
|
+
let plaintextForSnapshot = resolvedRaw;
|
|
62
|
+
if (conflict.entityType === "note") {
|
|
63
|
+
if (resolution === "custom") {
|
|
64
|
+
plaintextForSnapshot = resolvedRaw; // already plaintext
|
|
65
|
+
storedContent = await encryptContent(resolvedRaw, workspaceId);
|
|
66
|
+
} else {
|
|
67
|
+
storedContent = resolvedRaw; // already ciphertext
|
|
68
|
+
plaintextForSnapshot = await decryptContent(resolvedRaw, workspaceId);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await db.$transaction(async (tx) => {
|
|
74
|
+
if (conflict.entityType === "note") {
|
|
75
|
+
await tx.note.update({
|
|
76
|
+
where: { id: conflict.entityId },
|
|
77
|
+
data: { content: storedContent, version: { increment: 1 }, pendingSync: false },
|
|
78
|
+
});
|
|
79
|
+
} else if (conflict.entityType === "task") {
|
|
80
|
+
await tx.task.update({
|
|
81
|
+
where: { id: conflict.entityId },
|
|
82
|
+
data: { status: resolvedRaw as never, version: { increment: 1 }, pendingSync: false },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await tx.conflictLog.update({
|
|
87
|
+
where: { id: conflictId },
|
|
88
|
+
data: { resolvedBy: "user", resolvedAt: new Date() },
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
} catch (txErr: unknown) {
|
|
92
|
+
// P2025 = "Record not found" — the entity was deleted after the conflict was created.
|
|
93
|
+
// Mark the conflict resolved so it no longer appears in the UI.
|
|
94
|
+
const code = (txErr as { code?: string })?.code;
|
|
95
|
+
if (code === "P2025") {
|
|
96
|
+
await db.conflictLog.update({
|
|
97
|
+
where: { id: conflictId },
|
|
98
|
+
data: { resolvedBy: "user", resolvedAt: new Date() },
|
|
99
|
+
}).catch(() => {});
|
|
100
|
+
return NextResponse.json(
|
|
101
|
+
{ error: "The entity was deleted before the conflict could be resolved", resolved: true },
|
|
102
|
+
{ status: 410 }
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
throw txErr;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Update snapshot so future merges use the resolved (plaintext) content as base.
|
|
109
|
+
// If the snapshot write fails, undo the resolution so the user can retry cleanly.
|
|
110
|
+
if (conflict.entityType === "note") {
|
|
111
|
+
try {
|
|
112
|
+
await saveSnapshot(workspaceId, conflict.entityId, plaintextForSnapshot, db);
|
|
113
|
+
} catch (snapshotErr) {
|
|
114
|
+
console.error("[brief] conflict snapshot failed, reopening conflict:", snapshotErr);
|
|
115
|
+
try {
|
|
116
|
+
await db.conflictLog.update({
|
|
117
|
+
where: { id: conflictId },
|
|
118
|
+
data: { resolvedBy: null, resolvedAt: null },
|
|
119
|
+
});
|
|
120
|
+
} catch { /* best-effort reopen */ }
|
|
121
|
+
return NextResponse.json({ error: "Failed to save snapshot — please retry" }, { status: 500 });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Also clear pendingSync on local PGlite if this was a Pro workspace conflict
|
|
126
|
+
if (db !== prisma && conflict.entityType === "note") {
|
|
127
|
+
await prisma.note.updateMany({
|
|
128
|
+
where: { id: conflict.entityId, workspaceId },
|
|
129
|
+
data: { pendingSync: false },
|
|
130
|
+
}).catch(() => {});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return NextResponse.json({ resolved: true, resolution });
|
|
134
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { auth } from "@/auth";
|
|
3
|
+
import { getPrimaryDb } from "@/lib/prisma";
|
|
4
|
+
import { decryptContent } from "@/lib/note-crypto";
|
|
5
|
+
import { getActiveWorkspaceId } from "@/lib/workspace";
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
const session = await auth();
|
|
9
|
+
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
10
|
+
|
|
11
|
+
const workspaceId = await getActiveWorkspaceId(session.user.id);
|
|
12
|
+
if (!workspaceId) return NextResponse.json({ error: "No workspace" }, { status: 404 });
|
|
13
|
+
|
|
14
|
+
const db = await getPrimaryDb(workspaceId);
|
|
15
|
+
const conflicts = await db.conflictLog.findMany({
|
|
16
|
+
where: { workspaceId, resolvedAt: null },
|
|
17
|
+
orderBy: { createdAt: "desc" },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Enrich with note/task titles. Note conflict values are stored as ciphertext,
|
|
21
|
+
// so decrypt them here for the diff view (tasks store plain status strings).
|
|
22
|
+
const enriched = await Promise.all(
|
|
23
|
+
conflicts.map(async (c) => {
|
|
24
|
+
let label = c.entityId;
|
|
25
|
+
let { localValue, cloudValue } = c;
|
|
26
|
+
if (c.entityType === "note") {
|
|
27
|
+
const note = await db.note.findUnique({
|
|
28
|
+
where: { id: c.entityId },
|
|
29
|
+
select: { title: true },
|
|
30
|
+
});
|
|
31
|
+
label = note?.title ?? c.entityId;
|
|
32
|
+
[localValue, cloudValue] = await Promise.all([
|
|
33
|
+
decryptContent(c.localValue, workspaceId),
|
|
34
|
+
decryptContent(c.cloudValue, workspaceId),
|
|
35
|
+
]);
|
|
36
|
+
} else if (c.entityType === "task") {
|
|
37
|
+
const task = await db.task.findUnique({
|
|
38
|
+
where: { id: c.entityId },
|
|
39
|
+
select: { title: true },
|
|
40
|
+
});
|
|
41
|
+
label = task?.title ?? c.entityId;
|
|
42
|
+
}
|
|
43
|
+
return { ...c, label, localValue, cloudValue };
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return NextResponse.json(enriched);
|
|
48
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { auth } from "@/auth";
|
|
3
|
+
import { prisma, getPrimaryDb } from "@/lib/prisma";
|
|
4
|
+
import { getActiveWorkspaceId } from "@/lib/workspace";
|
|
5
|
+
import { isWorkspacePro, isWorkspaceCloud } from "@/lib/license";
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
const session = await auth();
|
|
9
|
+
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
10
|
+
|
|
11
|
+
const activeWs = await getActiveWorkspaceId(session.user.id);
|
|
12
|
+
const member = await prisma.workspaceMember.findFirst({
|
|
13
|
+
where: { userId: session.user.id, workspaceId: activeWs ?? undefined },
|
|
14
|
+
include: { workspace: { include: { syncState: true } } },
|
|
15
|
+
});
|
|
16
|
+
if (!member) return NextResponse.json({ error: "No workspace" }, { status: 404 });
|
|
17
|
+
|
|
18
|
+
const { workspace } = member;
|
|
19
|
+
const db = await getPrimaryDb(workspace.id);
|
|
20
|
+
const syncState = workspace.syncState;
|
|
21
|
+
|
|
22
|
+
// Derive entitlement from canonical license fields (not the cached booleans,
|
|
23
|
+
// which drift on direct DB edits). See lib/license.ts.
|
|
24
|
+
const isPro = isWorkspacePro(workspace);
|
|
25
|
+
const isCloud = isWorkspaceCloud(workspace);
|
|
26
|
+
|
|
27
|
+
const unresolvedConflicts = await db.conflictLog.count({
|
|
28
|
+
where: { workspaceId: workspace.id, resolvedAt: null },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Count locally-buffered writes not yet flushed to Neon (Pro offline mode)
|
|
32
|
+
const [pendingNotes, pendingTasks] = isPro
|
|
33
|
+
? await Promise.all([
|
|
34
|
+
prisma.note.count({ where: { workspaceId: workspace.id, pendingSync: true } }),
|
|
35
|
+
prisma.task.count({ where: { workspaceId: workspace.id, pendingSync: true } }),
|
|
36
|
+
])
|
|
37
|
+
: [0, 0];
|
|
38
|
+
|
|
39
|
+
return NextResponse.json({
|
|
40
|
+
isCloud,
|
|
41
|
+
isPro,
|
|
42
|
+
cloudFirst: isPro && isCloud,
|
|
43
|
+
lastSyncedAt: syncState?.lastSyncedAt ?? null,
|
|
44
|
+
localVersion: syncState?.localVersion ?? 0,
|
|
45
|
+
cloudVersion: syncState?.cloudVersion ?? 0,
|
|
46
|
+
unresolvedConflicts,
|
|
47
|
+
pendingWrites: pendingNotes + pendingTasks,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { auth } from "@/auth";
|
|
3
|
+
import { runProFlush } from "@/lib/pro-flush";
|
|
4
|
+
import { getActiveWorkspaceId } from "@/lib/workspace";
|
|
5
|
+
|
|
6
|
+
export async function POST() {
|
|
7
|
+
const session = await auth();
|
|
8
|
+
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
9
|
+
|
|
10
|
+
const workspaceId = await getActiveWorkspaceId(session.user.id);
|
|
11
|
+
if (!workspaceId) return NextResponse.json({ error: "No workspace" }, { status: 404 });
|
|
12
|
+
|
|
13
|
+
const result = await runProFlush(workspaceId);
|
|
14
|
+
return NextResponse.json(result);
|
|
15
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { auth } from "@/auth";
|
|
3
|
+
import { getPrimaryDb } from "@/lib/prisma";
|
|
4
|
+
import { decryptContent } from "@/lib/note-crypto";
|
|
5
|
+
import { getActiveWorkspaceId } from "@/lib/workspace";
|
|
6
|
+
import { extractSnippet } from "@/lib/extract-snippet";
|
|
7
|
+
|
|
8
|
+
export async function GET(
|
|
9
|
+
_req: NextRequest,
|
|
10
|
+
{ params }: { params: Promise<{ taskId: string }> }
|
|
11
|
+
) {
|
|
12
|
+
const session = await auth();
|
|
13
|
+
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
14
|
+
|
|
15
|
+
const { taskId } = await params;
|
|
16
|
+
|
|
17
|
+
const workspaceId = await getActiveWorkspaceId(session.user.id);
|
|
18
|
+
if (!workspaceId) return NextResponse.json({ error: "No workspace" }, { status: 404 });
|
|
19
|
+
|
|
20
|
+
const db = await getPrimaryDb(workspaceId);
|
|
21
|
+
const task = await db.task.findFirst({
|
|
22
|
+
where: { id: taskId, workspaceId },
|
|
23
|
+
include: {
|
|
24
|
+
note: { select: { id: true, title: true, content: true } },
|
|
25
|
+
assignee: { select: { id: true, name: true, email: true, image: true } },
|
|
26
|
+
auditLogs: {
|
|
27
|
+
orderBy: { createdAt: "desc" },
|
|
28
|
+
take: 20,
|
|
29
|
+
include: { user: { select: { id: true, name: true } } },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!task) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
35
|
+
|
|
36
|
+
// Extract the paragraph context from the source note (decrypt first)
|
|
37
|
+
const noteContent = await decryptContent(task.note.content, workspaceId);
|
|
38
|
+
const snippet = extractSnippet(noteContent, task.title);
|
|
39
|
+
|
|
40
|
+
// Find other notes that reference this task's title
|
|
41
|
+
const otherRefs = await db.taskReference.findMany({
|
|
42
|
+
where: { taskId },
|
|
43
|
+
include: { note: { select: { id: true, title: true, content: true } } },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const references = [
|
|
47
|
+
{
|
|
48
|
+
noteId: task.note.id,
|
|
49
|
+
noteTitle: task.note.title,
|
|
50
|
+
snippet,
|
|
51
|
+
},
|
|
52
|
+
...await Promise.all(
|
|
53
|
+
otherRefs
|
|
54
|
+
.filter((r) => r.noteId !== task.noteId)
|
|
55
|
+
.map(async (r) => {
|
|
56
|
+
const refContent = await decryptContent(r.note.content, workspaceId);
|
|
57
|
+
return {
|
|
58
|
+
noteId: r.noteId,
|
|
59
|
+
noteTitle: r.note.title,
|
|
60
|
+
snippet: r.snippet || extractSnippet(refContent, task.title),
|
|
61
|
+
};
|
|
62
|
+
})
|
|
63
|
+
),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
return NextResponse.json({ ...task, references });
|
|
67
|
+
}
|
|
68
|
+
|