@nexttylabs/echo 0.2.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/.changeset/README.md +21 -0
- package/.changeset/config.json +11 -0
- package/.changeset/cozy-ghosts-care.md +5 -0
- package/.changeset/sharp-lines-stand.md +5 -0
- package/.changeset/sour-doodles-eat.md +5 -0
- package/.changeset/tender-moose-shop.md +5 -0
- package/.github/pull_request_template.md +13 -0
- package/.github/workflows/ci.yml +41 -0
- package/.github/workflows/publish.yml +44 -0
- package/.github/workflows/release.yml +73 -0
- package/AGENTS.md +92 -0
- package/CHANGELOG.md +13 -0
- package/Dockerfile +57 -0
- package/LICENSE +661 -0
- package/Makefile +77 -0
- package/README.md +198 -0
- package/app/(auth)/login/page.tsx +53 -0
- package/app/(auth)/register/page.tsx +48 -0
- package/app/(auth)/sign-in/page.tsx +22 -0
- package/app/(dashboard)/admin/feedback/[id]/edit/page.tsx +103 -0
- package/app/(dashboard)/admin/feedback/[id]/page.tsx +154 -0
- package/app/(dashboard)/admin/feedback/new/page.tsx +91 -0
- package/app/(dashboard)/admin/feedback/page.tsx +81 -0
- package/app/(dashboard)/admin/layout.tsx +48 -0
- package/app/(dashboard)/analytics/portal/page.tsx +30 -0
- package/app/(dashboard)/dashboard/page.tsx +133 -0
- package/app/(dashboard)/layout.tsx +69 -0
- package/app/(dashboard)/no-access/page.tsx +45 -0
- package/app/(dashboard)/settings/access/page.tsx +56 -0
- package/app/(dashboard)/settings/api-keys/page.tsx +55 -0
- package/app/(dashboard)/settings/appearance/page.tsx +40 -0
- package/app/(dashboard)/settings/branding/page.tsx +62 -0
- package/app/(dashboard)/settings/changelog/page.tsx +51 -0
- package/app/(dashboard)/settings/danger-zone/page.tsx +92 -0
- package/app/(dashboard)/settings/feedback/page.tsx +63 -0
- package/app/(dashboard)/settings/integrations/page.tsx +94 -0
- package/app/(dashboard)/settings/layout.tsx +43 -0
- package/app/(dashboard)/settings/modules/page.tsx +54 -0
- package/app/(dashboard)/settings/notifications/page.tsx +48 -0
- package/app/(dashboard)/settings/organization/page.tsx +104 -0
- package/app/(dashboard)/settings/organization/portal/access/page.tsx +22 -0
- package/app/(dashboard)/settings/organization/portal/experience/page.tsx +22 -0
- package/app/(dashboard)/settings/organization/portal/growth/page.tsx +22 -0
- package/app/(dashboard)/settings/organization/portal/layout.tsx +24 -0
- package/app/(dashboard)/settings/organization/portal/page.tsx +22 -0
- package/app/(dashboard)/settings/organizations/[orgId]/members/page.tsx +69 -0
- package/app/(dashboard)/settings/organizations/new/page.tsx +36 -0
- package/app/(dashboard)/settings/page.tsx +22 -0
- package/app/(dashboard)/settings/portal-access/page.tsx +53 -0
- package/app/(dashboard)/settings/portal-branding/page.tsx +59 -0
- package/app/(dashboard)/settings/portal-growth/page.tsx +57 -0
- package/app/(dashboard)/settings/portal-modules/page.tsx +49 -0
- package/app/(dashboard)/settings/portal-resources/page.tsx +66 -0
- package/app/(dashboard)/settings/profile/page.tsx +48 -0
- package/app/(dashboard)/settings/widgets/page.tsx +63 -0
- package/app/(public)/[organizationSlug]/changelog/page.tsx +109 -0
- package/app/(public)/[organizationSlug]/feedback/[id]/page.tsx +146 -0
- package/app/(public)/[organizationSlug]/page.tsx +160 -0
- package/app/(public)/[organizationSlug]/roadmap/page.tsx +142 -0
- package/app/(public)/docs/page.tsx +48 -0
- package/app/(public)/feedback/[id]/not-found.tsx +33 -0
- package/app/(public)/feedback/[id]/page.tsx +102 -0
- package/app/(public)/invite/[token]/page.tsx +121 -0
- package/app/(public)/page.tsx +22 -0
- package/app/(public)/widget/[organizationId]/page.tsx +122 -0
- package/app/api/_utils.ts +29 -0
- package/app/api/admin/backup/route.ts +72 -0
- package/app/api/api-keys/[keyId]/route.ts +92 -0
- package/app/api/api-keys/route.ts +116 -0
- package/app/api/auth/[...all]/route.ts +21 -0
- package/app/api/auth/clear-session/route.ts +43 -0
- package/app/api/auth/register/handler.ts +176 -0
- package/app/api/auth/register/route.ts +26 -0
- package/app/api/docs/route.ts +28 -0
- package/app/api/feedback/[id]/comments/[commentId]/route.ts +105 -0
- package/app/api/feedback/[id]/comments/route.ts +421 -0
- package/app/api/feedback/[id]/duplicates/route.ts +285 -0
- package/app/api/feedback/[id]/handler.ts +91 -0
- package/app/api/feedback/[id]/processing-status/route.ts +199 -0
- package/app/api/feedback/[id]/reclassify/route.ts +145 -0
- package/app/api/feedback/[id]/route.ts +511 -0
- package/app/api/feedback/[id]/suggest-tags/route.ts +227 -0
- package/app/api/feedback/[id]/sync-github/route.ts +52 -0
- package/app/api/feedback/[id]/vote/route.ts +431 -0
- package/app/api/feedback/bulk/route.ts +212 -0
- package/app/api/feedback/handler.ts +138 -0
- package/app/api/feedback/route.ts +298 -0
- package/app/api/feedback/similar/route.ts +100 -0
- package/app/api/health/route.test.ts +64 -0
- package/app/api/health/route.ts +92 -0
- package/app/api/identify/jwt/route.ts +29 -0
- package/app/api/integrations/github/route.ts +196 -0
- package/app/api/internal/domain-lookup/route.ts +67 -0
- package/app/api/invitations/accept/handler.ts +101 -0
- package/app/api/invitations/accept/route.ts +29 -0
- package/app/api/notifications/preferences/route.ts +109 -0
- package/app/api/organizations/[orgId]/handler.ts +123 -0
- package/app/api/organizations/[orgId]/invitations/handler.ts +121 -0
- package/app/api/organizations/[orgId]/invitations/route.ts +29 -0
- package/app/api/organizations/[orgId]/members/[memberId]/handler.ts +208 -0
- package/app/api/organizations/[orgId]/members/[memberId]/route.ts +30 -0
- package/app/api/organizations/[orgId]/members/handler.ts +77 -0
- package/app/api/organizations/[orgId]/members/route.ts +29 -0
- package/app/api/organizations/[orgId]/route.ts +30 -0
- package/app/api/organizations/handler.ts +97 -0
- package/app/api/organizations/route.ts +29 -0
- package/app/api/tags/sync/route.ts +88 -0
- package/app/api/upload/handler.ts +79 -0
- package/app/api/upload/route.ts +37 -0
- package/app/api/v1/feedback/[id]/route.ts +276 -0
- package/app/api/v1/feedback/route.ts +250 -0
- package/app/api/v1/spec/route.ts +356 -0
- package/app/api/webhooks/[webhookId]/route.ts +213 -0
- package/app/api/webhooks/github/route.ts +158 -0
- package/app/api/webhooks/route.ts +143 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +139 -0
- package/app/health/route.ts +108 -0
- package/app/layout.tsx +60 -0
- package/bun.lock +2503 -0
- package/components/api/rate-limit-info.tsx +86 -0
- package/components/api-keys/api-key-manager.tsx +262 -0
- package/components/auth/login-form.tsx +207 -0
- package/components/auth/register-form.tsx +230 -0
- package/components/comment/comment-form.tsx +111 -0
- package/components/comment/internal-notes.tsx +219 -0
- package/components/comment/public-comments.tsx +387 -0
- package/components/component-example-client-only.tsx +29 -0
- package/components/component-example.tsx +519 -0
- package/components/dashboard/index.ts +22 -0
- package/components/dashboard/organization-switcher.tsx +96 -0
- package/components/dashboard/quick-actions.tsx +57 -0
- package/components/dashboard/recent-feedback-list.tsx +152 -0
- package/components/dashboard/stats-cards.tsx +88 -0
- package/components/dashboard/status-chart.tsx +106 -0
- package/components/example.tsx +70 -0
- package/components/feedback/attachment-list.tsx +103 -0
- package/components/feedback/auto-classification-badge.tsx +92 -0
- package/components/feedback/classification-override.tsx +64 -0
- package/components/feedback/duplicate-suggestions-inline.tsx +158 -0
- package/components/feedback/duplicate-suggestions.tsx +188 -0
- package/components/feedback/embedded-feedback-form.tsx +439 -0
- package/components/feedback/feedback-actions.tsx +160 -0
- package/components/feedback/feedback-bulk-actions.tsx +184 -0
- package/components/feedback/feedback-detail-view.tsx +321 -0
- package/components/feedback/feedback-detail.tsx +305 -0
- package/components/feedback/feedback-edit-form.tsx +131 -0
- package/components/feedback/feedback-filters.tsx +222 -0
- package/components/feedback/feedback-list-controls.tsx +433 -0
- package/components/feedback/feedback-list-item.tsx +298 -0
- package/components/feedback/feedback-list-skeleton.tsx +49 -0
- package/components/feedback/feedback-list.tsx +523 -0
- package/components/feedback/feedback-sorter.tsx +117 -0
- package/components/feedback/feedback-stats.tsx +124 -0
- package/components/feedback/file-upload.tsx +289 -0
- package/components/feedback/processing-status.tsx +161 -0
- package/components/feedback/status-history.tsx +134 -0
- package/components/feedback/status-selector.tsx +153 -0
- package/components/feedback/submit-on-behalf-form.tsx +403 -0
- package/components/feedback/tag-suggestions.tsx +212 -0
- package/components/feedback/vote-button.tsx +113 -0
- package/components/feedback/vote-list.tsx +108 -0
- package/components/integrations/github-config.tsx +200 -0
- package/components/landing/hero.tsx +150 -0
- package/components/layout/dashboard-layout.tsx +59 -0
- package/components/layout/index.ts +20 -0
- package/components/layout/language-switcher.tsx +129 -0
- package/components/layout/mobile-sidebar.tsx +66 -0
- package/components/layout/sidebar.tsx +279 -0
- package/components/portal/changelog-entry.tsx +132 -0
- package/components/portal/changelog-list.tsx +85 -0
- package/components/portal/contributor-badge.tsx +29 -0
- package/components/portal/contributors-sidebar.tsx +98 -0
- package/components/portal/create-post-dialog.tsx +247 -0
- package/components/portal/feedback-board.tsx +205 -0
- package/components/portal/feedback-post-card.tsx +198 -0
- package/components/portal/help-center.tsx +169 -0
- package/components/portal/leaderboard.tsx +29 -0
- package/components/portal/portal-header.tsx +153 -0
- package/components/portal/portal-layout.tsx +62 -0
- package/components/portal/portal-modules-panel.tsx +118 -0
- package/components/portal/portal-nav.tsx +59 -0
- package/components/portal/portal-overview.tsx +174 -0
- package/components/portal/portal-settings-nav.tsx +62 -0
- package/components/portal/portal-settings-shell.tsx +71 -0
- package/components/portal/portal-shell.tsx +62 -0
- package/components/portal/portal-tab-nav.tsx +77 -0
- package/components/portal/project-switcher.tsx +20 -0
- package/components/portal/roadmap-board.tsx +82 -0
- package/components/portal/roadmap-card.tsx +76 -0
- package/components/portal/roadmap-column.tsx +78 -0
- package/components/portal/settings-forms/access-form.tsx +194 -0
- package/components/portal/settings-forms/copy-form.tsx +95 -0
- package/components/portal/settings-forms/index.ts +23 -0
- package/components/portal/settings-forms/languages-form.tsx +223 -0
- package/components/portal/settings-forms/seo-form.tsx +156 -0
- package/components/portal/settings-forms/sharing-form.tsx +155 -0
- package/components/portal/settings-forms/theme-form.tsx +104 -0
- package/components/settings/api-keys-list.tsx +167 -0
- package/components/settings/appearance-form.tsx +71 -0
- package/components/settings/index.ts +25 -0
- package/components/settings/invite-member-form.tsx +119 -0
- package/components/settings/notification-preferences.tsx +174 -0
- package/components/settings/organization-form.tsx +165 -0
- package/components/settings/organization-members-list.tsx +197 -0
- package/components/settings/profile-form.tsx +124 -0
- package/components/settings/role-selector.tsx +57 -0
- package/components/settings/settings-sidebar.tsx +115 -0
- package/components/shared/pagination.tsx +215 -0
- package/components/ui/alert-dialog.tsx +201 -0
- package/components/ui/alert.tsx +75 -0
- package/components/ui/avatar.tsx +126 -0
- package/components/ui/badge.tsx +62 -0
- package/components/ui/button.tsx +77 -0
- package/components/ui/card.tsx +111 -0
- package/components/ui/combobox.tsx +311 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +272 -0
- package/components/ui/field.tsx +256 -0
- package/components/ui/input-group.tsx +164 -0
- package/components/ui/input.tsx +36 -0
- package/components/ui/label.tsx +41 -0
- package/components/ui/pagination.tsx +142 -0
- package/components/ui/select.tsx +202 -0
- package/components/ui/separator.tsx +45 -0
- package/components/ui/sheet.tsx +151 -0
- package/components/ui/skeleton.tsx +32 -0
- package/components/ui/switch.tsx +49 -0
- package/components/ui/table.tsx +118 -0
- package/components/ui/tabs.tsx +107 -0
- package/components/ui/textarea.tsx +35 -0
- package/components/ui/tooltip.tsx +78 -0
- package/components/widget/widget-form.tsx +439 -0
- package/components.json +24 -0
- package/db/init/01-init.sql +13 -0
- package/docker-compose.dev.yml +26 -0
- package/docker-compose.yml +98 -0
- package/docs/architecture.md +259 -0
- package/docs/component-inventory.md +261 -0
- package/docs/database-migrations.md +76 -0
- package/docs/development-guide.md +209 -0
- package/docs/e2e-user-flows.csv +31 -0
- package/docs/er-diagram-feedback.mmd +138 -0
- package/docs/er-diagram.mmd +281 -0
- package/docs/i18n-check-report.md +296 -0
- package/docs/index.md +214 -0
- package/docs/logic-chain.md +94 -0
- package/docs/plans/2026-01-02-database-migration-scripts.md +496 -0
- package/docs/plans/2026-01-02-user-login-design.md +37 -0
- package/docs/plans/2026-01-02-user-login.md +437 -0
- package/docs/plans/2026-01-02-user-registration-design.md +47 -0
- package/docs/plans/2026-01-02-user-registration.md +628 -0
- package/docs/plans/2026-01-03-roles-permissions-design.md +20 -0
- package/docs/plans/2026-01-03-roles-permissions.md +266 -0
- package/docs/plans/2026-01-05-authentication-middleware.md +207 -0
- package/docs/plans/2026-01-05-member-removal.md +186 -0
- package/docs/plans/2026-01-05-organization-creation.md +374 -0
- package/docs/plans/2026-01-05-rbac-middleware.md +112 -0
- package/docs/plans/2026-01-05-role-configuration.md +441 -0
- package/docs/plans/2026-01-06-file-upload-support.md +804 -0
- package/docs/plans/2026-01-06-permission-check-hook.md +155 -0
- package/docs/plans/2026-01-06-resource-ownership-check.md +231 -0
- package/docs/plans/2026-01-07-feedback-tracking-link.md +459 -0
- package/docs/plans/2026-01-09-logout-redirect-design.md +52 -0
- package/docs/plans/2026-01-09-phase2-3-plan.md +654 -0
- package/docs/plans/2026-01-09-portal-execution-plan.md +408 -0
- package/docs/plans/2026-01-09-project-delete-feature-design.md +163 -0
- package/docs/plans/2026-01-09-project-delete-implementation.md +451 -0
- package/docs/plans/2026-01-09-project-edit-delete-design.md +52 -0
- package/docs/plans/2026-01-09-settings-center-design.md +114 -0
- package/docs/plans/2026-01-09-settings-center.md +948 -0
- package/docs/plans/2026-01-10-organization-only-design.md +66 -0
- package/docs/plans/2026-01-10-organization-only-implementation.md +433 -0
- package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +18 -0
- package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +296 -0
- package/docs/plans/2026-01-14-e2e-playwright-feedback.md +173 -0
- package/docs/plans/2026-01-15-feedback-management-org-context-design.md +82 -0
- package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +521 -0
- package/docs/plans/2026-01-16-admin-feedback-filters-design.md +75 -0
- package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +293 -0
- package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +180 -0
- package/docs/plans/2026-01-16-e2e-test-fixes.md +158 -0
- package/docs/plans/2026-01-17-admin-feedback-filters.md +214 -0
- package/docs/plans/2026-01-17-admin-feedback-improvements.md +453 -0
- package/docs/plans/2026-01-18-changesets-design.md +40 -0
- package/docs/product_changes.md +37 -0
- package/docs/project-overview.md +159 -0
- package/docs/project-scan-report.json +104 -0
- package/docs/route-role-visibility.md +51 -0
- package/docs/source-tree-analysis.md +150 -0
- package/docs/testing/delete-project-manual-tests.md +18 -0
- package/docs/user-story-tracking.md +191 -0
- package/drizzle.config.ts +32 -0
- package/eslint.config.mjs +19 -0
- package/hooks/use-permissions.ts +56 -0
- package/i18n/config.ts +45 -0
- package/i18n/request.ts +28 -0
- package/i18n/resolve-locale.ts +38 -0
- package/lib/api/errors.ts +62 -0
- package/lib/auth/cli-config.ts +35 -0
- package/lib/auth/client.ts +20 -0
- package/lib/auth/config.ts +55 -0
- package/lib/auth/jwt-identity.ts +21 -0
- package/lib/auth/org-context.ts +71 -0
- package/lib/auth/organization.ts +107 -0
- package/lib/auth/permissions.ts +87 -0
- package/lib/auth/session.ts +23 -0
- package/lib/config/rate-limits.ts +64 -0
- package/lib/dashboard/get-dashboard-stats.ts +136 -0
- package/lib/db/index.ts +41 -0
- package/lib/db/migrate.test.ts +49 -0
- package/lib/db/migrate.ts +62 -0
- package/lib/db/migrations/.gitkeep +0 -0
- package/lib/db/migrations/0000_cynical_gladiator.sql +53 -0
- package/lib/db/migrations/0001_wandering_sunfire.sql +27 -0
- package/lib/db/migrations/0002_shallow_speedball.sql +1 -0
- package/lib/db/migrations/0003_add_org_description.sql +1 -0
- package/lib/db/migrations/0003_boring_wild_pack.sql +13 -0
- package/lib/db/migrations/0004_windy_tyrannus.sql +27 -0
- package/lib/db/migrations/0005_perpetual_doorman.sql +5 -0
- package/lib/db/migrations/0006_aberrant_captain_midlands.sql +13 -0
- package/lib/db/migrations/0007_clever_captain_cross.sql +14 -0
- package/lib/db/migrations/0008_sparkling_pandemic.sql +2 -0
- package/lib/db/migrations/0009_happy_black_tom.sql +29 -0
- package/lib/db/migrations/0010_kind_junta.sql +8 -0
- package/lib/db/migrations/0011_mute_squadron_supreme.sql +25 -0
- package/lib/db/migrations/0012_giant_power_man.sql +24 -0
- package/lib/db/migrations/0013_damp_titanium_man.sql +17 -0
- package/lib/db/migrations/0014_blue_alice.sql +18 -0
- package/lib/db/migrations/0015_webhook_tables.sql +41 -0
- package/lib/db/migrations/0016_github_integration.sql +30 -0
- package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +22 -0
- package/lib/db/migrations/0017_slimy_inhumans.sql +6 -0
- package/lib/db/migrations/0018_same_spitfire.sql +1 -0
- package/lib/db/migrations/0019_jittery_loners.sql +16 -0
- package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +14 -0
- package/lib/db/migrations/meta/0000_snapshot.json +374 -0
- package/lib/db/migrations/meta/0001_snapshot.json +553 -0
- package/lib/db/migrations/meta/0002_snapshot.json +560 -0
- package/lib/db/migrations/meta/0003_snapshot.json +650 -0
- package/lib/db/migrations/meta/0004_snapshot.json +852 -0
- package/lib/db/migrations/meta/0005_snapshot.json +900 -0
- package/lib/db/migrations/meta/0006_snapshot.json +1011 -0
- package/lib/db/migrations/meta/0007_snapshot.json +1125 -0
- package/lib/db/migrations/meta/0008_snapshot.json +1146 -0
- package/lib/db/migrations/meta/0009_snapshot.json +1386 -0
- package/lib/db/migrations/meta/0010_snapshot.json +1419 -0
- package/lib/db/migrations/meta/0011_snapshot.json +1615 -0
- package/lib/db/migrations/meta/0012_snapshot.json +1805 -0
- package/lib/db/migrations/meta/0013_snapshot.json +1948 -0
- package/lib/db/migrations/meta/0014_snapshot.json +2082 -0
- package/lib/db/migrations/meta/0015_snapshot.json +2476 -0
- package/lib/db/migrations/meta/0016_snapshot.json +2633 -0
- package/lib/db/migrations/meta/0017_snapshot.json +2680 -0
- package/lib/db/migrations/meta/0018_snapshot.json +2686 -0
- package/lib/db/migrations/meta/0019_snapshot.json +2741 -0
- package/lib/db/migrations/meta/_journal.json +146 -0
- package/lib/db/schema/ai-processing.ts +90 -0
- package/lib/db/schema/api-keys.ts +61 -0
- package/lib/db/schema/attachments.ts +48 -0
- package/lib/db/schema/auth.ts +111 -0
- package/lib/db/schema/comments.ts +74 -0
- package/lib/db/schema/duplicates.ts +80 -0
- package/lib/db/schema/feedback.ts +88 -0
- package/lib/db/schema/github-integrations.ts +66 -0
- package/lib/db/schema/index.ts +35 -0
- package/lib/db/schema/invitations.ts +32 -0
- package/lib/db/schema/notifications.ts +85 -0
- package/lib/db/schema/organization-members.ts +37 -0
- package/lib/db/schema/organization-settings.ts +134 -0
- package/lib/db/schema/organizations.ts +30 -0
- package/lib/db/schema/projects.ts +145 -0
- package/lib/db/schema/status-history.ts +63 -0
- package/lib/db/schema/tags.ts +194 -0
- package/lib/db/schema/user-profiles.ts +31 -0
- package/lib/db/schema/votes.ts +60 -0
- package/lib/db/schema/webhooks.ts +106 -0
- package/lib/feedback/filters.ts +28 -0
- package/lib/feedback/find-similar.ts +49 -0
- package/lib/feedback/get-feedback-by-id.ts +159 -0
- package/lib/feedback/prefill.ts +51 -0
- package/lib/http/get-request-url.ts +28 -0
- package/lib/integrations/github.ts +159 -0
- package/lib/invitations.ts +22 -0
- package/lib/logger.test.ts +31 -0
- package/lib/logger.ts +58 -0
- package/lib/middleware/api-key.ts +126 -0
- package/lib/middleware/rate-limit-keys.ts +47 -0
- package/lib/middleware/rate-limit.ts +148 -0
- package/lib/middleware/rbac.ts +39 -0
- package/lib/middleware/request-id.test.ts +28 -0
- package/lib/middleware/request-id.ts +30 -0
- package/lib/middleware/request-logger.test.ts +36 -0
- package/lib/middleware/request-logger.ts +41 -0
- package/lib/middleware/with-rate-limit.ts +33 -0
- package/lib/portal/analytics.ts +20 -0
- package/lib/portal/contributors.ts +27 -0
- package/lib/portal/i18n.ts +20 -0
- package/lib/portal/leaderboard-settings.ts +20 -0
- package/lib/portal/modules.ts +20 -0
- package/lib/portal/portal-copy.ts +20 -0
- package/lib/portal/public-context.tsx +110 -0
- package/lib/portal/seo.ts +20 -0
- package/lib/portal/settings-context.ts +56 -0
- package/lib/portal/sharing.ts +20 -0
- package/lib/portal/sorting.ts +20 -0
- package/lib/portal/theme.ts +20 -0
- package/lib/services/ai/classifier.ts +296 -0
- package/lib/services/ai/duplicate-detector.ts +255 -0
- package/lib/services/ai/tag-suggester.ts +108 -0
- package/lib/services/api-keys.ts +164 -0
- package/lib/services/backup.ts +173 -0
- package/lib/services/email/templates.ts +158 -0
- package/lib/services/email.ts +68 -0
- package/lib/services/github-sync.ts +205 -0
- package/lib/services/notifications/index.ts +224 -0
- package/lib/services/portal-settings.ts +157 -0
- package/lib/swagger/config.ts +296 -0
- package/lib/swagger/generate.ts +400 -0
- package/lib/upload/file-validator.ts +52 -0
- package/lib/upload/storage.ts +59 -0
- package/lib/utils/format.ts +26 -0
- package/lib/utils/slug.ts +28 -0
- package/lib/utils.ts +23 -0
- package/lib/validations/auth.ts +56 -0
- package/lib/validations/comment.ts +44 -0
- package/lib/validations/feedback.ts +51 -0
- package/lib/validations/invitations.ts +23 -0
- package/lib/validations/organizations.ts +34 -0
- package/lib/validations/projects.ts +49 -0
- package/lib/validators/feedback.ts +57 -0
- package/lib/validators/index.ts +18 -0
- package/lib/webhooks/events.ts +73 -0
- package/lib/webhooks/index.ts +21 -0
- package/lib/webhooks/retry.ts +188 -0
- package/lib/webhooks/sender.ts +183 -0
- package/lib/webhooks/verify.ts +37 -0
- package/lib/workers/feedback-processor.ts +255 -0
- package/messages/en.json +965 -0
- package/messages/jp.json +862 -0
- package/messages/zh-CN.json +855 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +66 -0
- package/package.json +84 -0
- package/playwright.config.ts +44 -0
- package/postcss.config.mjs +7 -0
- package/proxy.test.ts +131 -0
- package/proxy.ts +190 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo-64.svg +5 -0
- package/public/logo.svg +5 -0
- package/public/next.svg +1 -0
- package/public/openapi.json +673 -0
- package/public/uploads/.gitkeep +0 -0
- package/public/uploads/02695701-ded0-4c81-8a21-9326c1d65448.pdf +1 -0
- package/public/uploads/178843ea-2780-48ef-8988-f4cba442e4cb.pdf +1 -0
- package/public/uploads/24b0a9ef-da93-49da-934f-637f89c7871d.pdf +1 -0
- package/public/uploads/7a11626d-a8e4-4b91-a8eb-20b6213b0a5a.pdf +1 -0
- package/public/uploads/b0703f4d-6e7b-4aab-8191-1a7b15f1b8ee.pdf +1 -0
- package/public/uploads/c8de0aed-4d3a-44aa-83bb-6594b7a2ddb3.pdf +1 -0
- package/public/uploads/e4cce295-0d85-4525-a1b0-a61c45722e26.pdf +1 -0
- package/public/uploads/eb4df45e-563c-48b8-9c68-c18212312426.pdf +1 -0
- package/public/vercel.svg +1 -0
- package/public/widget/embed.js +249 -0
- package/public/window.svg +1 -0
- package/scripts/backup-db.sh +57 -0
- package/scripts/backup-db.ts +24 -0
- package/scripts/generate-openapi.ts +22 -0
- package/scripts/migration-helper.ts +39 -0
- package/scripts/pre-deploy.ts +75 -0
- package/scripts/restore-db.sh +60 -0
- package/scripts/rollback.ts +72 -0
- package/scripts/seed-tags.ts +48 -0
- package/tests/api/feedback-bulk.test.ts +47 -0
- package/tests/api/feedback-by-id.test.ts +67 -0
- package/tests/api/feedback-comments-route-import.test.ts +26 -0
- package/tests/api/feedback-create.test.ts +71 -0
- package/tests/api/feedback-delete.test.ts +160 -0
- package/tests/api/feedback-filter.test.ts +250 -0
- package/tests/api/feedback-list.test.ts +234 -0
- package/tests/api/feedback-route-assignee-condition.test.ts +32 -0
- package/tests/api/feedback-similar.test.ts +46 -0
- package/tests/api/feedback-sort.test.ts +261 -0
- package/tests/api/feedback-status-enum.test.ts +49 -0
- package/tests/api/feedback-status-filter.test.ts +117 -0
- package/tests/api/feedback-submit-on-behalf.test.ts +269 -0
- package/tests/api/feedback.test.ts +175 -0
- package/tests/api/identify-jwt.test.ts +25 -0
- package/tests/api/invitation-accept.test.ts +213 -0
- package/tests/api/organization-invitations.test.ts +186 -0
- package/tests/api/organization-members-list.test.ts +79 -0
- package/tests/api/organization-members.test.ts +340 -0
- package/tests/api/organizations.test.ts +149 -0
- package/tests/api/register.test.ts +112 -0
- package/tests/api/upload.test.ts +103 -0
- package/tests/api/vote.test.ts +82 -0
- package/tests/app/admin-feedback-detail-page.test.tsx +25 -0
- package/tests/app/admin-feedback-list-page.test.tsx +25 -0
- package/tests/app/admin-feedback-new-page.test.tsx +25 -0
- package/tests/app/health-route-helpers.test.ts +27 -0
- package/tests/app/login-page.test.ts +26 -0
- package/tests/app/portal-page.test.ts +29 -0
- package/tests/app/project-portal-overview.test.tsx +25 -0
- package/tests/app/widget-page-import.test.ts +25 -0
- package/tests/components/create-post-dialog-defaults.test.ts +43 -0
- package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +27 -0
- package/tests/components/feedback/embedded-feedback-form.test.tsx +96 -0
- package/tests/components/feedback/feedback-detail.test.tsx +25 -0
- package/tests/components/feedback/feedback-stats.test.tsx +49 -0
- package/tests/components/feedback-bulk-actions.test.tsx +39 -0
- package/tests/components/feedback-i18n-keys.test.ts +70 -0
- package/tests/components/feedback-list-controls-compile.test.ts +25 -0
- package/tests/components/feedback-list-controls.test.tsx +204 -0
- package/tests/components/feedback-list-item.test.tsx +67 -0
- package/tests/components/landing/hero.test.tsx +46 -0
- package/tests/components/layout/language-switcher.test.tsx +25 -0
- package/tests/components/layout/sidebar.test.tsx +157 -0
- package/tests/components/login-form.test.ts +25 -0
- package/tests/components/organization-form.test.ts +32 -0
- package/tests/components/organization-switcher.test.ts +25 -0
- package/tests/components/pagination.test.tsx +43 -0
- package/tests/components/portal-overview.test.tsx +25 -0
- package/tests/components/profile-form.test.tsx +139 -0
- package/tests/components/role-selector.test.ts +31 -0
- package/tests/components/status-chart.test.tsx +90 -0
- package/tests/e2e/auth.e2e.ts +323 -0
- package/tests/e2e/feedback-actions.e2e.ts +471 -0
- package/tests/e2e/feedback-attachment.e2e.ts +168 -0
- package/tests/e2e/feedback-customer.e2e.ts +226 -0
- package/tests/e2e/feedback-management.e2e.ts +565 -0
- package/tests/e2e/feedback-submit.e2e.ts +133 -0
- package/tests/e2e/feedback-view.e2e.ts +297 -0
- package/tests/e2e/fixtures/test-data.ts +235 -0
- package/tests/e2e/health-check.e2e.ts +230 -0
- package/tests/e2e/helpers/test-utils-helpers.test.ts +43 -0
- package/tests/e2e/helpers/test-utils.ts +298 -0
- package/tests/e2e/integration-placeholders.e2e.ts +199 -0
- package/tests/e2e/organization.e2e.ts +292 -0
- package/tests/e2e/permissions.e2e.ts +424 -0
- package/tests/e2e/project-widget.e2e.ts +63 -0
- package/tests/feedback/filters.test.ts +29 -0
- package/tests/hooks/use-permissions.test.ts +52 -0
- package/tests/lib/ai/classifier.test.ts +104 -0
- package/tests/lib/ai/duplicate-detector.test.ts +234 -0
- package/tests/lib/attachments-schema.test.ts +30 -0
- package/tests/lib/auth/session.test.ts +49 -0
- package/tests/lib/auth-client.test.ts +37 -0
- package/tests/lib/auth-config.test.ts +26 -0
- package/tests/lib/feedback-prefill.test.ts +52 -0
- package/tests/lib/feedback-processor.test.ts +41 -0
- package/tests/lib/feedback-schema.test.ts +33 -0
- package/tests/lib/file-validator.test.ts +48 -0
- package/tests/lib/get-feedback-by-id.test.ts +37 -0
- package/tests/lib/invitations.test.ts +35 -0
- package/tests/lib/login-schema.test.ts +36 -0
- package/tests/lib/org-context.test.ts +95 -0
- package/tests/lib/organization-access.test.ts +44 -0
- package/tests/lib/organization-member-role-schema.test.ts +41 -0
- package/tests/lib/permissions.test.ts +88 -0
- package/tests/lib/portal-analytics.test.ts +25 -0
- package/tests/lib/portal-contributors.test.ts +25 -0
- package/tests/lib/portal-copy.test.ts +27 -0
- package/tests/lib/portal-i18n.test.ts +30 -0
- package/tests/lib/portal-leaderboard-settings.test.ts +25 -0
- package/tests/lib/portal-modules.test.ts +25 -0
- package/tests/lib/portal-seo.test.ts +25 -0
- package/tests/lib/portal-sharing.test.ts +25 -0
- package/tests/lib/portal-sorting.test.ts +25 -0
- package/tests/lib/portal-theme.test.ts +25 -0
- package/tests/lib/rate-limit.test.ts +142 -0
- package/tests/lib/resolve-locale.test.ts +34 -0
- package/tests/lib/services/backup.test.ts +145 -0
- package/tests/lib/user-organizations.test.ts +42 -0
- package/tests/lib/user-role-schema.test.ts +33 -0
- package/tests/lib/user-schema.test.ts +25 -0
- package/tests/setup.ts +74 -0
- package/tsconfig.json +34 -0
- package/types/bun-test.d.ts +31 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# Roles and Permissions Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add a minimal RBAC module with a `customer` role and keep user schema and validation aligned with the role model.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Implement a pure RBAC helper module in `lib/auth/permissions.ts`, update Drizzle `user` table schema to include a `role` column with default `customer`, and add a Zod role schema in `lib/validations/auth.ts`. Cover behavior with focused bun tests.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Next.js, TypeScript, Bun test runner, Drizzle ORM, Zod.
|
|
10
|
+
|
|
11
|
+
### Task 1: Permissions tests
|
|
12
|
+
|
|
13
|
+
**Files:**
|
|
14
|
+
- Create: `tests/lib/permissions.test.ts`
|
|
15
|
+
|
|
16
|
+
**Step 1: Write the failing test**
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { describe, expect, it } from "bun:test";
|
|
20
|
+
import {
|
|
21
|
+
PERMISSIONS,
|
|
22
|
+
ROLE_PERMISSIONS,
|
|
23
|
+
hasPermission,
|
|
24
|
+
canSubmitOnBehalf,
|
|
25
|
+
} from "@/lib/auth/permissions";
|
|
26
|
+
|
|
27
|
+
describe("permissions", () => {
|
|
28
|
+
it("maps roles to expected permissions", () => {
|
|
29
|
+
expect(ROLE_PERMISSIONS.admin).toEqual(
|
|
30
|
+
expect.arrayContaining([
|
|
31
|
+
PERMISSIONS.CREATE_FEEDBACK,
|
|
32
|
+
PERMISSIONS.SUBMIT_ON_BEHALF,
|
|
33
|
+
PERMISSIONS.DELETE_FEEDBACK,
|
|
34
|
+
PERMISSIONS.MANAGE_ORG,
|
|
35
|
+
]),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(ROLE_PERMISSIONS.product_manager).toEqual(
|
|
39
|
+
expect.arrayContaining([
|
|
40
|
+
PERMISSIONS.CREATE_FEEDBACK,
|
|
41
|
+
PERMISSIONS.SUBMIT_ON_BEHALF,
|
|
42
|
+
PERMISSIONS.DELETE_FEEDBACK,
|
|
43
|
+
]),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(ROLE_PERMISSIONS.developer).toEqual(
|
|
47
|
+
expect.arrayContaining([PERMISSIONS.CREATE_FEEDBACK]),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(ROLE_PERMISSIONS.customer_support).toEqual(
|
|
51
|
+
expect.arrayContaining([
|
|
52
|
+
PERMISSIONS.CREATE_FEEDBACK,
|
|
53
|
+
PERMISSIONS.SUBMIT_ON_BEHALF,
|
|
54
|
+
]),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(ROLE_PERMISSIONS.customer).toEqual(
|
|
58
|
+
expect.arrayContaining([PERMISSIONS.CREATE_FEEDBACK]),
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("checks permissions by role", () => {
|
|
63
|
+
expect(hasPermission("admin", PERMISSIONS.MANAGE_ORG)).toBe(true);
|
|
64
|
+
expect(hasPermission("developer", PERMISSIONS.DELETE_FEEDBACK)).toBe(false);
|
|
65
|
+
expect(hasPermission("customer", PERMISSIONS.CREATE_FEEDBACK)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("checks submit-on-behalf permission", () => {
|
|
69
|
+
expect(canSubmitOnBehalf("customer_support")).toBe(true);
|
|
70
|
+
expect(canSubmitOnBehalf("customer")).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Step 2: Run test to verify it fails**
|
|
76
|
+
|
|
77
|
+
Run: `bun test tests/lib/permissions.test.ts`
|
|
78
|
+
Expected: FAIL because `@/lib/auth/permissions` does not exist.
|
|
79
|
+
|
|
80
|
+
**Step 3: Commit**
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git add tests/lib/permissions.test.ts
|
|
84
|
+
git commit -m "test: add permissions coverage"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Task 2: Implement permissions module
|
|
88
|
+
|
|
89
|
+
**Files:**
|
|
90
|
+
- Create: `lib/auth/permissions.ts`
|
|
91
|
+
|
|
92
|
+
**Step 1: Write minimal implementation**
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
export type UserRole =
|
|
96
|
+
| "admin"
|
|
97
|
+
| "product_manager"
|
|
98
|
+
| "developer"
|
|
99
|
+
| "customer_support"
|
|
100
|
+
| "customer";
|
|
101
|
+
|
|
102
|
+
export const PERMISSIONS = {
|
|
103
|
+
CREATE_FEEDBACK: "create_feedback",
|
|
104
|
+
SUBMIT_ON_BEHALF: "submit_on_behalf",
|
|
105
|
+
DELETE_FEEDBACK: "delete_feedback",
|
|
106
|
+
MANAGE_ORG: "manage_org",
|
|
107
|
+
} as const;
|
|
108
|
+
|
|
109
|
+
export type Permission = (typeof PERMISSIONS)[keyof typeof PERMISSIONS];
|
|
110
|
+
|
|
111
|
+
export const ROLE_PERMISSIONS: Record<UserRole, Permission[]> = {
|
|
112
|
+
admin: [
|
|
113
|
+
PERMISSIONS.CREATE_FEEDBACK,
|
|
114
|
+
PERMISSIONS.SUBMIT_ON_BEHALF,
|
|
115
|
+
PERMISSIONS.DELETE_FEEDBACK,
|
|
116
|
+
PERMISSIONS.MANAGE_ORG,
|
|
117
|
+
],
|
|
118
|
+
product_manager: [
|
|
119
|
+
PERMISSIONS.CREATE_FEEDBACK,
|
|
120
|
+
PERMISSIONS.SUBMIT_ON_BEHALF,
|
|
121
|
+
PERMISSIONS.DELETE_FEEDBACK,
|
|
122
|
+
],
|
|
123
|
+
developer: [PERMISSIONS.CREATE_FEEDBACK],
|
|
124
|
+
customer_support: [
|
|
125
|
+
PERMISSIONS.CREATE_FEEDBACK,
|
|
126
|
+
PERMISSIONS.SUBMIT_ON_BEHALF,
|
|
127
|
+
],
|
|
128
|
+
customer: [PERMISSIONS.CREATE_FEEDBACK],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export function hasPermission(role: UserRole, permission: Permission): boolean {
|
|
132
|
+
return ROLE_PERMISSIONS[role]?.includes(permission) ?? false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function canSubmitOnBehalf(role: UserRole): boolean {
|
|
136
|
+
return hasPermission(role, PERMISSIONS.SUBMIT_ON_BEHALF);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Step 2: Run test to verify it passes**
|
|
141
|
+
|
|
142
|
+
Run: `bun test tests/lib/permissions.test.ts`
|
|
143
|
+
Expected: PASS
|
|
144
|
+
|
|
145
|
+
**Step 3: Commit**
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
git add lib/auth/permissions.ts
|
|
149
|
+
git commit -m "feat: add rbac permissions module"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Task 3: Role validation
|
|
153
|
+
|
|
154
|
+
**Files:**
|
|
155
|
+
- Create: `tests/lib/user-role-schema.test.ts`
|
|
156
|
+
- Modify: `lib/validations/auth.ts`
|
|
157
|
+
|
|
158
|
+
**Step 1: Write the failing test**
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { describe, expect, it } from "bun:test";
|
|
162
|
+
import { userRoleSchema } from "@/lib/validations/auth";
|
|
163
|
+
|
|
164
|
+
describe("userRoleSchema", () => {
|
|
165
|
+
it("accepts known roles", () => {
|
|
166
|
+
expect(userRoleSchema.safeParse("customer").success).toBe(true);
|
|
167
|
+
expect(userRoleSchema.safeParse("admin").success).toBe(true);
|
|
168
|
+
expect(userRoleSchema.safeParse("product_manager").success).toBe(true);
|
|
169
|
+
expect(userRoleSchema.safeParse("developer").success).toBe(true);
|
|
170
|
+
expect(userRoleSchema.safeParse("customer_support").success).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("rejects unknown roles", () => {
|
|
174
|
+
expect(userRoleSchema.safeParse("guest").success).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Step 2: Run test to verify it fails**
|
|
180
|
+
|
|
181
|
+
Run: `bun test tests/lib/user-role-schema.test.ts`
|
|
182
|
+
Expected: FAIL because `userRoleSchema` is missing.
|
|
183
|
+
|
|
184
|
+
**Step 3: Write minimal implementation**
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { z } from "zod";
|
|
188
|
+
|
|
189
|
+
export const userRoleSchema = z.enum([
|
|
190
|
+
"admin",
|
|
191
|
+
"product_manager",
|
|
192
|
+
"developer",
|
|
193
|
+
"customer_support",
|
|
194
|
+
"customer",
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
export type UserRoleInput = z.infer<typeof userRoleSchema>;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
(Add the above to `lib/validations/auth.ts` while keeping existing schemas.)
|
|
201
|
+
|
|
202
|
+
**Step 4: Run test to verify it passes**
|
|
203
|
+
|
|
204
|
+
Run: `bun test tests/lib/user-role-schema.test.ts`
|
|
205
|
+
Expected: PASS
|
|
206
|
+
|
|
207
|
+
**Step 5: Commit**
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
git add tests/lib/user-role-schema.test.ts lib/validations/auth.ts
|
|
211
|
+
git commit -m "feat: add user role validation"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Task 4: User schema role column
|
|
215
|
+
|
|
216
|
+
**Files:**
|
|
217
|
+
- Create: `tests/lib/user-schema.test.ts`
|
|
218
|
+
- Modify: `lib/db/schema/auth.ts`
|
|
219
|
+
|
|
220
|
+
**Step 1: Write the failing test**
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { describe, expect, it } from "bun:test";
|
|
224
|
+
import { user } from "@/lib/db/schema";
|
|
225
|
+
|
|
226
|
+
describe("user schema", () => {
|
|
227
|
+
it("includes role column", () => {
|
|
228
|
+
expect(user.role).toBeDefined();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Step 2: Run test to verify it fails**
|
|
234
|
+
|
|
235
|
+
Run: `bun test tests/lib/user-schema.test.ts`
|
|
236
|
+
Expected: FAIL because `user.role` is undefined.
|
|
237
|
+
|
|
238
|
+
**Step 3: Write minimal implementation**
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
role: text("role").notNull().default("customer"),
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
(Add to `lib/db/schema/auth.ts` in the `user` table definition.)
|
|
245
|
+
|
|
246
|
+
**Step 4: Run test to verify it passes**
|
|
247
|
+
|
|
248
|
+
Run: `bun test tests/lib/user-schema.test.ts`
|
|
249
|
+
Expected: PASS
|
|
250
|
+
|
|
251
|
+
**Step 5: Commit**
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
git add tests/lib/user-schema.test.ts lib/db/schema/auth.ts
|
|
255
|
+
git commit -m "feat: add user role column"
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
**Plan complete and saved to `docs/plans/2026-01-03-roles-permissions.md`. Two execution options:**
|
|
261
|
+
|
|
262
|
+
**1. Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration
|
|
263
|
+
|
|
264
|
+
**2. Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints
|
|
265
|
+
|
|
266
|
+
**Which approach?**
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Authentication Middleware Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add session-aware auth guarding in Next.js middleware so `/dashboard` and `/feedback` require login while `/login`, `/register`, `/invite/*`, and `/api/auth/*` stay public, without breaking existing request-id/logging behavior.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Keep the current global middleware (for `x-request-id` and request logging) and add a protected-route check inside it. Implement a small `getServerSession` helper that wraps `better-auth`’s `auth.api.getSession({ headers })` so middleware can request session data from `NextRequest` headers. Only protected routes trigger session checks; public routes pass through.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Next.js App Router middleware, TypeScript, better-auth, Bun test runner.
|
|
10
|
+
|
|
11
|
+
### Task 1: Add middleware auth behavior tests (failing first)
|
|
12
|
+
|
|
13
|
+
**Files:**
|
|
14
|
+
- Modify: `middleware.test.ts`
|
|
15
|
+
|
|
16
|
+
**Step 1: Write the failing tests**
|
|
17
|
+
|
|
18
|
+
Add tests that cover:
|
|
19
|
+
- Protected route unauthenticated → redirect to `/login`
|
|
20
|
+
- Public route unauthenticated → allowed
|
|
21
|
+
- Protected route authenticated → allowed
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { describe, it, expect, mock } from "bun:test";
|
|
25
|
+
import { NextRequest } from "next/server";
|
|
26
|
+
|
|
27
|
+
mock.module("@/lib/auth/session", () => ({
|
|
28
|
+
getServerSession: async (req: NextRequest) => {
|
|
29
|
+
const isAuthed = req.headers.get("x-test-auth") === "1";
|
|
30
|
+
return isAuthed ? { user: { id: "u_test" } } : null;
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const { middleware } = await import("./middleware");
|
|
35
|
+
|
|
36
|
+
describe("middleware auth", () => {
|
|
37
|
+
it("redirects unauthenticated users from protected routes", async () => {
|
|
38
|
+
const req = new NextRequest("http://localhost/dashboard");
|
|
39
|
+
const res = await middleware(req);
|
|
40
|
+
expect(res.headers.get("location")).toBe("http://localhost/login");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("allows unauthenticated users on public routes", async () => {
|
|
44
|
+
const req = new NextRequest("http://localhost/login");
|
|
45
|
+
const res = await middleware(req);
|
|
46
|
+
expect(res.headers.get("location")).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("allows authenticated users on protected routes", async () => {
|
|
50
|
+
const req = new NextRequest("http://localhost/dashboard", {
|
|
51
|
+
headers: { "x-test-auth": "1" },
|
|
52
|
+
});
|
|
53
|
+
const res = await middleware(req);
|
|
54
|
+
expect(res.headers.get("location")).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Step 2: Run test to verify it fails**
|
|
60
|
+
|
|
61
|
+
Run: `bun test middleware.test.ts`
|
|
62
|
+
Expected: FAIL because middleware does not yet enforce auth redirect.
|
|
63
|
+
|
|
64
|
+
**Step 3: Commit**
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git add middleware.test.ts
|
|
68
|
+
git commit -m "test: cover auth redirect behavior in middleware"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Task 2: Implement getServerSession helper
|
|
72
|
+
|
|
73
|
+
**Files:**
|
|
74
|
+
- Create: `lib/auth/session.ts`
|
|
75
|
+
|
|
76
|
+
**Step 1: Write the failing test (optional if skipping unit test)**
|
|
77
|
+
|
|
78
|
+
If adding a unit test, create `tests/auth/session.test.ts` verifying `getServerSession` returns `null` when no session is found. Otherwise, proceed to implementation and rely on middleware tests.
|
|
79
|
+
|
|
80
|
+
**Step 2: Run test to verify it fails**
|
|
81
|
+
|
|
82
|
+
Run (if test added): `bun test tests/auth/session.test.ts`
|
|
83
|
+
Expected: FAIL because helper does not exist.
|
|
84
|
+
|
|
85
|
+
**Step 3: Write minimal implementation**
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import type { NextRequest } from "next/server";
|
|
89
|
+
import { auth } from "@/lib/auth/config";
|
|
90
|
+
|
|
91
|
+
export async function getServerSession(req: NextRequest) {
|
|
92
|
+
return auth.api.getSession({ headers: req.headers });
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Step 4: Run test to verify it passes**
|
|
97
|
+
|
|
98
|
+
Run: `bun test tests/auth/session.test.ts`
|
|
99
|
+
Expected: PASS (if test added).
|
|
100
|
+
|
|
101
|
+
**Step 5: Commit**
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
git add lib/auth/session.ts tests/auth/session.test.ts
|
|
105
|
+
git commit -m "feat: add getServerSession helper for middleware"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Task 3: Enforce authentication in middleware
|
|
109
|
+
|
|
110
|
+
**Files:**
|
|
111
|
+
- Modify: `middleware.ts`
|
|
112
|
+
|
|
113
|
+
**Step 1: Write the failing test (already in Task 1)**
|
|
114
|
+
|
|
115
|
+
Use the tests from Task 1; they should still be failing.
|
|
116
|
+
|
|
117
|
+
**Step 2: Run test to verify it fails**
|
|
118
|
+
|
|
119
|
+
Run: `bun test middleware.test.ts`
|
|
120
|
+
Expected: FAIL because `/dashboard` is not redirected yet.
|
|
121
|
+
|
|
122
|
+
**Step 3: Write minimal implementation**
|
|
123
|
+
|
|
124
|
+
Update middleware to:
|
|
125
|
+
- Define `publicRoutes` and `protectedRoutes` lists
|
|
126
|
+
- Skip session check for public routes
|
|
127
|
+
- For protected routes, call `getServerSession(req)` and redirect if falsy
|
|
128
|
+
- Preserve existing `x-request-id` and logging behavior
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
132
|
+
import { generateRequestId } from "@/lib/middleware/request-id";
|
|
133
|
+
import { logRequest, logResponse } from "@/lib/middleware/request-logger";
|
|
134
|
+
import { getServerSession } from "@/lib/auth/session";
|
|
135
|
+
|
|
136
|
+
const publicRoutes = ["/login", "/register", "/invite", "/invite/", "/api/auth"];
|
|
137
|
+
const protectedRoutes = ["/dashboard", "/feedback"];
|
|
138
|
+
|
|
139
|
+
function isRouteMatch(pathname: string, routes: string[]) {
|
|
140
|
+
return routes.some((route) => pathname === route || pathname.startsWith(route));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function middleware(req: NextRequest) {
|
|
144
|
+
const startTime = Date.now();
|
|
145
|
+
const reqId = req.headers.get("x-request-id") || generateRequestId();
|
|
146
|
+
|
|
147
|
+
const requestHeaders = new Headers(req.headers);
|
|
148
|
+
requestHeaders.set("x-request-id", reqId);
|
|
149
|
+
|
|
150
|
+
logRequest(new Request(req.url, { method: req.method, headers: requestHeaders }));
|
|
151
|
+
|
|
152
|
+
const pathname = req.nextUrl.pathname;
|
|
153
|
+
const isPublic = isRouteMatch(pathname, publicRoutes);
|
|
154
|
+
const isProtected = isRouteMatch(pathname, protectedRoutes);
|
|
155
|
+
|
|
156
|
+
let response = NextResponse.next({
|
|
157
|
+
request: {
|
|
158
|
+
headers: requestHeaders,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (isProtected && !isPublic) {
|
|
163
|
+
const session = await getServerSession(req);
|
|
164
|
+
if (!session) {
|
|
165
|
+
response = NextResponse.redirect(new URL("/login", req.url));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
response.headers.set("x-request-id", reqId);
|
|
170
|
+
const duration = Date.now() - startTime;
|
|
171
|
+
logResponse(reqId, response.status, duration);
|
|
172
|
+
|
|
173
|
+
return response;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Step 4: Run test to verify it passes**
|
|
178
|
+
|
|
179
|
+
Run: `bun test middleware.test.ts`
|
|
180
|
+
Expected: PASS
|
|
181
|
+
|
|
182
|
+
**Step 5: Commit**
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
git add middleware.ts
|
|
186
|
+
git commit -m "feat: enforce auth on protected routes in middleware"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Task 4: Full validation
|
|
190
|
+
|
|
191
|
+
**Files:**
|
|
192
|
+
- None
|
|
193
|
+
|
|
194
|
+
**Step 1: Run lint and tests**
|
|
195
|
+
|
|
196
|
+
Run: `bun run lint`
|
|
197
|
+
Expected: PASS
|
|
198
|
+
|
|
199
|
+
Run: `bun test`
|
|
200
|
+
Expected: PASS
|
|
201
|
+
|
|
202
|
+
**Step 2: Commit (if any fixes were made)**
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
git add .
|
|
206
|
+
git commit -m "chore: fix lint/test issues"
|
|
207
|
+
```
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Member Removal Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Allow organization admins to remove members while preventing removal of the last admin or self-removal.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Add a DELETE handler using the existing build*Handler pattern and Drizzle queries for role checks and deletion. Render a server-fetched members list in the org settings page, and use a client component with a confirmation dialog to call the DELETE API and update local state.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Next.js App Router (RSC), TypeScript, Drizzle ORM, Bun test runner, shadcn/ui components.
|
|
10
|
+
|
|
11
|
+
### Task 1: Add member removal API (handler + route)
|
|
12
|
+
|
|
13
|
+
**Files:**
|
|
14
|
+
- Create: `app/api/organizations/[orgId]/members/[memberId]/handler.ts`
|
|
15
|
+
- Create: `app/api/organizations/[orgId]/members/[memberId]/route.ts`
|
|
16
|
+
- Test: `tests/api/organization-members.test.ts`
|
|
17
|
+
|
|
18
|
+
**Step 1: Write the failing tests**
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { describe, expect, it } from "bun:test";
|
|
22
|
+
import { buildRemoveMemberHandler } from "@/app/api/organizations/[orgId]/members/[memberId]/handler";
|
|
23
|
+
import { organizationMembers } from "@/lib/db/schema";
|
|
24
|
+
|
|
25
|
+
// Tests:
|
|
26
|
+
// - 401 when unauthenticated
|
|
27
|
+
// - 403 when requester is not admin
|
|
28
|
+
// - 404 when target member not found
|
|
29
|
+
// - 400 when target is last admin (message: 组织至少需要一个管理员)
|
|
30
|
+
// - 400 when requester tries to remove self (message: 不能移除自己)
|
|
31
|
+
// - 200 on success and delete called
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Step 2: Run test to verify it fails**
|
|
35
|
+
|
|
36
|
+
Run: `bun test tests/api/organization-members.test.ts`
|
|
37
|
+
Expected: FAIL with "buildRemoveMemberHandler is not defined" (or module not found).
|
|
38
|
+
|
|
39
|
+
**Step 3: Write minimal implementation**
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
export function buildRemoveMemberHandler(deps: RemoveMemberDeps) {
|
|
43
|
+
return async function DELETE(req: Request, context: { params: { orgId: string; memberId: string } }) {
|
|
44
|
+
const session = await deps.auth.api.getSession({ headers: req.headers });
|
|
45
|
+
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
46
|
+
|
|
47
|
+
const { orgId, memberId } = await Promise.resolve(context.params);
|
|
48
|
+
|
|
49
|
+
if (memberId === session.user.id) {
|
|
50
|
+
return NextResponse.json({ error: "不能移除自己" }, { status: 400 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const [requester] = await deps.db
|
|
54
|
+
.select()
|
|
55
|
+
.from(organizationMembers)
|
|
56
|
+
.where(and(eq(organizationMembers.organizationId, orgId), eq(organizationMembers.userId, session.user.id)))
|
|
57
|
+
.limit(1);
|
|
58
|
+
|
|
59
|
+
if (!requester || requester.role !== "admin") {
|
|
60
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const [target] = await deps.db
|
|
64
|
+
.select()
|
|
65
|
+
.from(organizationMembers)
|
|
66
|
+
.where(and(eq(organizationMembers.organizationId, orgId), eq(organizationMembers.userId, memberId)))
|
|
67
|
+
.limit(1);
|
|
68
|
+
|
|
69
|
+
if (!target) {
|
|
70
|
+
return NextResponse.json({ error: "Member not found" }, { status: 404 });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const [adminCount] = await deps.db
|
|
74
|
+
.select({ count: count() })
|
|
75
|
+
.from(organizationMembers)
|
|
76
|
+
.where(and(eq(organizationMembers.organizationId, orgId), eq(organizationMembers.role, "admin")));
|
|
77
|
+
|
|
78
|
+
if (target.role === "admin" && adminCount.count === 1) {
|
|
79
|
+
return NextResponse.json({ error: "组织至少需要一个管理员" }, { status: 400 });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await deps.db
|
|
83
|
+
.delete(organizationMembers)
|
|
84
|
+
.where(and(eq(organizationMembers.organizationId, orgId), eq(organizationMembers.userId, memberId)));
|
|
85
|
+
|
|
86
|
+
return NextResponse.json({ message: "成员已移除" }, { status: 200 });
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Step 4: Run test to verify it passes**
|
|
92
|
+
|
|
93
|
+
Run: `bun test tests/api/organization-members.test.ts`
|
|
94
|
+
Expected: PASS
|
|
95
|
+
|
|
96
|
+
**Step 5: Commit**
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
git add app/api/organizations/[orgId]/members/[memberId]/handler.ts app/api/organizations/[orgId]/members/[memberId]/route.ts tests/api/organization-members.test.ts
|
|
100
|
+
git commit -m "feat: add member removal API"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Task 2: Render member list and removal UI
|
|
104
|
+
|
|
105
|
+
**Files:**
|
|
106
|
+
- Modify: `app/(dashboard)/settings/organizations/[orgId]/members/page.tsx`
|
|
107
|
+
- Create: `components/settings/organization-members-list.tsx`
|
|
108
|
+
|
|
109
|
+
**Step 1: Add a server-side members query**
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const members = db
|
|
113
|
+
? await db
|
|
114
|
+
.select({
|
|
115
|
+
userId: organizationMembers.userId,
|
|
116
|
+
role: organizationMembers.role,
|
|
117
|
+
name: user.name,
|
|
118
|
+
email: user.email,
|
|
119
|
+
})
|
|
120
|
+
.from(organizationMembers)
|
|
121
|
+
.leftJoin(user, eq(user.id, organizationMembers.userId))
|
|
122
|
+
.where(eq(organizationMembers.organizationId, params.orgId))
|
|
123
|
+
: [];
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Step 2: Implement client list with removal**
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
<AlertDialog>
|
|
130
|
+
<AlertDialogTrigger asChild>
|
|
131
|
+
<Button variant="destructive">移除</Button>
|
|
132
|
+
</AlertDialogTrigger>
|
|
133
|
+
<AlertDialogContent>
|
|
134
|
+
<AlertDialogTitle>确认移除?</AlertDialogTitle>
|
|
135
|
+
<AlertDialogAction onClick={() => removeMember(member.userId)}>确认</AlertDialogAction>
|
|
136
|
+
</AlertDialogContent>
|
|
137
|
+
</AlertDialog>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Step 3: Disable remove for self**
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const isSelf = member.userId === currentUserId;
|
|
144
|
+
<Button disabled={isSelf}>移除</Button>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Step 4: Run UI smoke check**
|
|
148
|
+
|
|
149
|
+
Run: `bun dev`
|
|
150
|
+
Expected: Members page shows list, remove button opens confirmation, success removes row from UI.
|
|
151
|
+
|
|
152
|
+
**Step 5: Commit**
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
git add app/(dashboard)/settings/organizations/[orgId]/members/page.tsx components/settings/organization-members-list.tsx
|
|
156
|
+
git commit -m "feat: render members list with removal UI"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Task 3: Edge cases + validation
|
|
160
|
+
|
|
161
|
+
**Files:**
|
|
162
|
+
- Modify: `tests/api/organization-members.test.ts`
|
|
163
|
+
|
|
164
|
+
**Step 1: Add edge-case tests (self-removal + missing member)**
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
it("rejects removing self", async () => {
|
|
168
|
+
// expect 400 "不能移除自己"
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("returns 404 when target member missing", async () => {
|
|
172
|
+
// expect 404
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Step 2: Run test suite**
|
|
177
|
+
|
|
178
|
+
Run: `bun test tests/api/organization-members.test.ts`
|
|
179
|
+
Expected: PASS
|
|
180
|
+
|
|
181
|
+
**Step 3: Commit**
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
git add tests/api/organization-members.test.ts
|
|
185
|
+
git commit -m "test: cover member removal edge cases"
|
|
186
|
+
```
|