@knotpad/app 0.1.0 → 0.1.1
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/bin/brief.js +165 -78
- package/package.json +3 -17
- package/app/(app)/calendar/page.tsx +0 -57
- package/app/(app)/error.tsx +0 -35
- package/app/(app)/graph/page.tsx +0 -32
- package/app/(app)/guide/page.tsx +0 -21
- package/app/(app)/kanban/loading.tsx +0 -24
- package/app/(app)/kanban/page.tsx +0 -59
- package/app/(app)/layout.tsx +0 -122
- package/app/(app)/list/loading.tsx +0 -21
- package/app/(app)/list/page.tsx +0 -137
- package/app/(app)/loading.tsx +0 -18
- package/app/(app)/notes/[noteId]/page.tsx +0 -84
- package/app/(app)/notes/layout.tsx +0 -30
- package/app/(app)/notes/page.tsx +0 -39
- package/app/(app)/page.tsx +0 -5
- package/app/(app)/settings/agent-token/page.tsx +0 -59
- package/app/(app)/settings/backup/page.tsx +0 -49
- package/app/(app)/settings/billing/page.tsx +0 -53
- package/app/(app)/settings/calendar/page.tsx +0 -41
- package/app/(app)/settings/layout.test.tsx +0 -39
- package/app/(app)/settings/layout.tsx +0 -71
- package/app/(app)/settings/page.tsx +0 -4
- package/app/(app)/settings/security/page.tsx +0 -43
- package/app/(app)/settings/team/page.tsx +0 -74
- package/app/(app)/settings/workspace/page.tsx +0 -27
- package/app/(app)/tasks/[taskId]/page.tsx +0 -79
- package/app/(auth)/forgot-password/page.tsx +0 -106
- package/app/(auth)/guest/page.tsx +0 -56
- package/app/(auth)/layout.tsx +0 -13
- package/app/(auth)/login/page.tsx +0 -14
- package/app/(auth)/register/page.tsx +0 -193
- package/app/(auth)/reset-password/page.tsx +0 -138
- package/app/api/account/claim/route.tsx +0 -135
- package/app/api/admin/backfill-encryption/route.tsx +0 -43
- package/app/api/admin/license/route.tsx +0 -42
- package/app/api/auth/2fa/route.tsx +0 -148
- package/app/api/auth/[...nextauth]/route.tsx +0 -3
- package/app/api/auth/change-password/route.tsx +0 -61
- package/app/api/auth/check-2fa/route.tsx +0 -19
- package/app/api/auth/forgot-password/route.tsx +0 -65
- package/app/api/auth/reset-password/route.tsx +0 -52
- package/app/api/auth/verify-2fa/route.tsx +0 -88
- package/app/api/backup/download/db/route.ts +0 -29
- package/app/api/backup/download/notes/route.ts +0 -25
- package/app/api/backup/settings/route.ts +0 -92
- package/app/api/billing/checkout/route.tsx +0 -81
- package/app/api/billing/migrate/route.tsx +0 -163
- package/app/api/billing/portal/route.tsx +0 -24
- package/app/api/billing/setup-intent/route.tsx +0 -55
- package/app/api/billing/status/route.tsx +0 -36
- package/app/api/billing/subscribe/route.tsx +0 -85
- package/app/api/billing/webhook/route.tsx +0 -199
- package/app/api/calendar-feeds/[feedId]/route.tsx +0 -67
- package/app/api/calendar-feeds/[feedId]/sync/route.tsx +0 -37
- package/app/api/calendar-feeds/events/route.tsx +0 -82
- package/app/api/calendar-feeds/route.tsx +0 -52
- package/app/api/calendar-feeds/sync-all/route.tsx +0 -34
- package/app/api/cron/calendar-feeds/route.tsx +0 -31
- package/app/api/cron/stale-tasks/route.tsx +0 -51
- package/app/api/cron/sync/route.tsx +0 -34
- package/app/api/devices/[deviceId]/route.tsx +0 -25
- package/app/api/devices/route.tsx +0 -41
- package/app/api/export/route.tsx +0 -40
- package/app/api/feedback/route.tsx +0 -54
- package/app/api/folders/[folderId]/route.tsx +0 -51
- package/app/api/folders/route.tsx +0 -37
- package/app/api/graph/route.tsx +0 -242
- package/app/api/guest/route.tsx +0 -58
- package/app/api/health/route.tsx +0 -10
- package/app/api/holidays/countries/route.tsx +0 -14
- package/app/api/holidays/route.tsx +0 -49
- package/app/api/holidays/states/route.tsx +0 -21
- package/app/api/invites/[token]/route.tsx +0 -131
- package/app/api/invites/route.tsx +0 -74
- package/app/api/mcp/generate-token/route.tsx +0 -55
- package/app/api/mcp/revoke-token/[tokenId]/route.tsx +0 -30
- package/app/api/mcp/update-alias/[tokenId]/route.tsx +0 -22
- package/app/api/notes/[noteId]/export/route.tsx +0 -45
- package/app/api/notes/[noteId]/route.tsx +0 -360
- package/app/api/notes/route.tsx +0 -112
- package/app/api/notifications/route.tsx +0 -44
- package/app/api/register/route.tsx +0 -67
- package/app/api/restore/route.tsx +0 -148
- package/app/api/sync/conflicts/[conflictId]/route.tsx +0 -134
- package/app/api/sync/conflicts/route.tsx +0 -48
- package/app/api/sync/status/route.tsx +0 -49
- package/app/api/sync/trigger/route.tsx +0 -15
- package/app/api/tasks/[taskId]/detail/route.tsx +0 -68
- package/app/api/tasks/[taskId]/route.tsx +0 -259
- package/app/api/tasks/bulk/route.tsx +0 -133
- package/app/api/tasks/route.tsx +0 -36
- package/app/api/workspace/active/route.tsx +0 -39
- package/app/api/workspace/create-team/route.tsx +0 -42
- package/app/api/workspace/kanban-statuses/route.tsx +0 -71
- package/app/api/workspace/members/[memberId]/route.tsx +0 -69
- package/app/api/workspace/route.tsx +0 -24
- package/app/download/page.tsx +0 -170
- package/app/favicon.ico +0 -0
- package/app/generated/prisma/client.d.ts +0 -1
- package/app/generated/prisma/client.js +0 -5
- package/app/generated/prisma/default.d.ts +0 -1
- package/app/generated/prisma/default.js +0 -5
- package/app/generated/prisma/edge.d.ts +0 -1
- package/app/generated/prisma/edge.js +0 -497
- package/app/generated/prisma/index-browser.js +0 -523
- package/app/generated/prisma/index.d.ts +0 -46376
- package/app/generated/prisma/index.js +0 -497
- package/app/generated/prisma/package.json +0 -144
- package/app/generated/prisma/query_compiler_fast_bg.js +0 -2
- package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +0 -2
- package/app/generated/prisma/runtime/client.d.ts +0 -3386
- package/app/generated/prisma/runtime/client.js +0 -86
- package/app/generated/prisma/runtime/index-browser.d.ts +0 -90
- package/app/generated/prisma/runtime/index-browser.js +0 -6
- package/app/generated/prisma/runtime/wasm-compiler-edge.js +0 -76
- package/app/generated/prisma/schema.prisma +0 -456
- package/app/generated/prisma/wasm-edge-light-loader.mjs +0 -5
- package/app/generated/prisma/wasm-worker-loader.mjs +0 -5
- package/app/globals.css +0 -54
- package/app/invite/[token]/page.tsx +0 -52
- package/app/layout.tsx +0 -90
- package/app/mcp/route.tsx +0 -430
- package/app/opengraph-image.tsx +0 -120
- package/app/page.tsx +0 -398
- package/app/privacy/page.tsx +0 -69
- package/app/robots.tsx +0 -25
- package/app/sitemap.tsx +0 -36
- package/app/terms/page.tsx +0 -69
- package/app/upgrade/page.tsx +0 -75
- package/auth.config.ts +0 -33
- package/auth.ts +0 -79
- package/components/auth/login-form.tsx +0 -302
- package/components/auth/password-checklist.tsx +0 -31
- package/components/auth/password-input.tsx +0 -36
- package/components/auth/switch-account-button.test.tsx +0 -22
- package/components/auth/switch-account-button.tsx +0 -19
- package/components/auth/two-factor-input.tsx +0 -116
- package/components/billing/billing-dashboard.tsx +0 -265
- package/components/billing/card-form.tsx +0 -210
- package/components/billing/claim-account-form.tsx +0 -99
- package/components/branding/app-logo.test.tsx +0 -20
- package/components/branding/app-logo.tsx +0 -25
- package/components/calendar/calendar-agenda.tsx +0 -150
- package/components/calendar/calendar-drag.test.tsx +0 -177
- package/components/calendar/calendar-grid.tsx +0 -357
- package/components/calendar/calendar-hooks.test.tsx +0 -27
- package/components/calendar/calendar-hooks.ts +0 -351
- package/components/calendar/calendar-toolbar.test.tsx +0 -68
- package/components/calendar/calendar-toolbar.tsx +0 -291
- package/components/calendar/calendar-types.ts +0 -148
- package/components/calendar/calendar-view.test.tsx +0 -295
- package/components/calendar/calendar-view.tsx +0 -307
- package/components/calendar/day-detail-popover.tsx +0 -174
- package/components/calendar/task-chip.tsx +0 -86
- package/components/command/command-palette.test.tsx +0 -33
- package/components/command/command-palette.tsx +0 -310
- package/components/download-cta.tsx +0 -87
- package/components/feedback/feedback-popup.tsx +0 -207
- package/components/graph/graph-draw.ts +0 -337
- package/components/graph/graph-overlays.tsx +0 -160
- package/components/graph/graph-page.test.tsx +0 -131
- package/components/graph/graph-page.tsx +0 -263
- package/components/graph/graph-types.ts +0 -47
- package/components/graph/graph-view.tsx +0 -322
- package/components/guide/guide-view.tsx +0 -522
- package/components/kanban/kanban-board.test.tsx +0 -128
- package/components/kanban/kanban-board.tsx +0 -361
- package/components/kanban/kanban-card-menu.tsx +0 -102
- package/components/kanban/kanban-card.tsx +0 -227
- package/components/kanban/kanban-column.tsx +0 -49
- package/components/kanban/kanban-status-context.tsx +0 -28
- package/components/landing/calendar-sandbox.test.tsx +0 -15
- package/components/landing/calendar-sandbox.tsx +0 -107
- package/components/landing/graph-sandbox.test.tsx +0 -27
- package/components/landing/graph-sandbox.tsx +0 -80
- package/components/landing/kanban-sandbox.test.tsx +0 -24
- package/components/landing/kanban-sandbox.tsx +0 -101
- package/components/landing/landing-showcase.test.tsx +0 -21
- package/components/landing/landing-showcase.tsx +0 -54
- package/components/landing/list-sandbox.tsx +0 -86
- package/components/landing/mock-workspace.ts +0 -168
- package/components/landing/notes-sandbox.test.tsx +0 -14
- package/components/landing/notes-sandbox.tsx +0 -88
- package/components/layout/app-shell.tsx +0 -83
- package/components/layout/backup-scheduler.tsx +0 -122
- package/components/layout/bottom-nav.tsx +0 -43
- package/components/layout/icon-bar.test.tsx +0 -29
- package/components/layout/icon-bar.tsx +0 -118
- package/components/layout/mobile-top-bar.tsx +0 -68
- package/components/layout/notes-panel-folder.tsx +0 -127
- package/components/layout/notes-panel-note-item.tsx +0 -140
- package/components/layout/notes-panel-task-tab.tsx +0 -63
- package/components/layout/notes-panel-types.ts +0 -44
- package/components/layout/notes-panel.tsx +0 -476
- package/components/layout/notification-bell.tsx +0 -251
- package/components/layout/paywall-screen.tsx +0 -41
- package/components/layout/pro-banner.tsx +0 -76
- package/components/layout/sw-register.tsx +0 -27
- package/components/layout/workspace-switcher.tsx +0 -90
- package/components/notes/mobile-bottom-sheet.tsx +0 -99
- package/components/notes/note-editor-context-menu.tsx +0 -47
- package/components/notes/note-editor-dom.ts +0 -33
- package/components/notes/note-editor-dropdowns.tsx +0 -484
- package/components/notes/note-editor-hooks.ts +0 -692
- package/components/notes/note-editor-keyboard.ts +0 -305
- package/components/notes/note-editor-overlay.tsx +0 -90
- package/components/notes/note-editor.test.tsx +0 -372
- package/components/notes/note-editor.tsx +0 -662
- package/components/notes/note-preview-pane.tsx +0 -156
- package/components/notes/note-tabs.tsx +0 -120
- package/components/notes/note-types.tsx +0 -157
- package/components/settings/accept-invite.tsx +0 -108
- package/components/settings/agent-token-settings.tsx +0 -369
- package/components/settings/backup-restore-settings.test.tsx +0 -25
- package/components/settings/backup-restore-settings.tsx +0 -327
- package/components/settings/calendar-feeds-settings.tsx +0 -489
- package/components/settings/calendar-general-settings.tsx +0 -174
- package/components/settings/confirm-danger-action.test.tsx +0 -215
- package/components/settings/confirm-danger-action.tsx +0 -65
- package/components/settings/security-settings.tsx +0 -252
- package/components/settings/settings-guidance.test.tsx +0 -98
- package/components/settings/team-settings.tsx +0 -319
- package/components/settings/two-factor-auth.tsx +0 -296
- package/components/settings/workspace-settings-client.tsx +0 -363
- package/components/settings/workspace-settings-form.tsx +0 -73
- package/components/sync/conflict-viewer.tsx +0 -247
- package/components/sync/sync-indicator.tsx +0 -171
- package/components/tasks/snippet-thread.tsx +0 -119
- package/components/tasks/status-dot.tsx +0 -47
- package/components/tasks/task-badge.tsx +0 -43
- package/components/tasks/task-detail.test.tsx +0 -187
- package/components/tasks/task-detail.tsx +0 -458
- package/components/tasks/task-list-filters.test.tsx +0 -75
- package/components/tasks/task-list-filters.tsx +0 -163
- package/components/tasks/task-list-types.ts +0 -20
- package/components/tasks/task-list.test.tsx +0 -175
- package/components/tasks/task-list.tsx +0 -481
- package/components/tasks/task-row.tsx +0 -85
- package/components/tasks/task-table-row.tsx +0 -259
- package/components/ui/skeleton.tsx +0 -3
- package/components/ui/toast.test.tsx +0 -42
- package/components/ui/toast.tsx +0 -70
- package/electron/main.ts +0 -251
- package/electron/preload.ts +0 -56
- package/instrumentation.tsx +0 -23
- package/lib/api-error.ts +0 -50
- package/lib/backup/backup-runner.test.ts +0 -32
- package/lib/backup/backup-runner.ts +0 -19
- package/lib/backup/backup-schedule.test.ts +0 -23
- package/lib/backup/backup-schedule.ts +0 -55
- package/lib/backup/backup-settings.test.ts +0 -30
- package/lib/backup/backup-settings.ts +0 -27
- package/lib/backup/export-notes-zip.test.ts +0 -26
- package/lib/backup/export-notes-zip.ts +0 -82
- package/lib/backup/export-workspace-backup.test.ts +0 -17
- package/lib/backup/export-workspace-backup.ts +0 -77
- package/lib/backup/restore-workspace-from-export.test.ts +0 -18
- package/lib/backup/restore-workspace-from-export.ts +0 -183
- package/lib/backup/types.ts +0 -14
- package/lib/brand-icons.ts +0 -1
- package/lib/calendar-feed-crypto.ts +0 -38
- package/lib/calendar-feed.ts +0 -239
- package/lib/client/online-status.ts +0 -47
- package/lib/conflict-resolver.test.ts +0 -57
- package/lib/conflict-resolver.ts +0 -240
- package/lib/db-init.ts +0 -79
- package/lib/email.ts +0 -159
- package/lib/encryption.test.ts +0 -41
- package/lib/encryption.ts +0 -98
- package/lib/extract-snippet.test.ts +0 -123
- package/lib/extract-snippet.ts +0 -69
- package/lib/kanban-status.ts +0 -55
- package/lib/license.ts +0 -21
- package/lib/limits.ts +0 -31
- package/lib/mcp-auth.test.ts +0 -58
- package/lib/mcp-auth.ts +0 -65
- package/lib/mcp-contract.test.ts +0 -25
- package/lib/mcp-contract.ts +0 -210
- package/lib/mcp-handler.ts +0 -31
- package/lib/mcp-url.test.ts +0 -12
- package/lib/mcp-url.ts +0 -7
- package/lib/mentions.test.ts +0 -45
- package/lib/mentions.ts +0 -73
- package/lib/note-crypto.ts +0 -108
- package/lib/note-sync.ts +0 -201
- package/lib/note-title.ts +0 -93
- package/lib/prisma.ts +0 -193
- package/lib/pro-flush.ts +0 -292
- package/lib/rate-limit.ts +0 -57
- package/lib/stripe.ts +0 -38
- package/lib/sync-worker.ts +0 -388
- package/lib/task-parser.test.ts +0 -91
- package/lib/task-parser.ts +0 -81
- package/lib/task-utils.ts +0 -52
- package/lib/use-is-electron.ts +0 -19
- package/lib/use-is-mobile.ts +0 -22
- package/lib/validation/calendar-feed.ts +0 -31
- package/lib/validation/note.ts +0 -27
- package/lib/validation/task.ts +0 -26
- package/lib/view-preferences.test.ts +0 -54
- package/lib/view-preferences.ts +0 -28
- package/lib/workspace.ts +0 -66
- package/next.config.ts +0 -21
- package/postcss.config.mjs +0 -7
- package/prisma/migrations/20260519021916_init/migration.sql +0 -388
- package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +0 -8
- package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +0 -2
- package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +0 -12
- package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +0 -3
- package/prisma/migrations/20260529030000_add_folders/migration.sql +0 -17
- package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +0 -31
- package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +0 -5
- package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +0 -14
- package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +0 -26
- package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +0 -23
- package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +0 -43
- package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +0 -2
- package/prisma/migrations/20260611050000_add_task_priority/migration.sql +0 -14
- package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +0 -2
- package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +0 -25
- package/prisma/migrations/20260614160000_add_feedback/migration.sql +0 -20
- package/prisma/migrations/20260614210000_add_2fa/migration.sql +0 -4
- package/prisma/migrations/migration_lock.toml +0 -3
- package/prisma/schema.prisma +0 -457
- package/public/Logo_icon.svg +0 -1
- package/public/file.svg +0 -1
- package/public/globe.svg +0 -1
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +0 -4
- package/public/icon_dark.svg +0 -1
- package/public/knotpad_icon.svg +0 -1
- package/public/knotpad_logo_full.svg +0 -1
- package/public/manifest.json +0 -14
- package/public/next.svg +0 -1
- package/public/sw.js +0 -137
- package/public/vercel.svg +0 -1
- package/public/window.svg +0 -1
- package/tsconfig.json +0 -35
|
@@ -1,193 +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 { PasswordChecklist } from "@/components/auth/password-checklist";
|
|
10
|
-
|
|
11
|
-
export default function RegisterPage() {
|
|
12
|
-
return (
|
|
13
|
-
<Suspense fallback={<RegisterSkeleton />}>
|
|
14
|
-
<RegisterForm />
|
|
15
|
-
</Suspense>
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function RegisterSkeleton() {
|
|
20
|
-
return (
|
|
21
|
-
<div className="w-full max-w-sm space-y-6">
|
|
22
|
-
<div className="h-8 w-48 animate-pulse rounded bg-zinc-800" />
|
|
23
|
-
<div className="h-4 w-64 animate-pulse rounded bg-zinc-800" />
|
|
24
|
-
<div className="space-y-4 pt-4">
|
|
25
|
-
<div className="h-10 animate-pulse rounded bg-zinc-800" />
|
|
26
|
-
<div className="h-10 animate-pulse rounded bg-zinc-800" />
|
|
27
|
-
<div className="h-10 animate-pulse rounded bg-zinc-800" />
|
|
28
|
-
<div className="h-10 animate-pulse rounded bg-zinc-800" />
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function RegisterForm() {
|
|
35
|
-
const router = useRouter();
|
|
36
|
-
const searchParams = useSearchParams();
|
|
37
|
-
const [error, setError] = useState("");
|
|
38
|
-
const [loading, setLoading] = useState(false);
|
|
39
|
-
const [password, setPassword] = useState("");
|
|
40
|
-
const [confirm, setConfirm] = useState("");
|
|
41
|
-
|
|
42
|
-
const rawNext = searchParams.get("next") ?? "";
|
|
43
|
-
const redirectTo =
|
|
44
|
-
rawNext.startsWith("/") && !rawNext.startsWith("//") ? rawNext : "/notes";
|
|
45
|
-
|
|
46
|
-
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
47
|
-
e.preventDefault();
|
|
48
|
-
|
|
49
|
-
if (password !== confirm) {
|
|
50
|
-
setError("Passwords don't match");
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
setLoading(true);
|
|
55
|
-
setError("");
|
|
56
|
-
|
|
57
|
-
const form = e.currentTarget;
|
|
58
|
-
const name = (form.elements.namedItem("name") as HTMLInputElement).value;
|
|
59
|
-
const email = (form.elements.namedItem("email") as HTMLInputElement).value;
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const res = await fetch("/api/register", {
|
|
63
|
-
method: "POST",
|
|
64
|
-
headers: { "Content-Type": "application/json" },
|
|
65
|
-
body: JSON.stringify({ name, email, password }),
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
if (!res.ok) {
|
|
69
|
-
const data = await res.json();
|
|
70
|
-
setError(data.error ?? "Registration failed");
|
|
71
|
-
setLoading(false);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const result = await signIn("credentials", { email, password, redirect: false });
|
|
76
|
-
if (result?.error) {
|
|
77
|
-
setError("Account created but sign-in failed. Please sign in manually.");
|
|
78
|
-
setLoading(false);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
router.push(redirectTo);
|
|
83
|
-
router.refresh();
|
|
84
|
-
} catch {
|
|
85
|
-
setError("Something went wrong. Please try again.");
|
|
86
|
-
setLoading(false);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return (
|
|
91
|
-
<div className="w-full max-w-sm space-y-6">
|
|
92
|
-
<div className="flex justify-end">
|
|
93
|
-
<Link href="/" className="text-sm text-zinc-500 hover:text-zinc-300 transition-colors">
|
|
94
|
-
back to home
|
|
95
|
-
</Link>
|
|
96
|
-
</div>
|
|
97
|
-
<div>
|
|
98
|
-
<AppLogo className="mb-4 h-8 w-auto" />
|
|
99
|
-
<h1 className="text-2xl font-semibold tracking-tight">Create your account</h1>
|
|
100
|
-
<p className="mt-1 text-sm text-zinc-400">Start writing. Tasks will follow.</p>
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
104
|
-
<div className="space-y-1">
|
|
105
|
-
<label htmlFor="name" className="text-sm font-medium text-zinc-300">
|
|
106
|
-
Name
|
|
107
|
-
</label>
|
|
108
|
-
<input
|
|
109
|
-
id="name"
|
|
110
|
-
name="name"
|
|
111
|
-
type="text"
|
|
112
|
-
required
|
|
113
|
-
autoComplete="name"
|
|
114
|
-
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"
|
|
115
|
-
placeholder="Your name"
|
|
116
|
-
/>
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
<div className="space-y-1">
|
|
120
|
-
<label htmlFor="email" className="text-sm font-medium text-zinc-300">
|
|
121
|
-
Email
|
|
122
|
-
</label>
|
|
123
|
-
<input
|
|
124
|
-
id="email"
|
|
125
|
-
name="email"
|
|
126
|
-
type="email"
|
|
127
|
-
required
|
|
128
|
-
autoComplete="email"
|
|
129
|
-
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"
|
|
130
|
-
placeholder="you@example.com"
|
|
131
|
-
/>
|
|
132
|
-
</div>
|
|
133
|
-
|
|
134
|
-
<div className="space-y-1">
|
|
135
|
-
<label htmlFor="password" className="text-sm font-medium text-zinc-300">
|
|
136
|
-
Password
|
|
137
|
-
</label>
|
|
138
|
-
<PasswordInput
|
|
139
|
-
id="password"
|
|
140
|
-
name="password"
|
|
141
|
-
required
|
|
142
|
-
minLength={8}
|
|
143
|
-
autoComplete="new-password"
|
|
144
|
-
value={password}
|
|
145
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
146
|
-
placeholder="••••••••"
|
|
147
|
-
/>
|
|
148
|
-
{password.length > 0 && (
|
|
149
|
-
<div className="pt-2">
|
|
150
|
-
<PasswordChecklist password={password} />
|
|
151
|
-
</div>
|
|
152
|
-
)}
|
|
153
|
-
</div>
|
|
154
|
-
|
|
155
|
-
<div className="space-y-1">
|
|
156
|
-
<label htmlFor="confirm" className="text-sm font-medium text-zinc-300">
|
|
157
|
-
Confirm password
|
|
158
|
-
</label>
|
|
159
|
-
<PasswordInput
|
|
160
|
-
id="confirm"
|
|
161
|
-
name="confirm"
|
|
162
|
-
required
|
|
163
|
-
minLength={8}
|
|
164
|
-
autoComplete="new-password"
|
|
165
|
-
value={confirm}
|
|
166
|
-
onChange={(e) => setConfirm(e.target.value)}
|
|
167
|
-
placeholder="••••••••"
|
|
168
|
-
/>
|
|
169
|
-
{confirm.length > 0 && password !== confirm && (
|
|
170
|
-
<p className="text-xs text-red-400 pt-1">Passwords don't match</p>
|
|
171
|
-
)}
|
|
172
|
-
</div>
|
|
173
|
-
|
|
174
|
-
{error && <p className="text-sm text-red-400">{error}</p>}
|
|
175
|
-
|
|
176
|
-
<button
|
|
177
|
-
type="submit"
|
|
178
|
-
disabled={loading}
|
|
179
|
-
className="w-full rounded-md bg-zinc-100 px-4 py-2 text-sm font-medium text-zinc-900 hover:bg-zinc-200 disabled:opacity-50"
|
|
180
|
-
>
|
|
181
|
-
{loading ? "Creating account…" : "Create account"}
|
|
182
|
-
</button>
|
|
183
|
-
</form>
|
|
184
|
-
|
|
185
|
-
<p className="text-center text-sm text-zinc-500">
|
|
186
|
-
Already have an account?{" "}
|
|
187
|
-
<Link href="/login" className="text-zinc-300 underline underline-offset-2 hover:text-white">
|
|
188
|
-
Sign in
|
|
189
|
-
</Link>
|
|
190
|
-
</p>
|
|
191
|
-
</div>
|
|
192
|
-
);
|
|
193
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, Suspense } from "react";
|
|
4
|
-
import { useRouter, useSearchParams } from "next/navigation";
|
|
5
|
-
import Link from "next/link";
|
|
6
|
-
import { PasswordInput } from "@/components/auth/password-input";
|
|
7
|
-
import { PasswordChecklist } from "@/components/auth/password-checklist";
|
|
8
|
-
|
|
9
|
-
export default function ResetPasswordPage() {
|
|
10
|
-
return (
|
|
11
|
-
<Suspense fallback={<ResetSkeleton />}>
|
|
12
|
-
<ResetForm />
|
|
13
|
-
</Suspense>
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function ResetSkeleton() {
|
|
18
|
-
return (
|
|
19
|
-
<div className="w-full max-w-sm space-y-6">
|
|
20
|
-
<div className="h-8 w-48 animate-pulse rounded bg-zinc-800" />
|
|
21
|
-
<div className="h-4 w-64 animate-pulse rounded bg-zinc-800" />
|
|
22
|
-
<div className="space-y-4 pt-4">
|
|
23
|
-
<div className="h-10 animate-pulse rounded bg-zinc-800" />
|
|
24
|
-
<div className="h-10 animate-pulse rounded bg-zinc-800" />
|
|
25
|
-
<div className="h-10 animate-pulse rounded bg-zinc-800" />
|
|
26
|
-
</div>
|
|
27
|
-
</div>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function ResetForm() {
|
|
32
|
-
const router = useRouter();
|
|
33
|
-
const searchParams = useSearchParams();
|
|
34
|
-
const token = searchParams.get("token") ?? "";
|
|
35
|
-
|
|
36
|
-
const [password, setPassword] = useState("");
|
|
37
|
-
const [confirm, setConfirm] = useState("");
|
|
38
|
-
const [loading, setLoading] = useState(false);
|
|
39
|
-
const [error, setError] = useState("");
|
|
40
|
-
|
|
41
|
-
if (!token) {
|
|
42
|
-
return (
|
|
43
|
-
<div className="w-full max-w-sm text-center space-y-3">
|
|
44
|
-
<h1 className="text-2xl font-semibold tracking-tight">Invalid link</h1>
|
|
45
|
-
<p className="text-sm text-zinc-400">This reset link is missing a token.</p>
|
|
46
|
-
<Link href="/forgot-password" className="text-sm text-zinc-300 underline">
|
|
47
|
-
Request a new one
|
|
48
|
-
</Link>
|
|
49
|
-
</div>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function handleSubmit(e: React.FormEvent) {
|
|
54
|
-
e.preventDefault();
|
|
55
|
-
if (password !== confirm) {
|
|
56
|
-
setError("Passwords don't match");
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
setLoading(true);
|
|
60
|
-
setError("");
|
|
61
|
-
try {
|
|
62
|
-
const res = await fetch("/api/auth/reset-password", {
|
|
63
|
-
method: "POST",
|
|
64
|
-
headers: { "Content-Type": "application/json" },
|
|
65
|
-
body: JSON.stringify({ token, password }),
|
|
66
|
-
});
|
|
67
|
-
if (res.ok) {
|
|
68
|
-
router.push("/login?reset=1");
|
|
69
|
-
} else {
|
|
70
|
-
const d = await res.json();
|
|
71
|
-
setError(d.error ?? "Reset failed. The link may have expired.");
|
|
72
|
-
}
|
|
73
|
-
} catch {
|
|
74
|
-
setError("Something went wrong. Please try again.");
|
|
75
|
-
} finally {
|
|
76
|
-
setLoading(false);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<div className="w-full max-w-sm space-y-6">
|
|
82
|
-
<div>
|
|
83
|
-
<h1 className="text-2xl font-semibold tracking-tight">Set new password</h1>
|
|
84
|
-
<p className="mt-1 text-sm text-zinc-400">Choose a password with at least 8 characters.</p>
|
|
85
|
-
</div>
|
|
86
|
-
|
|
87
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
88
|
-
<div className="space-y-1">
|
|
89
|
-
<label htmlFor="password" className="text-sm font-medium text-zinc-300">
|
|
90
|
-
New password
|
|
91
|
-
</label>
|
|
92
|
-
<PasswordInput
|
|
93
|
-
id="password"
|
|
94
|
-
required
|
|
95
|
-
minLength={8}
|
|
96
|
-
value={password}
|
|
97
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
98
|
-
autoComplete="new-password"
|
|
99
|
-
placeholder="••••••••"
|
|
100
|
-
/>
|
|
101
|
-
{password.length > 0 && (
|
|
102
|
-
<div className="pt-2">
|
|
103
|
-
<PasswordChecklist password={password} />
|
|
104
|
-
</div>
|
|
105
|
-
)}
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
<div className="space-y-1">
|
|
109
|
-
<label htmlFor="confirm" className="text-sm font-medium text-zinc-300">
|
|
110
|
-
Confirm password
|
|
111
|
-
</label>
|
|
112
|
-
<PasswordInput
|
|
113
|
-
id="confirm"
|
|
114
|
-
required
|
|
115
|
-
minLength={8}
|
|
116
|
-
value={confirm}
|
|
117
|
-
onChange={(e) => setConfirm(e.target.value)}
|
|
118
|
-
autoComplete="new-password"
|
|
119
|
-
placeholder="••••••••"
|
|
120
|
-
/>
|
|
121
|
-
{confirm.length > 0 && password !== confirm && (
|
|
122
|
-
<p className="text-xs text-red-400 pt-1">Passwords don't match</p>
|
|
123
|
-
)}
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
{error && <p className="text-sm text-red-400">{error}</p>}
|
|
127
|
-
|
|
128
|
-
<button
|
|
129
|
-
type="submit"
|
|
130
|
-
disabled={loading}
|
|
131
|
-
className="w-full rounded-md bg-zinc-100 px-4 py-2 text-sm font-medium text-zinc-900 hover:bg-zinc-200 disabled:opacity-50"
|
|
132
|
-
>
|
|
133
|
-
{loading ? "Saving…" : "Set new password"}
|
|
134
|
-
</button>
|
|
135
|
-
</form>
|
|
136
|
-
</div>
|
|
137
|
-
);
|
|
138
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import bcrypt from "bcryptjs";
|
|
3
|
-
import { auth } from "@/auth";
|
|
4
|
-
import { prisma, getCloudPrisma } from "@/lib/prisma";
|
|
5
|
-
import { rateLimit, getClientIp } from "@/lib/rate-limit";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* POST /api/account/claim
|
|
9
|
-
* Body: { name, email, password }
|
|
10
|
-
*
|
|
11
|
-
* Converts the current GUEST (local, no-password `*@local.brief`) into a real
|
|
12
|
-
* account WITHOUT orphaning their notes:
|
|
13
|
-
* 1. Attach real name/email/passwordHash to the existing local user (same id,
|
|
14
|
-
* same workspace → their notes stay put).
|
|
15
|
-
* 2. Mirror the identity (User + each Workspace + WorkspaceMember + that
|
|
16
|
-
* workspace's KanbanStatuses) into Neon BY ID, because identity is
|
|
17
|
-
* cloud-scoped (PrismaAdapter(cloud)) and `/api/billing/migrate` is keyed by
|
|
18
|
-
* workspaceId — both need the records to exist in cloud.
|
|
19
|
-
*
|
|
20
|
-
* After this, the client signs in with the new credentials and proceeds to
|
|
21
|
-
* billing (setup-intent → subscribe); the Stripe webhook flips the plan flags
|
|
22
|
-
* and triggers the data migration. This route does NOT touch Stripe or plan
|
|
23
|
-
* flags — it only establishes the real, cloud-resolvable identity.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
const AUTH_MAX = 10;
|
|
27
|
-
const AUTH_WINDOW_MS = 60 * 60_000;
|
|
28
|
-
|
|
29
|
-
export async function POST(req: NextRequest) {
|
|
30
|
-
const ip = getClientIp(req);
|
|
31
|
-
const rl = rateLimit(`claim:${ip}`, AUTH_MAX, AUTH_WINDOW_MS);
|
|
32
|
-
if (rl.limited) {
|
|
33
|
-
return NextResponse.json(
|
|
34
|
-
{ error: "Too many attempts. Try again later." },
|
|
35
|
-
{ status: 429, headers: { "Retry-After": String(rl.retryAfter) } }
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const session = await auth();
|
|
40
|
-
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
41
|
-
|
|
42
|
-
const cloud = getCloudPrisma();
|
|
43
|
-
if (!cloud) {
|
|
44
|
-
return NextResponse.json(
|
|
45
|
-
{ error: "Cloud is not configured — upgrading requires a cloud connection." },
|
|
46
|
-
{ status: 503 }
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// The signed-in user must be an unclaimed guest.
|
|
51
|
-
const me = await prisma.user.findUnique({ where: { id: session.user.id } });
|
|
52
|
-
if (!me || !me.email?.endsWith("@local.brief") || me.passwordHash) {
|
|
53
|
-
return NextResponse.json(
|
|
54
|
-
{ error: "This account can't be upgraded (already a real account)." },
|
|
55
|
-
{ status: 400 }
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const { name, email, password } = await req.json().catch(() => ({}));
|
|
60
|
-
if (!name || !email || !password) {
|
|
61
|
-
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
|
|
62
|
-
}
|
|
63
|
-
if (typeof password !== "string" || password.length < 8) {
|
|
64
|
-
return NextResponse.json({ error: "Password must be at least 8 characters" }, { status: 400 });
|
|
65
|
-
}
|
|
66
|
-
if (typeof email !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
67
|
-
return NextResponse.json({ error: "Invalid email address" }, { status: 400 });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Email must be free in BOTH databases (auth checks cloud-first, but the local
|
|
71
|
-
// DB also holds users in fully-local mode).
|
|
72
|
-
for (const db of [prisma, cloud]) {
|
|
73
|
-
const taken = await db.user.findUnique({ where: { email } });
|
|
74
|
-
if (taken) return NextResponse.json({ error: "Email already in use" }, { status: 409 });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const passwordHash = await bcrypt.hash(password, 12);
|
|
78
|
-
|
|
79
|
-
// 1) Promote the local user in place (keeps id + workspace + notes).
|
|
80
|
-
await prisma.user.update({
|
|
81
|
-
where: { id: me.id },
|
|
82
|
-
data: { name, email, passwordHash },
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// 2) Mirror identity into Neon by id so cloud login + migrate resolve.
|
|
86
|
-
const memberships = await prisma.workspaceMember.findMany({
|
|
87
|
-
where: { userId: me.id, revokedAt: null },
|
|
88
|
-
include: { workspace: true },
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
await cloud.user.upsert({
|
|
92
|
-
where: { id: me.id },
|
|
93
|
-
create: { id: me.id, name, email, passwordHash, role: me.role, createdAt: me.createdAt },
|
|
94
|
-
update: { name, email, passwordHash },
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
for (const m of memberships) {
|
|
98
|
-
const ws = m.workspace;
|
|
99
|
-
await cloud.workspace.upsert({
|
|
100
|
-
where: { id: ws.id },
|
|
101
|
-
create: {
|
|
102
|
-
id: ws.id,
|
|
103
|
-
name: ws.name,
|
|
104
|
-
slug: ws.slug,
|
|
105
|
-
type: ws.type,
|
|
106
|
-
planType: ws.planType,
|
|
107
|
-
licenseType: ws.licenseType,
|
|
108
|
-
isCloud: ws.isCloud,
|
|
109
|
-
isPro: ws.isPro,
|
|
110
|
-
seatCount: ws.seatCount,
|
|
111
|
-
encryptionSalt: ws.encryptionSalt,
|
|
112
|
-
createdAt: ws.createdAt,
|
|
113
|
-
},
|
|
114
|
-
update: { name: ws.name, slug: ws.slug, encryptionSalt: ws.encryptionSalt },
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
await cloud.workspaceMember.upsert({
|
|
118
|
-
where: { userId_workspaceId: { userId: me.id, workspaceId: ws.id } },
|
|
119
|
-
create: { id: m.id, userId: me.id, workspaceId: ws.id, role: m.role, joinedAt: m.joinedAt },
|
|
120
|
-
update: { role: m.role },
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// Mirror the workspace's kanban statuses so the board looks identical in cloud.
|
|
124
|
-
const statuses = await prisma.kanbanStatus.findMany({ where: { workspaceId: ws.id } });
|
|
125
|
-
for (const s of statuses) {
|
|
126
|
-
await cloud.kanbanStatus.upsert({
|
|
127
|
-
where: { workspaceId_key: { workspaceId: ws.id, key: s.key } },
|
|
128
|
-
create: { workspaceId: ws.id, key: s.key, label: s.label, color: s.color, order: s.order, isVisible: s.isVisible },
|
|
129
|
-
update: { label: s.label, color: s.color, order: s.order, isVisible: s.isVisible },
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return NextResponse.json({ ok: true, email });
|
|
135
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { prisma } from "@/lib/prisma";
|
|
3
|
-
import { encryptContent } from "@/lib/note-crypto";
|
|
4
|
-
import { isEncrypted } from "@/lib/encryption";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* POST /api/admin/backfill-encryption
|
|
8
|
-
* Header: Authorization: Bearer <ADMIN_SECRET>
|
|
9
|
-
*
|
|
10
|
-
* One-off migration: encrypts any note.content rows still stored as plaintext
|
|
11
|
-
* (e.g. created while encryption was disabled). Idempotent — already-encrypted
|
|
12
|
-
* rows are skipped via isEncrypted(), so it is safe to re-run.
|
|
13
|
-
*/
|
|
14
|
-
export async function POST(req: NextRequest) {
|
|
15
|
-
const secret = process.env.ADMIN_SECRET;
|
|
16
|
-
if (!secret) return NextResponse.json({ error: "Not configured" }, { status: 501 });
|
|
17
|
-
|
|
18
|
-
const auth = req.headers.get("authorization");
|
|
19
|
-
if (auth !== `Bearer ${secret}`) {
|
|
20
|
-
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const notes = await prisma.note.findMany({ select: { id: true, content: true, workspaceId: true } });
|
|
24
|
-
|
|
25
|
-
let encrypted = 0;
|
|
26
|
-
let skipped = 0;
|
|
27
|
-
for (const note of notes) {
|
|
28
|
-
if (isEncrypted(note.content)) {
|
|
29
|
-
skipped++;
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
const ciphertext = await encryptContent(note.content, note.workspaceId);
|
|
33
|
-
// version stays put (storage-format change, not a content edit); @updatedAt
|
|
34
|
-
// will refresh, which simply makes sync push the now-encrypted row once.
|
|
35
|
-
await prisma.note.update({
|
|
36
|
-
where: { id: note.id },
|
|
37
|
-
data: { content: ciphertext },
|
|
38
|
-
});
|
|
39
|
-
encrypted++;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return NextResponse.json({ ok: true, total: notes.length, encrypted, skipped });
|
|
43
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { prisma } from "@/lib/prisma";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* POST /api/admin/license
|
|
6
|
-
* Body: { workspaceId: string, licenseType: "STANDARD" | "COMPLIMENTARY" }
|
|
7
|
-
* Header: Authorization: Bearer <ADMIN_SECRET>
|
|
8
|
-
*
|
|
9
|
-
* Sets the license type on a workspace.
|
|
10
|
-
* COMPLIMENTARY workspaces get isPro + isCloud for free — no Stripe subscription needed.
|
|
11
|
-
*/
|
|
12
|
-
export async function POST(req: NextRequest) {
|
|
13
|
-
const secret = process.env.ADMIN_SECRET;
|
|
14
|
-
if (!secret) return NextResponse.json({ error: "Not configured" }, { status: 501 });
|
|
15
|
-
|
|
16
|
-
const auth = req.headers.get("authorization");
|
|
17
|
-
if (auth !== `Bearer ${secret}`) {
|
|
18
|
-
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const { workspaceId, licenseType } = await req.json().catch(() => ({}));
|
|
22
|
-
|
|
23
|
-
if (!workspaceId) return NextResponse.json({ error: "workspaceId required" }, { status: 400 });
|
|
24
|
-
if (!["STANDARD", "COMPLIMENTARY"].includes(licenseType)) {
|
|
25
|
-
return NextResponse.json({ error: "licenseType must be STANDARD or COMPLIMENTARY" }, { status: 400 });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const isComplimentary = licenseType === "COMPLIMENTARY";
|
|
29
|
-
|
|
30
|
-
const workspace = await prisma.workspace.update({
|
|
31
|
-
where: { id: workspaceId },
|
|
32
|
-
data: {
|
|
33
|
-
licenseType,
|
|
34
|
-
isPro: isComplimentary ? true : undefined,
|
|
35
|
-
isCloud: isComplimentary ? true : undefined,
|
|
36
|
-
// Downgrading to STANDARD: let Stripe state govern isPro/isCloud (don't touch them here)
|
|
37
|
-
},
|
|
38
|
-
select: { id: true, name: true, licenseType: true, isPro: true, isCloud: true, planType: true },
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
return NextResponse.json(workspace);
|
|
42
|
-
}
|