@knotpad/app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +167 -0
- package/app/(app)/calendar/page.tsx +57 -0
- package/app/(app)/error.tsx +35 -0
- package/app/(app)/graph/page.tsx +32 -0
- package/app/(app)/guide/page.tsx +21 -0
- package/app/(app)/kanban/loading.tsx +24 -0
- package/app/(app)/kanban/page.tsx +59 -0
- package/app/(app)/layout.tsx +122 -0
- package/app/(app)/list/loading.tsx +21 -0
- package/app/(app)/list/page.tsx +137 -0
- package/app/(app)/loading.tsx +18 -0
- package/app/(app)/notes/[noteId]/page.tsx +84 -0
- package/app/(app)/notes/layout.tsx +30 -0
- package/app/(app)/notes/page.tsx +39 -0
- package/app/(app)/page.tsx +5 -0
- package/app/(app)/settings/agent-token/page.tsx +59 -0
- package/app/(app)/settings/backup/page.tsx +49 -0
- package/app/(app)/settings/billing/page.tsx +53 -0
- package/app/(app)/settings/calendar/page.tsx +41 -0
- package/app/(app)/settings/layout.test.tsx +39 -0
- package/app/(app)/settings/layout.tsx +71 -0
- package/app/(app)/settings/page.tsx +4 -0
- package/app/(app)/settings/security/page.tsx +43 -0
- package/app/(app)/settings/team/page.tsx +74 -0
- package/app/(app)/settings/workspace/page.tsx +27 -0
- package/app/(app)/tasks/[taskId]/page.tsx +79 -0
- package/app/(auth)/forgot-password/page.tsx +106 -0
- package/app/(auth)/guest/page.tsx +56 -0
- package/app/(auth)/layout.tsx +13 -0
- package/app/(auth)/login/page.tsx +14 -0
- package/app/(auth)/register/page.tsx +193 -0
- package/app/(auth)/reset-password/page.tsx +138 -0
- package/app/api/account/claim/route.tsx +135 -0
- package/app/api/admin/backfill-encryption/route.tsx +43 -0
- package/app/api/admin/license/route.tsx +42 -0
- package/app/api/auth/2fa/route.tsx +148 -0
- package/app/api/auth/[...nextauth]/route.tsx +3 -0
- package/app/api/auth/change-password/route.tsx +61 -0
- package/app/api/auth/check-2fa/route.tsx +19 -0
- package/app/api/auth/forgot-password/route.tsx +65 -0
- package/app/api/auth/reset-password/route.tsx +52 -0
- package/app/api/auth/verify-2fa/route.tsx +88 -0
- package/app/api/backup/download/db/route.ts +29 -0
- package/app/api/backup/download/notes/route.ts +25 -0
- package/app/api/backup/settings/route.ts +92 -0
- package/app/api/billing/checkout/route.tsx +81 -0
- package/app/api/billing/migrate/route.tsx +163 -0
- package/app/api/billing/portal/route.tsx +24 -0
- package/app/api/billing/setup-intent/route.tsx +55 -0
- package/app/api/billing/status/route.tsx +36 -0
- package/app/api/billing/subscribe/route.tsx +85 -0
- package/app/api/billing/webhook/route.tsx +199 -0
- package/app/api/calendar-feeds/[feedId]/route.tsx +67 -0
- package/app/api/calendar-feeds/[feedId]/sync/route.tsx +37 -0
- package/app/api/calendar-feeds/events/route.tsx +82 -0
- package/app/api/calendar-feeds/route.tsx +52 -0
- package/app/api/calendar-feeds/sync-all/route.tsx +34 -0
- package/app/api/cron/calendar-feeds/route.tsx +31 -0
- package/app/api/cron/stale-tasks/route.tsx +51 -0
- package/app/api/cron/sync/route.tsx +34 -0
- package/app/api/devices/[deviceId]/route.tsx +25 -0
- package/app/api/devices/route.tsx +41 -0
- package/app/api/export/route.tsx +40 -0
- package/app/api/feedback/route.tsx +54 -0
- package/app/api/folders/[folderId]/route.tsx +51 -0
- package/app/api/folders/route.tsx +37 -0
- package/app/api/graph/route.tsx +242 -0
- package/app/api/guest/route.tsx +58 -0
- package/app/api/health/route.tsx +10 -0
- package/app/api/holidays/countries/route.tsx +14 -0
- package/app/api/holidays/route.tsx +49 -0
- package/app/api/holidays/states/route.tsx +21 -0
- package/app/api/invites/[token]/route.tsx +131 -0
- package/app/api/invites/route.tsx +74 -0
- package/app/api/mcp/generate-token/route.tsx +55 -0
- package/app/api/mcp/revoke-token/[tokenId]/route.tsx +30 -0
- package/app/api/mcp/update-alias/[tokenId]/route.tsx +22 -0
- package/app/api/notes/[noteId]/export/route.tsx +45 -0
- package/app/api/notes/[noteId]/route.tsx +360 -0
- package/app/api/notes/route.tsx +112 -0
- package/app/api/notifications/route.tsx +44 -0
- package/app/api/register/route.tsx +67 -0
- package/app/api/restore/route.tsx +148 -0
- package/app/api/sync/conflicts/[conflictId]/route.tsx +134 -0
- package/app/api/sync/conflicts/route.tsx +48 -0
- package/app/api/sync/status/route.tsx +49 -0
- package/app/api/sync/trigger/route.tsx +15 -0
- package/app/api/tasks/[taskId]/detail/route.tsx +68 -0
- package/app/api/tasks/[taskId]/route.tsx +259 -0
- package/app/api/tasks/bulk/route.tsx +133 -0
- package/app/api/tasks/route.tsx +36 -0
- package/app/api/workspace/active/route.tsx +39 -0
- package/app/api/workspace/create-team/route.tsx +42 -0
- package/app/api/workspace/kanban-statuses/route.tsx +71 -0
- package/app/api/workspace/members/[memberId]/route.tsx +69 -0
- package/app/api/workspace/route.tsx +24 -0
- package/app/download/page.tsx +170 -0
- package/app/favicon.ico +0 -0
- package/app/generated/prisma/client.d.ts +1 -0
- package/app/generated/prisma/client.js +5 -0
- package/app/generated/prisma/default.d.ts +1 -0
- package/app/generated/prisma/default.js +5 -0
- package/app/generated/prisma/edge.d.ts +1 -0
- package/app/generated/prisma/edge.js +497 -0
- package/app/generated/prisma/index-browser.js +523 -0
- package/app/generated/prisma/index.d.ts +46376 -0
- package/app/generated/prisma/index.js +497 -0
- package/app/generated/prisma/package.json +144 -0
- package/app/generated/prisma/query_compiler_fast_bg.js +2 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +2 -0
- package/app/generated/prisma/runtime/client.d.ts +3386 -0
- package/app/generated/prisma/runtime/client.js +86 -0
- package/app/generated/prisma/runtime/index-browser.d.ts +90 -0
- package/app/generated/prisma/runtime/index-browser.js +6 -0
- package/app/generated/prisma/runtime/wasm-compiler-edge.js +76 -0
- package/app/generated/prisma/schema.prisma +456 -0
- package/app/generated/prisma/wasm-edge-light-loader.mjs +5 -0
- package/app/generated/prisma/wasm-worker-loader.mjs +5 -0
- package/app/globals.css +54 -0
- package/app/invite/[token]/page.tsx +52 -0
- package/app/layout.tsx +90 -0
- package/app/mcp/route.tsx +430 -0
- package/app/opengraph-image.tsx +120 -0
- package/app/page.tsx +398 -0
- package/app/privacy/page.tsx +69 -0
- package/app/robots.tsx +25 -0
- package/app/sitemap.tsx +36 -0
- package/app/terms/page.tsx +69 -0
- package/app/upgrade/page.tsx +75 -0
- package/auth.config.ts +33 -0
- package/auth.ts +79 -0
- package/bin/brief.js +224 -0
- package/components/auth/login-form.tsx +302 -0
- package/components/auth/password-checklist.tsx +31 -0
- package/components/auth/password-input.tsx +36 -0
- package/components/auth/switch-account-button.test.tsx +22 -0
- package/components/auth/switch-account-button.tsx +19 -0
- package/components/auth/two-factor-input.tsx +116 -0
- package/components/billing/billing-dashboard.tsx +265 -0
- package/components/billing/card-form.tsx +210 -0
- package/components/billing/claim-account-form.tsx +99 -0
- package/components/branding/app-logo.test.tsx +20 -0
- package/components/branding/app-logo.tsx +25 -0
- package/components/calendar/calendar-agenda.tsx +150 -0
- package/components/calendar/calendar-drag.test.tsx +177 -0
- package/components/calendar/calendar-grid.tsx +357 -0
- package/components/calendar/calendar-hooks.test.tsx +27 -0
- package/components/calendar/calendar-hooks.ts +351 -0
- package/components/calendar/calendar-toolbar.test.tsx +68 -0
- package/components/calendar/calendar-toolbar.tsx +291 -0
- package/components/calendar/calendar-types.ts +148 -0
- package/components/calendar/calendar-view.test.tsx +295 -0
- package/components/calendar/calendar-view.tsx +307 -0
- package/components/calendar/day-detail-popover.tsx +174 -0
- package/components/calendar/task-chip.tsx +86 -0
- package/components/command/command-palette.test.tsx +33 -0
- package/components/command/command-palette.tsx +310 -0
- package/components/download-cta.tsx +87 -0
- package/components/feedback/feedback-popup.tsx +207 -0
- package/components/graph/graph-draw.ts +337 -0
- package/components/graph/graph-overlays.tsx +160 -0
- package/components/graph/graph-page.test.tsx +131 -0
- package/components/graph/graph-page.tsx +263 -0
- package/components/graph/graph-types.ts +47 -0
- package/components/graph/graph-view.tsx +322 -0
- package/components/guide/guide-view.tsx +522 -0
- package/components/kanban/kanban-board.test.tsx +128 -0
- package/components/kanban/kanban-board.tsx +361 -0
- package/components/kanban/kanban-card-menu.tsx +102 -0
- package/components/kanban/kanban-card.tsx +227 -0
- package/components/kanban/kanban-column.tsx +49 -0
- package/components/kanban/kanban-status-context.tsx +28 -0
- package/components/landing/calendar-sandbox.test.tsx +15 -0
- package/components/landing/calendar-sandbox.tsx +107 -0
- package/components/landing/graph-sandbox.test.tsx +27 -0
- package/components/landing/graph-sandbox.tsx +80 -0
- package/components/landing/kanban-sandbox.test.tsx +24 -0
- package/components/landing/kanban-sandbox.tsx +101 -0
- package/components/landing/landing-showcase.test.tsx +21 -0
- package/components/landing/landing-showcase.tsx +54 -0
- package/components/landing/list-sandbox.tsx +86 -0
- package/components/landing/mock-workspace.ts +168 -0
- package/components/landing/notes-sandbox.test.tsx +14 -0
- package/components/landing/notes-sandbox.tsx +88 -0
- package/components/layout/app-shell.tsx +83 -0
- package/components/layout/backup-scheduler.tsx +122 -0
- package/components/layout/bottom-nav.tsx +43 -0
- package/components/layout/icon-bar.test.tsx +29 -0
- package/components/layout/icon-bar.tsx +118 -0
- package/components/layout/mobile-top-bar.tsx +68 -0
- package/components/layout/notes-panel-folder.tsx +127 -0
- package/components/layout/notes-panel-note-item.tsx +140 -0
- package/components/layout/notes-panel-task-tab.tsx +63 -0
- package/components/layout/notes-panel-types.ts +44 -0
- package/components/layout/notes-panel.tsx +476 -0
- package/components/layout/notification-bell.tsx +251 -0
- package/components/layout/paywall-screen.tsx +41 -0
- package/components/layout/pro-banner.tsx +76 -0
- package/components/layout/sw-register.tsx +27 -0
- package/components/layout/workspace-switcher.tsx +90 -0
- package/components/notes/mobile-bottom-sheet.tsx +99 -0
- package/components/notes/note-editor-context-menu.tsx +47 -0
- package/components/notes/note-editor-dom.ts +33 -0
- package/components/notes/note-editor-dropdowns.tsx +484 -0
- package/components/notes/note-editor-hooks.ts +692 -0
- package/components/notes/note-editor-keyboard.ts +305 -0
- package/components/notes/note-editor-overlay.tsx +90 -0
- package/components/notes/note-editor.test.tsx +372 -0
- package/components/notes/note-editor.tsx +662 -0
- package/components/notes/note-preview-pane.tsx +156 -0
- package/components/notes/note-tabs.tsx +120 -0
- package/components/notes/note-types.tsx +157 -0
- package/components/settings/accept-invite.tsx +108 -0
- package/components/settings/agent-token-settings.tsx +369 -0
- package/components/settings/backup-restore-settings.test.tsx +25 -0
- package/components/settings/backup-restore-settings.tsx +327 -0
- package/components/settings/calendar-feeds-settings.tsx +489 -0
- package/components/settings/calendar-general-settings.tsx +174 -0
- package/components/settings/confirm-danger-action.test.tsx +215 -0
- package/components/settings/confirm-danger-action.tsx +65 -0
- package/components/settings/security-settings.tsx +252 -0
- package/components/settings/settings-guidance.test.tsx +98 -0
- package/components/settings/team-settings.tsx +319 -0
- package/components/settings/two-factor-auth.tsx +296 -0
- package/components/settings/workspace-settings-client.tsx +363 -0
- package/components/settings/workspace-settings-form.tsx +73 -0
- package/components/sync/conflict-viewer.tsx +247 -0
- package/components/sync/sync-indicator.tsx +171 -0
- package/components/tasks/snippet-thread.tsx +119 -0
- package/components/tasks/status-dot.tsx +47 -0
- package/components/tasks/task-badge.tsx +43 -0
- package/components/tasks/task-detail.test.tsx +187 -0
- package/components/tasks/task-detail.tsx +458 -0
- package/components/tasks/task-list-filters.test.tsx +75 -0
- package/components/tasks/task-list-filters.tsx +163 -0
- package/components/tasks/task-list-types.ts +20 -0
- package/components/tasks/task-list.test.tsx +175 -0
- package/components/tasks/task-list.tsx +481 -0
- package/components/tasks/task-row.tsx +85 -0
- package/components/tasks/task-table-row.tsx +259 -0
- package/components/ui/skeleton.tsx +3 -0
- package/components/ui/toast.test.tsx +42 -0
- package/components/ui/toast.tsx +70 -0
- package/electron/main.ts +251 -0
- package/electron/preload.ts +56 -0
- package/instrumentation.tsx +23 -0
- package/lib/api-error.ts +50 -0
- package/lib/backup/backup-runner.test.ts +32 -0
- package/lib/backup/backup-runner.ts +19 -0
- package/lib/backup/backup-schedule.test.ts +23 -0
- package/lib/backup/backup-schedule.ts +55 -0
- package/lib/backup/backup-settings.test.ts +30 -0
- package/lib/backup/backup-settings.ts +27 -0
- package/lib/backup/export-notes-zip.test.ts +26 -0
- package/lib/backup/export-notes-zip.ts +82 -0
- package/lib/backup/export-workspace-backup.test.ts +17 -0
- package/lib/backup/export-workspace-backup.ts +77 -0
- package/lib/backup/restore-workspace-from-export.test.ts +18 -0
- package/lib/backup/restore-workspace-from-export.ts +183 -0
- package/lib/backup/types.ts +14 -0
- package/lib/brand-icons.ts +1 -0
- package/lib/calendar-feed-crypto.ts +38 -0
- package/lib/calendar-feed.ts +239 -0
- package/lib/client/online-status.ts +47 -0
- package/lib/conflict-resolver.test.ts +57 -0
- package/lib/conflict-resolver.ts +240 -0
- package/lib/db-init.ts +79 -0
- package/lib/email.ts +159 -0
- package/lib/encryption.test.ts +41 -0
- package/lib/encryption.ts +98 -0
- package/lib/extract-snippet.test.ts +123 -0
- package/lib/extract-snippet.ts +69 -0
- package/lib/kanban-status.ts +55 -0
- package/lib/license.ts +21 -0
- package/lib/limits.ts +31 -0
- package/lib/mcp-auth.test.ts +58 -0
- package/lib/mcp-auth.ts +65 -0
- package/lib/mcp-contract.test.ts +25 -0
- package/lib/mcp-contract.ts +210 -0
- package/lib/mcp-handler.ts +31 -0
- package/lib/mcp-url.test.ts +12 -0
- package/lib/mcp-url.ts +7 -0
- package/lib/mentions.test.ts +45 -0
- package/lib/mentions.ts +73 -0
- package/lib/note-crypto.ts +108 -0
- package/lib/note-sync.ts +201 -0
- package/lib/note-title.ts +93 -0
- package/lib/prisma.ts +193 -0
- package/lib/pro-flush.ts +292 -0
- package/lib/rate-limit.ts +57 -0
- package/lib/stripe.ts +38 -0
- package/lib/sync-worker.ts +388 -0
- package/lib/task-parser.test.ts +91 -0
- package/lib/task-parser.ts +81 -0
- package/lib/task-utils.ts +52 -0
- package/lib/use-is-electron.ts +19 -0
- package/lib/use-is-mobile.ts +22 -0
- package/lib/validation/calendar-feed.ts +31 -0
- package/lib/validation/note.ts +27 -0
- package/lib/validation/task.ts +26 -0
- package/lib/view-preferences.test.ts +54 -0
- package/lib/view-preferences.ts +28 -0
- package/lib/workspace.ts +66 -0
- package/next.config.ts +21 -0
- package/package.json +99 -0
- package/postcss.config.mjs +7 -0
- package/prisma/migrations/20260519021916_init/migration.sql +388 -0
- package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +8 -0
- package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +2 -0
- package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +12 -0
- package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +3 -0
- package/prisma/migrations/20260529030000_add_folders/migration.sql +17 -0
- package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +31 -0
- package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +5 -0
- package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +14 -0
- package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +26 -0
- package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +23 -0
- package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +43 -0
- package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +2 -0
- package/prisma/migrations/20260611050000_add_task_priority/migration.sql +14 -0
- package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +2 -0
- package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +25 -0
- package/prisma/migrations/20260614160000_add_feedback/migration.sql +20 -0
- package/prisma/migrations/20260614210000_add_2fa/migration.sql +4 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +457 -0
- package/public/Logo_icon.svg +1 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +4 -0
- package/public/icon_dark.svg +1 -0
- package/public/knotpad_icon.svg +1 -0
- package/public/knotpad_logo_full.svg +1 -0
- package/public/manifest.json +14 -0
- package/public/next.svg +1 -0
- package/public/sw.js +137 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +35 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { auth } from "@/auth";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { stripe } from "@/lib/stripe";
|
|
5
|
+
|
|
6
|
+
export async function GET(
|
|
7
|
+
_req: NextRequest,
|
|
8
|
+
{ params }: { params: Promise<{ token: string }> }
|
|
9
|
+
) {
|
|
10
|
+
const { token } = await params;
|
|
11
|
+
|
|
12
|
+
const invite = await prisma.inviteToken.findUnique({
|
|
13
|
+
where: { token },
|
|
14
|
+
include: { workspace: { select: { name: true } }, invitedBy: { select: { name: true } } },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (!invite) return NextResponse.json({ error: "Invite not found" }, { status: 404 });
|
|
18
|
+
if (invite.acceptedAt) return NextResponse.json({ error: "Already accepted" }, { status: 410 });
|
|
19
|
+
if (invite.expiresAt < new Date()) return NextResponse.json({ error: "Invite expired" }, { status: 410 });
|
|
20
|
+
|
|
21
|
+
return NextResponse.json({
|
|
22
|
+
email: invite.email,
|
|
23
|
+
workspaceName: invite.workspace.name,
|
|
24
|
+
invitedBy: invite.invitedBy.name,
|
|
25
|
+
role: invite.role,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// POST = accept invite
|
|
30
|
+
export async function POST(
|
|
31
|
+
_req: NextRequest,
|
|
32
|
+
{ params }: { params: Promise<{ token: string }> }
|
|
33
|
+
) {
|
|
34
|
+
const session = await auth();
|
|
35
|
+
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
36
|
+
|
|
37
|
+
const { token } = await params;
|
|
38
|
+
|
|
39
|
+
const invite = await prisma.inviteToken.findUnique({
|
|
40
|
+
where: { token },
|
|
41
|
+
include: { workspace: true },
|
|
42
|
+
});
|
|
43
|
+
if (!invite) return NextResponse.json({ error: "Invalid invite" }, { status: 404 });
|
|
44
|
+
if (invite.acceptedAt) return NextResponse.json({ error: "Already used" }, { status: 410 });
|
|
45
|
+
if (invite.expiresAt < new Date()) return NextResponse.json({ error: "Expired" }, { status: 410 });
|
|
46
|
+
if (invite.email !== session.user.email) {
|
|
47
|
+
return NextResponse.json({ error: "This invite was sent to a different email" }, { status: 403 });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const workspace = invite.workspace;
|
|
51
|
+
|
|
52
|
+
await prisma.$transaction(async (tx) => {
|
|
53
|
+
// Re-activate if previously revoked, otherwise create fresh
|
|
54
|
+
const existingMember = await tx.workspaceMember.findUnique({
|
|
55
|
+
where: { userId_workspaceId: { userId: session.user.id, workspaceId: workspace.id } },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (existingMember) {
|
|
59
|
+
await tx.workspaceMember.update({
|
|
60
|
+
where: { id: existingMember.id },
|
|
61
|
+
data: { role: invite.role, revokedAt: null },
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
await tx.workspaceMember.create({
|
|
65
|
+
data: { userId: session.user.id, workspaceId: workspace.id, role: invite.role },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await tx.inviteToken.update({
|
|
70
|
+
where: { token },
|
|
71
|
+
data: { acceptedAt: new Date() },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await tx.notification.create({
|
|
75
|
+
data: {
|
|
76
|
+
userId: invite.invitedById,
|
|
77
|
+
type: "invite_accepted",
|
|
78
|
+
title: `${session.user.name ?? session.user.email} accepted your invite`,
|
|
79
|
+
body: `They've joined the workspace as ${invite.role.toLowerCase()}.`,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Increment Stripe seat quantity if workspace has an active subscription
|
|
85
|
+
if (workspace.stripeSubId) {
|
|
86
|
+
try {
|
|
87
|
+
const sub = await stripe.subscriptions.retrieve(workspace.stripeSubId);
|
|
88
|
+
const item = sub.items.data[0];
|
|
89
|
+
if (item) {
|
|
90
|
+
const newQuantity = (item.quantity ?? 1) + 1;
|
|
91
|
+
await stripe.subscriptionItems.update(item.id, {
|
|
92
|
+
quantity: newQuantity,
|
|
93
|
+
proration_behavior: "create_prorations", // charge immediately for new seat
|
|
94
|
+
});
|
|
95
|
+
await prisma.workspace.update({
|
|
96
|
+
where: { id: workspace.id },
|
|
97
|
+
data: { seatCount: newQuantity },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// Non-fatal: Stripe seat sync failure doesn't block the join.
|
|
102
|
+
// Seat count will reconcile on next webhook or manual billing check.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return NextResponse.json({ workspaceId: workspace.id });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// DELETE = revoke invite
|
|
110
|
+
export async function DELETE(
|
|
111
|
+
_req: NextRequest,
|
|
112
|
+
{ params }: { params: Promise<{ token: string }> }
|
|
113
|
+
) {
|
|
114
|
+
const session = await auth();
|
|
115
|
+
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
116
|
+
|
|
117
|
+
const { token } = await params;
|
|
118
|
+
|
|
119
|
+
const invite = await prisma.inviteToken.findUnique({ where: { token } });
|
|
120
|
+
if (!invite) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
121
|
+
|
|
122
|
+
const member = await prisma.workspaceMember.findFirst({
|
|
123
|
+
where: { userId: session.user.id, workspaceId: invite.workspaceId, revokedAt: null },
|
|
124
|
+
});
|
|
125
|
+
if (!member || member.role === "MEMBER") {
|
|
126
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await prisma.inviteToken.delete({ where: { token } });
|
|
130
|
+
return NextResponse.json({ ok: true });
|
|
131
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { auth } from "@/auth";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
|
|
5
|
+
export async function POST(req: NextRequest) {
|
|
6
|
+
const session = await auth();
|
|
7
|
+
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
8
|
+
|
|
9
|
+
const body = await req.json();
|
|
10
|
+
const { email, role = "MEMBER", workspaceId } = body;
|
|
11
|
+
|
|
12
|
+
if (!email) return NextResponse.json({ error: "Email required" }, { status: 400 });
|
|
13
|
+
if (!["MEMBER", "ADMIN"].includes(role)) {
|
|
14
|
+
return NextResponse.json({ error: "Invalid role" }, { status: 400 });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Find the workspace the caller wants to invite into
|
|
18
|
+
const member = workspaceId
|
|
19
|
+
? await prisma.workspaceMember.findFirst({
|
|
20
|
+
where: { userId: session.user.id, workspaceId, revokedAt: null },
|
|
21
|
+
include: { workspace: true },
|
|
22
|
+
})
|
|
23
|
+
: await prisma.workspaceMember.findFirst({
|
|
24
|
+
where: { userId: session.user.id, revokedAt: null },
|
|
25
|
+
include: { workspace: true },
|
|
26
|
+
orderBy: { joinedAt: "asc" },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!member) return NextResponse.json({ error: "No workspace found" }, { status: 404 });
|
|
30
|
+
if (member.role === "MEMBER") {
|
|
31
|
+
return NextResponse.json({ error: "Only owners and admins can invite" }, { status: 403 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { workspace } = member;
|
|
35
|
+
|
|
36
|
+
// Personal workspaces cannot have invites — owner must upgrade to Team Pro first
|
|
37
|
+
if (workspace.type === "PERSONAL") {
|
|
38
|
+
return NextResponse.json(
|
|
39
|
+
{
|
|
40
|
+
error: "upgrade_required",
|
|
41
|
+
message: "Inviting members requires a Team Pro workspace.",
|
|
42
|
+
},
|
|
43
|
+
{ status: 402 }
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if already an active member
|
|
48
|
+
const existing = await prisma.user.findUnique({
|
|
49
|
+
where: { email },
|
|
50
|
+
include: {
|
|
51
|
+
workspaces: { where: { workspaceId: workspace.id, revokedAt: null } },
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
if (existing?.workspaces.length) {
|
|
55
|
+
return NextResponse.json({ error: "User is already a member" }, { status: 409 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
59
|
+
|
|
60
|
+
const invite = await prisma.inviteToken.create({
|
|
61
|
+
data: {
|
|
62
|
+
email,
|
|
63
|
+
role,
|
|
64
|
+
workspaceId: workspace.id,
|
|
65
|
+
invitedById: session.user.id,
|
|
66
|
+
expiresAt,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
|
|
71
|
+
const link = `${appUrl}/invite/${invite.token}`;
|
|
72
|
+
|
|
73
|
+
return NextResponse.json({ link, token: invite.token }, { status: 201 });
|
|
74
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { auth } from "@/auth";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { signMcpToken } from "@/lib/mcp-auth";
|
|
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
|
+
const requestedWorkspaceId: string | undefined = body?.workspaceId;
|
|
12
|
+
|
|
13
|
+
let workspaceId: string;
|
|
14
|
+
|
|
15
|
+
if (requestedWorkspaceId) {
|
|
16
|
+
const member = await prisma.workspaceMember.findFirst({
|
|
17
|
+
where: { userId: session.user.id, workspaceId: requestedWorkspaceId, revokedAt: null },
|
|
18
|
+
});
|
|
19
|
+
if (!member) return NextResponse.json({ error: "Not a member of that workspace" }, { status: 403 });
|
|
20
|
+
workspaceId = requestedWorkspaceId;
|
|
21
|
+
} else {
|
|
22
|
+
const member = await prisma.workspaceMember.findFirst({
|
|
23
|
+
where: { userId: session.user.id, revokedAt: null },
|
|
24
|
+
});
|
|
25
|
+
if (!member) return NextResponse.json({ error: "No workspace" }, { status: 404 });
|
|
26
|
+
workspaceId = member.workspaceId;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Revoke any existing active tokens for this user+workspace
|
|
30
|
+
await prisma.mcpToken.updateMany({
|
|
31
|
+
where: { userId: session.user.id, workspaceId, revokedAt: null },
|
|
32
|
+
data: { revokedAt: new Date() },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const record = await prisma.mcpToken.create({
|
|
36
|
+
data: {
|
|
37
|
+
token: "pending",
|
|
38
|
+
userId: session.user.id,
|
|
39
|
+
workspaceId,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const jwt = await signMcpToken({
|
|
44
|
+
userId: session.user.id,
|
|
45
|
+
workspaceId,
|
|
46
|
+
tokenId: record.id,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await prisma.mcpToken.update({
|
|
50
|
+
where: { id: record.id },
|
|
51
|
+
data: { token: jwt },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return NextResponse.json({ id: record.id, token: jwt, alias: null }, { status: 201 });
|
|
55
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { auth } from "@/auth";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
|
|
5
|
+
export async function DELETE(
|
|
6
|
+
_req: NextRequest,
|
|
7
|
+
{ params }: { params: Promise<{ tokenId: string }> }
|
|
8
|
+
) {
|
|
9
|
+
const session = await auth();
|
|
10
|
+
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
11
|
+
|
|
12
|
+
const { tokenId } = await params;
|
|
13
|
+
const record = await prisma.mcpToken.findUnique({ where: { id: tokenId } });
|
|
14
|
+
if (!record || record.revokedAt) {
|
|
15
|
+
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (record.userId !== session.user.id) {
|
|
19
|
+
// Workspace admins and owners may revoke any token in their workspace
|
|
20
|
+
const callerMember = await prisma.workspaceMember.findFirst({
|
|
21
|
+
where: { userId: session.user.id, workspaceId: record.workspaceId, revokedAt: null },
|
|
22
|
+
});
|
|
23
|
+
if (!callerMember || callerMember.role === "MEMBER") {
|
|
24
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await prisma.mcpToken.update({ where: { id: tokenId }, data: { revokedAt: new Date() } });
|
|
29
|
+
return NextResponse.json({ ok: true });
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { auth } from "@/auth";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
|
|
5
|
+
export async function PATCH(
|
|
6
|
+
req: NextRequest,
|
|
7
|
+
{ params }: { params: Promise<{ tokenId: string }> }
|
|
8
|
+
) {
|
|
9
|
+
const session = await auth();
|
|
10
|
+
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
11
|
+
|
|
12
|
+
const { tokenId } = await params;
|
|
13
|
+
const { alias } = await req.json();
|
|
14
|
+
|
|
15
|
+
const record = await prisma.mcpToken.findUnique({ where: { id: tokenId } });
|
|
16
|
+
if (!record || record.userId !== session.user.id) {
|
|
17
|
+
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await prisma.mcpToken.update({ where: { id: tokenId }, data: { alias: alias || null } });
|
|
21
|
+
return NextResponse.json({ ok: true });
|
|
22
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { auth } from "@/auth";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { decryptContent } from "@/lib/note-crypto";
|
|
5
|
+
import { getActiveWorkspaceId } from "@/lib/workspace";
|
|
6
|
+
|
|
7
|
+
// Tolerates both legacy <!--task::STATUS--> and id-bearing <!--task::ID::STATUS-->.
|
|
8
|
+
const BADGE_COMMENT_RE = /\s*<!--task::[^>]*-->/g;
|
|
9
|
+
|
|
10
|
+
function toCleanMarkdown(title: string, content: string): string {
|
|
11
|
+
const lines = content
|
|
12
|
+
.split("\n")
|
|
13
|
+
.map((line) => line.replace(BADGE_COMMENT_RE, ""));
|
|
14
|
+
return `# ${title}\n\n${lines.join("\n")}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function GET(
|
|
18
|
+
_req: NextRequest,
|
|
19
|
+
{ params }: { params: Promise<{ noteId: string }> }
|
|
20
|
+
) {
|
|
21
|
+
const session = await auth();
|
|
22
|
+
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
23
|
+
|
|
24
|
+
const { noteId } = await params;
|
|
25
|
+
|
|
26
|
+
const workspaceId = await getActiveWorkspaceId(session.user.id);
|
|
27
|
+
if (!workspaceId) return NextResponse.json({ error: "No workspace" }, { status: 404 });
|
|
28
|
+
|
|
29
|
+
const note = await prisma.note.findFirst({
|
|
30
|
+
where: { id: noteId, workspaceId },
|
|
31
|
+
});
|
|
32
|
+
if (!note) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
33
|
+
|
|
34
|
+
const plainContent = await decryptContent(note.content, workspaceId);
|
|
35
|
+
const md = toCleanMarkdown(note.title, plainContent);
|
|
36
|
+
const slug = note.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
37
|
+
const filename = `${slug || "note"}.md`;
|
|
38
|
+
|
|
39
|
+
return new NextResponse(md, {
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
42
|
+
"Content-Disposition": `attachment; filename="${filename}"`,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|