@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,148 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { auth } from "@/auth";
|
|
3
|
-
import { prisma } from "@/lib/prisma";
|
|
4
|
-
import { randomBytes, createHmac } from "crypto";
|
|
5
|
-
|
|
6
|
-
// TOTP implementation using Node.js crypto
|
|
7
|
-
function generateSecret(): string {
|
|
8
|
-
return randomBytes(20).toString("base64url").slice(0, 32);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function generateTOTP(secret: string, window = 0): string {
|
|
12
|
-
const counter = Math.floor(Date.now() / 1000 / 30) + window;
|
|
13
|
-
const counterBuffer = Buffer.alloc(8);
|
|
14
|
-
counterBuffer.writeBigUInt64BE(BigInt(counter), 0);
|
|
15
|
-
|
|
16
|
-
const secretBuffer = Buffer.from(secret, "base64url");
|
|
17
|
-
const hmac = createHmac("sha1", secretBuffer);
|
|
18
|
-
hmac.update(counterBuffer);
|
|
19
|
-
const hash = hmac.digest();
|
|
20
|
-
|
|
21
|
-
const offset = hash[hash.length - 1] & 0x0f;
|
|
22
|
-
const code =
|
|
23
|
-
((hash[offset] & 0x7f) << 24) |
|
|
24
|
-
((hash[offset + 1] & 0xff) << 16) |
|
|
25
|
-
((hash[offset + 2] & 0xff) << 8) |
|
|
26
|
-
(hash[offset + 3] & 0xff);
|
|
27
|
-
|
|
28
|
-
return (code % 1000000).toString().padStart(6, "0");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function verifyTOTP(secret: string, token: string): boolean {
|
|
32
|
-
// Check current and adjacent time windows (±1 window = ±30 seconds)
|
|
33
|
-
for (let window = -1; window <= 1; window++) {
|
|
34
|
-
if (generateTOTP(secret, window) === token) {
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function generateQRCodeURL(secret: string, email: string, appName: string): string {
|
|
42
|
-
const encodedSecret = secret.replace(/=/g, "");
|
|
43
|
-
const encodedApp = encodeURIComponent(appName);
|
|
44
|
-
const encodedEmail = encodeURIComponent(email);
|
|
45
|
-
return `otpauth://totp/${encodedApp}:${encodedEmail}?secret=${encodedSecret}&issuer=${encodedApp}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// GET: Check if 2FA is enabled for current user
|
|
49
|
-
export async function GET() {
|
|
50
|
-
const session = await auth();
|
|
51
|
-
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
52
|
-
|
|
53
|
-
const user = await prisma.user.findUnique({
|
|
54
|
-
where: { id: session.user.id },
|
|
55
|
-
select: { twoFactorEnabled: true },
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
return NextResponse.json({ enabled: user?.twoFactorEnabled ?? false });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// POST: Enable 2FA - generate secret and return QR code
|
|
62
|
-
export async function POST(req: NextRequest) {
|
|
63
|
-
const session = await auth();
|
|
64
|
-
if (!session?.user?.email) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
65
|
-
|
|
66
|
-
const { action } = await req.json();
|
|
67
|
-
|
|
68
|
-
if (action === "setup") {
|
|
69
|
-
// Generate new secret
|
|
70
|
-
const secret = generateSecret();
|
|
71
|
-
const qrCodeURL = generateQRCodeURL(secret, session.user.email, "Knotpad");
|
|
72
|
-
|
|
73
|
-
// Store secret temporarily (not enabled until verified)
|
|
74
|
-
await prisma.user.update({
|
|
75
|
-
where: { id: session.user.id },
|
|
76
|
-
data: { twoFactorSecret: secret, twoFactorEnabled: false, twoFactorVerified: false },
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
return NextResponse.json({
|
|
80
|
-
secret,
|
|
81
|
-
qrCodeURL,
|
|
82
|
-
manualEntryKey: secret.replace(/=/g, "").slice(0, 32).toUpperCase().replace(/(.{4})/g, "$1 ").trim(),
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (action === "verify") {
|
|
87
|
-
const { code } = await req.json();
|
|
88
|
-
if (!code || !/^\d{6}$/.test(code)) {
|
|
89
|
-
return NextResponse.json({ error: "Invalid code format" }, { status: 400 });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const user = await prisma.user.findUnique({
|
|
93
|
-
where: { id: session.user.id },
|
|
94
|
-
select: { twoFactorSecret: true, twoFactorEnabled: true },
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
if (!user?.twoFactorSecret) {
|
|
98
|
-
return NextResponse.json({ error: "2FA not set up" }, { status: 400 });
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (verifyTOTP(user.twoFactorSecret, code)) {
|
|
102
|
-
await prisma.user.update({
|
|
103
|
-
where: { id: session.user.id },
|
|
104
|
-
data: { twoFactorEnabled: true, twoFactorVerified: true },
|
|
105
|
-
});
|
|
106
|
-
return NextResponse.json({ success: true });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return NextResponse.json({ error: "Invalid code" }, { status: 400 });
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (action === "disable") {
|
|
113
|
-
const { code, password } = await req.json();
|
|
114
|
-
|
|
115
|
-
// Verify password first
|
|
116
|
-
const user = await prisma.user.findUnique({
|
|
117
|
-
where: { id: session.user.id },
|
|
118
|
-
select: { passwordHash: true, twoFactorSecret: true, twoFactorEnabled: true },
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
if (!user?.passwordHash) {
|
|
122
|
-
return NextResponse.json({ error: "Password required" }, { status: 400 });
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Import bcrypt for password verification
|
|
126
|
-
const { default: bcrypt } = await import("bcryptjs");
|
|
127
|
-
const validPassword = await bcrypt.compare(password, user.passwordHash);
|
|
128
|
-
if (!validPassword) {
|
|
129
|
-
return NextResponse.json({ error: "Invalid password" }, { status: 400 });
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Verify 2FA code if enabled
|
|
133
|
-
if (user.twoFactorEnabled && user.twoFactorSecret) {
|
|
134
|
-
if (!code || !verifyTOTP(user.twoFactorSecret, code)) {
|
|
135
|
-
return NextResponse.json({ error: "Invalid 2FA code" }, { status: 400 });
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
await prisma.user.update({
|
|
140
|
-
where: { id: session.user.id },
|
|
141
|
-
data: { twoFactorSecret: null, twoFactorEnabled: false, twoFactorVerified: false },
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
return NextResponse.json({ success: true });
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
|
148
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import bcrypt from "bcryptjs";
|
|
3
|
-
import { auth } from "@/auth";
|
|
4
|
-
import { prisma, getCloudPrisma } from "@/lib/prisma";
|
|
5
|
-
import { rateLimit, getClientIp } from "@/lib/rate-limit";
|
|
6
|
-
|
|
7
|
-
const RL_MAX = 10;
|
|
8
|
-
const RL_WINDOW_MS = 60 * 60_000;
|
|
9
|
-
|
|
10
|
-
export async function POST(req: NextRequest) {
|
|
11
|
-
const session = await auth();
|
|
12
|
-
if (!session?.user?.id) {
|
|
13
|
-
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const ip = getClientIp(req);
|
|
17
|
-
const rl = rateLimit(`change-password:${ip}`, RL_MAX, RL_WINDOW_MS);
|
|
18
|
-
if (rl.limited) {
|
|
19
|
-
return NextResponse.json(
|
|
20
|
-
{ error: "Too many attempts. Try again later." },
|
|
21
|
-
{ status: 429, headers: { "Retry-After": String(rl.retryAfter) } }
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const { currentPassword, newPassword } = await req.json();
|
|
26
|
-
|
|
27
|
-
if (!currentPassword || typeof currentPassword !== "string") {
|
|
28
|
-
return NextResponse.json({ error: "Current password required" }, { status: 400 });
|
|
29
|
-
}
|
|
30
|
-
if (!newPassword || typeof newPassword !== "string" || newPassword.length < 8) {
|
|
31
|
-
return NextResponse.json({ error: "New password must be at least 8 characters" }, { status: 400 });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const db = getCloudPrisma() ?? prisma;
|
|
35
|
-
|
|
36
|
-
const user = await db.user.findUnique({
|
|
37
|
-
where: { id: session.user.id },
|
|
38
|
-
select: { id: true, passwordHash: true },
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
if (!user || !user.passwordHash) {
|
|
42
|
-
return NextResponse.json(
|
|
43
|
-
{ error: "Password change is not available for this account." },
|
|
44
|
-
{ status: 400 }
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const valid = await bcrypt.compare(currentPassword, user.passwordHash);
|
|
49
|
-
if (!valid) {
|
|
50
|
-
return NextResponse.json({ error: "Current password is incorrect" }, { status: 400 });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const passwordHash = await bcrypt.hash(newPassword, 12);
|
|
54
|
-
|
|
55
|
-
await db.user.update({
|
|
56
|
-
where: { id: user.id },
|
|
57
|
-
data: { passwordHash },
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
return NextResponse.json({ ok: true });
|
|
61
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { prisma } from "@/lib/prisma";
|
|
3
|
-
|
|
4
|
-
export async function POST(req: NextRequest) {
|
|
5
|
-
const { email } = await req.json();
|
|
6
|
-
|
|
7
|
-
if (!email) {
|
|
8
|
-
return NextResponse.json({ error: "Email required" }, { status: 400 });
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const user = await prisma.user.findUnique({
|
|
12
|
-
where: { email },
|
|
13
|
-
select: { twoFactorEnabled: true },
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
return NextResponse.json({
|
|
17
|
-
requires2FA: user?.twoFactorEnabled ?? false,
|
|
18
|
-
});
|
|
19
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { prisma, getCloudPrisma } from "@/lib/prisma";
|
|
3
|
-
import { rateLimit, getClientIp } from "@/lib/rate-limit";
|
|
4
|
-
import { sendAuthEmail } from "@/lib/email";
|
|
5
|
-
|
|
6
|
-
const RL_MAX = 5;
|
|
7
|
-
const RL_WINDOW_MS = 60 * 60_000; // 5 attempts per IP per hour
|
|
8
|
-
|
|
9
|
-
export async function POST(req: NextRequest) {
|
|
10
|
-
const ip = getClientIp(req);
|
|
11
|
-
const rl = rateLimit(`forgot-password:${ip}`, RL_MAX, RL_WINDOW_MS);
|
|
12
|
-
if (rl.limited) {
|
|
13
|
-
return NextResponse.json(
|
|
14
|
-
{ error: "Too many attempts. Try again later." },
|
|
15
|
-
{ status: 429, headers: { "Retry-After": String(rl.retryAfter) } }
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const { email } = await req.json();
|
|
20
|
-
if (!email || typeof email !== "string") {
|
|
21
|
-
return NextResponse.json({ error: "Email required" }, { status: 400 });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Look up user in the same DB auth reads from (cloud if available, local otherwise).
|
|
25
|
-
const db = getCloudPrisma() ?? prisma;
|
|
26
|
-
const user = await db.user.findUnique({ where: { email: email.toLowerCase().trim() } });
|
|
27
|
-
|
|
28
|
-
// Always return 200 to prevent email enumeration.
|
|
29
|
-
if (!user || !user.passwordHash) {
|
|
30
|
-
return NextResponse.json({ ok: true });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Invalidate any existing unused tokens for this user.
|
|
34
|
-
await db.passwordResetToken.updateMany({
|
|
35
|
-
where: { userId: user.id, usedAt: null },
|
|
36
|
-
data: { usedAt: new Date() },
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const record = await db.passwordResetToken.create({
|
|
40
|
-
data: {
|
|
41
|
-
userId: user.id,
|
|
42
|
-
expiresAt: new Date(Date.now() + 60 * 60_000), // 1 hour
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
|
|
47
|
-
const resetUrl = `${appUrl}/reset-password?token=${record.token}`;
|
|
48
|
-
|
|
49
|
-
// Send reset email via Brevo. If no email provider is configured the URL is
|
|
50
|
-
// returned in the response so admins/devs can copy it manually.
|
|
51
|
-
const sent = await sendAuthEmail(
|
|
52
|
-
email,
|
|
53
|
-
"Reset your Knotpad password",
|
|
54
|
-
`
|
|
55
|
-
<p>Hi${user.name ? ` ${user.name}` : ""},</p>
|
|
56
|
-
<p>Click the link below to reset your password. This link expires in 1 hour.</p>
|
|
57
|
-
<p><a href="${resetUrl}">${resetUrl}</a></p>
|
|
58
|
-
<p>If you didn't request this, you can ignore this email.</p>
|
|
59
|
-
`
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
// When no email provider is configured, expose the URL for local/dev use.
|
|
63
|
-
if (!sent) return NextResponse.json({ ok: true, resetUrl });
|
|
64
|
-
return NextResponse.json({ ok: true });
|
|
65
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import bcrypt from "bcryptjs";
|
|
3
|
-
import { prisma, getCloudPrisma } from "@/lib/prisma";
|
|
4
|
-
import { rateLimit, getClientIp } from "@/lib/rate-limit";
|
|
5
|
-
|
|
6
|
-
const RL_MAX = 10;
|
|
7
|
-
const RL_WINDOW_MS = 60 * 60_000;
|
|
8
|
-
|
|
9
|
-
export async function POST(req: NextRequest) {
|
|
10
|
-
const ip = getClientIp(req);
|
|
11
|
-
const rl = rateLimit(`reset-password:${ip}`, RL_MAX, RL_WINDOW_MS);
|
|
12
|
-
if (rl.limited) {
|
|
13
|
-
return NextResponse.json(
|
|
14
|
-
{ error: "Too many attempts. Try again later." },
|
|
15
|
-
{ status: 429, headers: { "Retry-After": String(rl.retryAfter) } }
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const { token, password } = await req.json();
|
|
20
|
-
if (!token || typeof token !== "string") {
|
|
21
|
-
return NextResponse.json({ error: "Token required" }, { status: 400 });
|
|
22
|
-
}
|
|
23
|
-
if (!password || typeof password !== "string" || password.length < 8) {
|
|
24
|
-
return NextResponse.json({ error: "Password must be at least 8 characters" }, { status: 400 });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const db = getCloudPrisma() ?? prisma;
|
|
28
|
-
|
|
29
|
-
const record = await db.passwordResetToken.findUnique({
|
|
30
|
-
where: { token },
|
|
31
|
-
include: { user: { select: { id: true, email: true } } },
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
if (!record || record.usedAt || record.expiresAt < new Date()) {
|
|
35
|
-
return NextResponse.json({ error: "Invalid or expired token" }, { status: 400 });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const passwordHash = await bcrypt.hash(password, 12);
|
|
39
|
-
|
|
40
|
-
await db.$transaction(async (tx) => {
|
|
41
|
-
await tx.user.update({
|
|
42
|
-
where: { id: record.userId },
|
|
43
|
-
data: { passwordHash },
|
|
44
|
-
});
|
|
45
|
-
await tx.passwordResetToken.update({
|
|
46
|
-
where: { id: record.id },
|
|
47
|
-
data: { usedAt: new Date() },
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
return NextResponse.json({ ok: true });
|
|
52
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { prisma } from "@/lib/prisma";
|
|
3
|
-
import { createHmac } from "crypto";
|
|
4
|
-
import bcrypt from "bcryptjs";
|
|
5
|
-
|
|
6
|
-
function generateTOTP(secret: string, window = 0): string {
|
|
7
|
-
const counter = Math.floor(Date.now() / 1000 / 30) + window;
|
|
8
|
-
const counterBuffer = Buffer.alloc(8);
|
|
9
|
-
counterBuffer.writeBigUInt64BE(BigInt(counter), 0);
|
|
10
|
-
|
|
11
|
-
const secretBuffer = Buffer.from(secret, "base64url");
|
|
12
|
-
const hmac = createHmac("sha1", secretBuffer);
|
|
13
|
-
hmac.update(counterBuffer);
|
|
14
|
-
const hash = hmac.digest();
|
|
15
|
-
|
|
16
|
-
const offset = hash[hash.length - 1] & 0x0f;
|
|
17
|
-
const code =
|
|
18
|
-
((hash[offset] & 0x7f) << 24) |
|
|
19
|
-
((hash[offset + 1] & 0xff) << 16) |
|
|
20
|
-
((hash[offset + 2] & 0xff) << 8) |
|
|
21
|
-
(hash[offset + 3] & 0xff);
|
|
22
|
-
|
|
23
|
-
return (code % 1000000).toString().padStart(6, "0");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function verifyTOTP(secret: string, token: string): boolean {
|
|
27
|
-
for (let window = -1; window <= 1; window++) {
|
|
28
|
-
if (generateTOTP(secret, window) === token) {
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function POST(req: NextRequest) {
|
|
36
|
-
const { email, password, code } = await req.json();
|
|
37
|
-
|
|
38
|
-
if (!email || !password || !code) {
|
|
39
|
-
return NextResponse.json({ error: "Missing credentials" }, { status: 400 });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (!/^\d{6}$/.test(code)) {
|
|
43
|
-
return NextResponse.json({ error: "Invalid 2FA code format" }, { status: 400 });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const db = prisma;
|
|
47
|
-
const user = await db.user.findUnique({
|
|
48
|
-
where: { email },
|
|
49
|
-
select: {
|
|
50
|
-
id: true,
|
|
51
|
-
email: true,
|
|
52
|
-
name: true,
|
|
53
|
-
passwordHash: true,
|
|
54
|
-
twoFactorSecret: true,
|
|
55
|
-
twoFactorEnabled: true,
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
if (!user || !user.passwordHash) {
|
|
60
|
-
return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Verify password first
|
|
64
|
-
const validPassword = await bcrypt.compare(password, user.passwordHash);
|
|
65
|
-
if (!validPassword) {
|
|
66
|
-
return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Check if 2FA is enabled
|
|
70
|
-
if (!user.twoFactorEnabled || !user.twoFactorSecret) {
|
|
71
|
-
return NextResponse.json({ error: "2FA not enabled" }, { status: 400 });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Verify TOTP code
|
|
75
|
-
if (!verifyTOTP(user.twoFactorSecret, code)) {
|
|
76
|
-
return NextResponse.json({ error: "Invalid 2FA code" }, { status: 401 });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Return user info for client-side session creation
|
|
80
|
-
return NextResponse.json({
|
|
81
|
-
success: true,
|
|
82
|
-
user: {
|
|
83
|
-
id: user.id,
|
|
84
|
-
email: user.email,
|
|
85
|
-
name: user.name ?? "",
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import { auth } from "@/auth";
|
|
3
|
-
import { prisma } from "@/lib/prisma";
|
|
4
|
-
import { buildWorkspaceBackup } from "@/lib/backup/export-workspace-backup";
|
|
5
|
-
|
|
6
|
-
export async function GET() {
|
|
7
|
-
const session = await auth();
|
|
8
|
-
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
9
|
-
|
|
10
|
-
const member = await prisma.workspaceMember.findFirst({
|
|
11
|
-
where: { userId: session.user.id, revokedAt: null },
|
|
12
|
-
include: { workspace: true },
|
|
13
|
-
});
|
|
14
|
-
if (!member) return NextResponse.json({ error: "No workspace" }, { status: 404 });
|
|
15
|
-
|
|
16
|
-
const { filename, payload } = await buildWorkspaceBackup(
|
|
17
|
-
member.workspaceId,
|
|
18
|
-
member.workspace.name,
|
|
19
|
-
member.workspace.slug
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
return new NextResponse(JSON.stringify(payload, null, 2), {
|
|
23
|
-
headers: {
|
|
24
|
-
"Content-Type": "application/json",
|
|
25
|
-
"Content-Disposition": `attachment; filename="${filename}"`,
|
|
26
|
-
"x-brief-filename": filename,
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import { auth } from "@/auth";
|
|
3
|
-
import { prisma } from "@/lib/prisma";
|
|
4
|
-
import { buildNotesZipExport } from "@/lib/backup/export-notes-zip";
|
|
5
|
-
|
|
6
|
-
export async function GET() {
|
|
7
|
-
const session = await auth();
|
|
8
|
-
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
9
|
-
|
|
10
|
-
const member = await prisma.workspaceMember.findFirst({
|
|
11
|
-
where: { userId: session.user.id, revokedAt: null },
|
|
12
|
-
include: { workspace: true },
|
|
13
|
-
});
|
|
14
|
-
if (!member) return NextResponse.json({ error: "No workspace" }, { status: 404 });
|
|
15
|
-
|
|
16
|
-
const { filename, bytes } = await buildNotesZipExport(member.workspaceId);
|
|
17
|
-
|
|
18
|
-
return new NextResponse(Buffer.from(bytes), {
|
|
19
|
-
headers: {
|
|
20
|
-
"Content-Type": "application/zip",
|
|
21
|
-
"Content-Disposition": `attachment; filename="${filename}"`,
|
|
22
|
-
"x-brief-filename": filename,
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { auth } from "@/auth";
|
|
3
|
-
import { prisma } from "@/lib/prisma";
|
|
4
|
-
import { validateBackupSettingsInput } from "@/lib/backup/backup-settings";
|
|
5
|
-
|
|
6
|
-
export async function GET() {
|
|
7
|
-
const session = await auth();
|
|
8
|
-
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
9
|
-
|
|
10
|
-
const member = await prisma.workspaceMember.findFirst({
|
|
11
|
-
where: {
|
|
12
|
-
userId: session.user.id,
|
|
13
|
-
role: { in: ["OWNER", "ADMIN"] },
|
|
14
|
-
revokedAt: null,
|
|
15
|
-
},
|
|
16
|
-
include: {
|
|
17
|
-
workspace: {
|
|
18
|
-
include: {
|
|
19
|
-
backupSettings: true,
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
if (!member) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
25
|
-
|
|
26
|
-
return NextResponse.json({
|
|
27
|
-
workspace: {
|
|
28
|
-
id: member.workspaceId,
|
|
29
|
-
isCloud: member.workspace.isCloud,
|
|
30
|
-
isPro: member.workspace.isPro,
|
|
31
|
-
},
|
|
32
|
-
settings: member.workspace.backupSettings,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function PUT(req: NextRequest) {
|
|
37
|
-
const session = await auth();
|
|
38
|
-
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
39
|
-
|
|
40
|
-
const member = await prisma.workspaceMember.findFirst({
|
|
41
|
-
where: {
|
|
42
|
-
userId: session.user.id,
|
|
43
|
-
role: { in: ["OWNER", "ADMIN"] },
|
|
44
|
-
revokedAt: null,
|
|
45
|
-
},
|
|
46
|
-
include: { workspace: true },
|
|
47
|
-
});
|
|
48
|
-
if (!member) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
49
|
-
|
|
50
|
-
const body = await req.json().catch(() => null);
|
|
51
|
-
if (!body) return NextResponse.json({ error: "Invalid body" }, { status: 400 });
|
|
52
|
-
|
|
53
|
-
const validation = validateBackupSettingsInput({
|
|
54
|
-
scheduleEnabled: Boolean(body.scheduleEnabled),
|
|
55
|
-
scheduleCadence: body.scheduleCadence,
|
|
56
|
-
destinationPath: typeof body.destinationPath === "string" ? body.destinationPath : "",
|
|
57
|
-
includeMarkdownZip: Boolean(body.includeMarkdownZip),
|
|
58
|
-
isCloudWorkspace: member.workspace.isCloud,
|
|
59
|
-
});
|
|
60
|
-
if (!validation.success) {
|
|
61
|
-
return NextResponse.json({ error: validation.error }, { status: 422 });
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const settings = await prisma.workspaceBackupSettings.upsert({
|
|
65
|
-
where: { workspaceId: member.workspaceId },
|
|
66
|
-
update: {
|
|
67
|
-
scheduleEnabled: Boolean(body.scheduleEnabled),
|
|
68
|
-
scheduleCadence: body.scheduleCadence,
|
|
69
|
-
destinationPath: body.destinationPath?.trim() ? body.destinationPath.trim() : null,
|
|
70
|
-
includeMarkdownZip: Boolean(body.includeMarkdownZip),
|
|
71
|
-
lastBackupAt: body.lastBackupAt ? new Date(body.lastBackupAt) : undefined,
|
|
72
|
-
lastBackupStatus:
|
|
73
|
-
typeof body.lastBackupStatus === "string" ? body.lastBackupStatus : undefined,
|
|
74
|
-
lastBackupError:
|
|
75
|
-
typeof body.lastBackupError === "string" ? body.lastBackupError : body.lastBackupError === null ? null : undefined,
|
|
76
|
-
},
|
|
77
|
-
create: {
|
|
78
|
-
workspaceId: member.workspaceId,
|
|
79
|
-
scheduleEnabled: Boolean(body.scheduleEnabled),
|
|
80
|
-
scheduleCadence: body.scheduleCadence,
|
|
81
|
-
destinationPath: body.destinationPath?.trim() ? body.destinationPath.trim() : null,
|
|
82
|
-
includeMarkdownZip: Boolean(body.includeMarkdownZip),
|
|
83
|
-
lastBackupAt: body.lastBackupAt ? new Date(body.lastBackupAt) : null,
|
|
84
|
-
lastBackupStatus:
|
|
85
|
-
typeof body.lastBackupStatus === "string" ? body.lastBackupStatus : "idle",
|
|
86
|
-
lastBackupError:
|
|
87
|
-
typeof body.lastBackupError === "string" ? body.lastBackupError : null,
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
return NextResponse.json({ settings });
|
|
92
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { auth } from "@/auth";
|
|
3
|
-
import { prisma } from "@/lib/prisma";
|
|
4
|
-
import { stripe, PRICE_ID_PERSONAL_PRO, PRICE_ID_TEAM_PRO, APP_URL } from "@/lib/stripe";
|
|
5
|
-
|
|
6
|
-
export async function POST(req: NextRequest) {
|
|
7
|
-
const session = await auth();
|
|
8
|
-
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
9
|
-
|
|
10
|
-
const body = await req.json().catch(() => ({}));
|
|
11
|
-
|
|
12
|
-
// plan = "personal" (1 seat, PERSONAL_PRO) | "team" (N seats, TEAM_PRO, min 2)
|
|
13
|
-
const plan: "personal" | "team" = body.plan === "team" ? "team" : "personal";
|
|
14
|
-
const seats: number = plan === "team" ? Math.max(2, parseInt(body.seats ?? "2")) : 1;
|
|
15
|
-
const targetWorkspaceId: string | undefined = body.workspaceId;
|
|
16
|
-
|
|
17
|
-
// Find the workspace to upgrade. For team checkouts a specific workspaceId is
|
|
18
|
-
// required (the newly created team workspace). For personal, use the caller's
|
|
19
|
-
// personal workspace.
|
|
20
|
-
const member = targetWorkspaceId
|
|
21
|
-
? await prisma.workspaceMember.findFirst({
|
|
22
|
-
where: {
|
|
23
|
-
userId: session.user.id,
|
|
24
|
-
workspaceId: targetWorkspaceId,
|
|
25
|
-
role: { in: ["OWNER", "ADMIN"] },
|
|
26
|
-
},
|
|
27
|
-
include: { workspace: true },
|
|
28
|
-
})
|
|
29
|
-
: await prisma.workspaceMember.findFirst({
|
|
30
|
-
where: {
|
|
31
|
-
userId: session.user.id,
|
|
32
|
-
role: { in: ["OWNER", "ADMIN"] },
|
|
33
|
-
workspace: { type: "PERSONAL" },
|
|
34
|
-
},
|
|
35
|
-
include: { workspace: true },
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
if (!member) return NextResponse.json({ error: "Must be owner or admin" }, { status: 403 });
|
|
39
|
-
|
|
40
|
-
const { workspace } = member;
|
|
41
|
-
|
|
42
|
-
// Guard: prevent creating a second checkout session for an already-active subscription.
|
|
43
|
-
if (workspace.isPro && workspace.stripeSubId) {
|
|
44
|
-
return NextResponse.json(
|
|
45
|
-
{ error: "Workspace is already subscribed", stripeSubId: workspace.stripeSubId },
|
|
46
|
-
{ status: 409 }
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Create or reuse Stripe customer
|
|
51
|
-
let customerId = workspace.stripeId;
|
|
52
|
-
if (!customerId) {
|
|
53
|
-
const customer = await stripe.customers.create({
|
|
54
|
-
email: session.user.email,
|
|
55
|
-
name: session.user.name,
|
|
56
|
-
metadata: { workspaceId: workspace.id },
|
|
57
|
-
});
|
|
58
|
-
customerId = customer.id;
|
|
59
|
-
await prisma.workspace.update({
|
|
60
|
-
where: { id: workspace.id },
|
|
61
|
-
data: { stripeId: customerId },
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const planType = plan === "team" ? "TEAM_PRO" : "PERSONAL_PRO";
|
|
66
|
-
const priceId = plan === "team" ? PRICE_ID_TEAM_PRO : PRICE_ID_PERSONAL_PRO;
|
|
67
|
-
|
|
68
|
-
const checkoutSession = await stripe.checkout.sessions.create({
|
|
69
|
-
customer: customerId,
|
|
70
|
-
mode: "subscription",
|
|
71
|
-
line_items: [{ price: priceId, quantity: seats }],
|
|
72
|
-
success_url: `${APP_URL}/settings/billing?upgraded=1`,
|
|
73
|
-
cancel_url: `${APP_URL}/settings/billing`,
|
|
74
|
-
metadata: { workspaceId: workspace.id, userId: session.user.id, planType },
|
|
75
|
-
subscription_data: {
|
|
76
|
-
metadata: { workspaceId: workspace.id, planType },
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
return NextResponse.json({ url: checkoutSession.url });
|
|
81
|
-
}
|