@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,109 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Echo Team
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
19
|
+
import { headers } from "next/headers";
|
|
20
|
+
import { eq } from "drizzle-orm";
|
|
21
|
+
import { db } from "@/lib/db";
|
|
22
|
+
import { notificationPreferences } from "@/lib/db/schema";
|
|
23
|
+
import { auth } from "@/lib/auth/config";
|
|
24
|
+
import { apiError } from "@/lib/api/errors";
|
|
25
|
+
|
|
26
|
+
export const dynamic = "force-dynamic";
|
|
27
|
+
export const runtime = "nodejs";
|
|
28
|
+
|
|
29
|
+
export async function GET() {
|
|
30
|
+
if (!db) {
|
|
31
|
+
return NextResponse.json(
|
|
32
|
+
{ error: "Database not configured", code: "DATABASE_NOT_CONFIGURED" },
|
|
33
|
+
{ status: 500 },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
39
|
+
|
|
40
|
+
if (!session?.user) {
|
|
41
|
+
return NextResponse.json(
|
|
42
|
+
{ error: "Authentication required", code: "UNAUTHORIZED" },
|
|
43
|
+
{ status: 401 },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const [prefs] = await db
|
|
48
|
+
.select()
|
|
49
|
+
.from(notificationPreferences)
|
|
50
|
+
.where(eq(notificationPreferences.userId, session.user.id))
|
|
51
|
+
.limit(1);
|
|
52
|
+
|
|
53
|
+
return NextResponse.json({
|
|
54
|
+
data: prefs || { statusChange: true, newComment: true },
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return apiError(error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function POST(req: NextRequest) {
|
|
62
|
+
if (!db) {
|
|
63
|
+
return NextResponse.json(
|
|
64
|
+
{ error: "Database not configured", code: "DATABASE_NOT_CONFIGURED" },
|
|
65
|
+
{ status: 500 },
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
71
|
+
|
|
72
|
+
if (!session?.user) {
|
|
73
|
+
return NextResponse.json(
|
|
74
|
+
{ error: "Authentication required", code: "UNAUTHORIZED" },
|
|
75
|
+
{ status: 401 },
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const body = await req.json();
|
|
80
|
+
const statusChange = body.statusChange ?? true;
|
|
81
|
+
const newComment = body.newComment ?? true;
|
|
82
|
+
|
|
83
|
+
const [existing] = await db
|
|
84
|
+
.select({ userId: notificationPreferences.userId })
|
|
85
|
+
.from(notificationPreferences)
|
|
86
|
+
.where(eq(notificationPreferences.userId, session.user.id))
|
|
87
|
+
.limit(1);
|
|
88
|
+
|
|
89
|
+
if (existing) {
|
|
90
|
+
await db
|
|
91
|
+
.update(notificationPreferences)
|
|
92
|
+
.set({ statusChange, newComment })
|
|
93
|
+
.where(eq(notificationPreferences.userId, session.user.id));
|
|
94
|
+
} else {
|
|
95
|
+
await db.insert(notificationPreferences).values({
|
|
96
|
+
userId: session.user.id,
|
|
97
|
+
statusChange,
|
|
98
|
+
newComment,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return NextResponse.json({
|
|
103
|
+
data: { statusChange, newComment },
|
|
104
|
+
message: "Preferences saved successfully",
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return apiError(error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Echo Team
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { NextResponse } from "next/server";
|
|
19
|
+
import { eq, and } from "drizzle-orm";
|
|
20
|
+
import { createOrganizationSchema } from "@/lib/validations/organizations";
|
|
21
|
+
import { organizations, organizationMembers } from "@/lib/db/schema";
|
|
22
|
+
import type { db as database } from "@/lib/db";
|
|
23
|
+
|
|
24
|
+
type Database = NonNullable<typeof database>;
|
|
25
|
+
|
|
26
|
+
type OrganizationDeps = {
|
|
27
|
+
auth: {
|
|
28
|
+
api: {
|
|
29
|
+
getSession: (args: { headers: Headers }) => Promise<{ user: { id: string; role?: string } } | null>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
db: {
|
|
33
|
+
select: Database["select"];
|
|
34
|
+
update: Database["update"];
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type RouteContext = {
|
|
39
|
+
params: Promise<{ orgId: string }>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function buildGetOrganizationHandler(deps: OrganizationDeps) {
|
|
43
|
+
return async function GET(req: Request, context: RouteContext) {
|
|
44
|
+
const session = await deps.auth.api.getSession({ headers: req.headers });
|
|
45
|
+
if (!session) {
|
|
46
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { orgId } = await context.params;
|
|
50
|
+
|
|
51
|
+
const [org] = await deps.db
|
|
52
|
+
.select()
|
|
53
|
+
.from(organizations)
|
|
54
|
+
.where(eq(organizations.id, orgId))
|
|
55
|
+
.limit(1);
|
|
56
|
+
|
|
57
|
+
if (!org) {
|
|
58
|
+
return NextResponse.json({ error: "Organization not found" }, { status: 404 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return NextResponse.json({ data: org });
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildUpdateOrganizationHandler(deps: OrganizationDeps) {
|
|
66
|
+
return async function PUT(req: Request, context: RouteContext) {
|
|
67
|
+
const session = await deps.auth.api.getSession({ headers: req.headers });
|
|
68
|
+
if (!session) {
|
|
69
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { orgId } = await context.params;
|
|
73
|
+
|
|
74
|
+
// Check if user is admin of this organization or system admin
|
|
75
|
+
const [membership] = await deps.db
|
|
76
|
+
.select()
|
|
77
|
+
.from(organizationMembers)
|
|
78
|
+
.where(
|
|
79
|
+
and(
|
|
80
|
+
eq(organizationMembers.organizationId, orgId),
|
|
81
|
+
eq(organizationMembers.userId, session.user.id)
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
.limit(1);
|
|
85
|
+
|
|
86
|
+
if (!membership && session.user.role !== "admin") {
|
|
87
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (membership && membership.role !== "admin" && session.user.role !== "admin") {
|
|
91
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let body: unknown;
|
|
95
|
+
try {
|
|
96
|
+
body = await req.json();
|
|
97
|
+
} catch {
|
|
98
|
+
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const parsed = createOrganizationSchema.safeParse(body);
|
|
102
|
+
if (!parsed.success) {
|
|
103
|
+
return NextResponse.json(
|
|
104
|
+
{ error: "Invalid request body", details: parsed.error.issues },
|
|
105
|
+
{ status: 400 },
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { name, description } = parsed.data;
|
|
110
|
+
|
|
111
|
+
const [updated] = await deps.db
|
|
112
|
+
.update(organizations)
|
|
113
|
+
.set({ name, description })
|
|
114
|
+
.where(eq(organizations.id, orgId))
|
|
115
|
+
.returning();
|
|
116
|
+
|
|
117
|
+
if (!updated) {
|
|
118
|
+
return NextResponse.json({ error: "Organization not found" }, { status: 404 });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return NextResponse.json({ data: updated });
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Echo Team
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { NextResponse } from "next/server";
|
|
19
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
20
|
+
import type { db as database } from "@/lib/db";
|
|
21
|
+
import { invitations } from "@/lib/db/schema";
|
|
22
|
+
import { assertOrganizationAccess } from "@/lib/auth/organization";
|
|
23
|
+
import { buildInviteExpiry } from "@/lib/invitations";
|
|
24
|
+
import { sendEmail } from "@/lib/services/email";
|
|
25
|
+
import { createInvitationSchema } from "@/lib/validations/invitations";
|
|
26
|
+
|
|
27
|
+
type Database = NonNullable<typeof database>;
|
|
28
|
+
|
|
29
|
+
type CreateInvitationDeps = {
|
|
30
|
+
auth: {
|
|
31
|
+
api: {
|
|
32
|
+
getSession: (args: { headers: Headers }) => Promise<{ user: { id: string } } | null>;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
db: {
|
|
36
|
+
select: Database["select"];
|
|
37
|
+
insert: Database["insert"];
|
|
38
|
+
};
|
|
39
|
+
email?: {
|
|
40
|
+
sendEmail: typeof sendEmail;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type InvitationParams = { orgId: string };
|
|
45
|
+
|
|
46
|
+
type InvitationContext = {
|
|
47
|
+
params: InvitationParams | Promise<InvitationParams>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function buildCreateInvitationHandler(deps: CreateInvitationDeps) {
|
|
51
|
+
return async function POST(req: Request, context: InvitationContext) {
|
|
52
|
+
const session = await deps.auth.api.getSession({ headers: req.headers });
|
|
53
|
+
if (!session) {
|
|
54
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let body: unknown;
|
|
58
|
+
try {
|
|
59
|
+
body = await req.json();
|
|
60
|
+
} catch {
|
|
61
|
+
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const parsed = createInvitationSchema.safeParse(body);
|
|
65
|
+
if (!parsed.success) {
|
|
66
|
+
return NextResponse.json(
|
|
67
|
+
{ error: "Invalid request body", details: parsed.error.issues },
|
|
68
|
+
{ status: 400 },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { email, role } = parsed.data;
|
|
73
|
+
const { orgId } = await Promise.resolve(context.params);
|
|
74
|
+
|
|
75
|
+
let member;
|
|
76
|
+
try {
|
|
77
|
+
member = await assertOrganizationAccess(deps.db, session.user.id, orgId);
|
|
78
|
+
} catch {
|
|
79
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!member || member.role !== "admin") {
|
|
83
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const baseUrl =
|
|
87
|
+
process.env.NEXT_PUBLIC_APP_URL ||
|
|
88
|
+
(process.env.NODE_ENV !== "production" ? "http://localhost:3000" : null);
|
|
89
|
+
if (!baseUrl) {
|
|
90
|
+
return NextResponse.json(
|
|
91
|
+
{ error: "Missing NEXT_PUBLIC_APP_URL" },
|
|
92
|
+
{ status: 500 },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const token = randomBytes(32).toString("hex");
|
|
97
|
+
const expiresAt = buildInviteExpiry(new Date());
|
|
98
|
+
|
|
99
|
+
const [invitation] = await deps.db
|
|
100
|
+
.insert(invitations)
|
|
101
|
+
.values({
|
|
102
|
+
id: randomUUID(),
|
|
103
|
+
organizationId: orgId,
|
|
104
|
+
email,
|
|
105
|
+
role,
|
|
106
|
+
token,
|
|
107
|
+
expiresAt,
|
|
108
|
+
})
|
|
109
|
+
.returning();
|
|
110
|
+
|
|
111
|
+
const inviteUrl = `${baseUrl}/invite/${token}`;
|
|
112
|
+
const mailer = deps.email?.sendEmail ?? sendEmail;
|
|
113
|
+
await mailer({
|
|
114
|
+
to: email,
|
|
115
|
+
subject: "加入组织的邀请",
|
|
116
|
+
html: `<p>点击链接接受邀请:<a href="${inviteUrl}">${inviteUrl}</a></p>`,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return NextResponse.json({ data: invitation }, { status: 201 });
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Echo Team
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { buildCreateInvitationHandler } from "./handler";
|
|
19
|
+
import { auth } from "@/lib/auth/config";
|
|
20
|
+
import { db } from "@/lib/db";
|
|
21
|
+
|
|
22
|
+
export const dynamic = "force-dynamic";
|
|
23
|
+
export const runtime = "nodejs";
|
|
24
|
+
|
|
25
|
+
if (!db) {
|
|
26
|
+
throw new Error("Database not initialized");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const POST = buildCreateInvitationHandler({ auth, db });
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Echo Team
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { NextResponse } from "next/server";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import { and, count, eq } from "drizzle-orm";
|
|
21
|
+
import type { db as database } from "@/lib/db";
|
|
22
|
+
import { assertOrganizationAccess } from "@/lib/auth/organization";
|
|
23
|
+
import { organizationMembers } from "@/lib/db/schema";
|
|
24
|
+
import { organizationMemberRoleSchema } from "@/lib/validations/organizations";
|
|
25
|
+
|
|
26
|
+
type Database = NonNullable<typeof database>;
|
|
27
|
+
|
|
28
|
+
type RemoveMemberDeps = {
|
|
29
|
+
auth: {
|
|
30
|
+
api: {
|
|
31
|
+
getSession: (args: { headers: Headers }) => Promise<{ user: { id: string } } | null>;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
db: {
|
|
35
|
+
select: Database["select"];
|
|
36
|
+
delete: Database["delete"];
|
|
37
|
+
update?: Database["update"];
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type RemoveMemberParams = { orgId: string; memberId: string };
|
|
42
|
+
|
|
43
|
+
type RemoveMemberContext = {
|
|
44
|
+
params: RemoveMemberParams | Promise<RemoveMemberParams>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const updateRoleSchema = z.object({
|
|
48
|
+
role: organizationMemberRoleSchema,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export function buildRemoveMemberHandler(deps: RemoveMemberDeps) {
|
|
52
|
+
return async function DELETE(req: Request, context: RemoveMemberContext) {
|
|
53
|
+
const session = await deps.auth.api.getSession({ headers: req.headers });
|
|
54
|
+
if (!session) {
|
|
55
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { orgId, memberId } = await Promise.resolve(context.params);
|
|
59
|
+
|
|
60
|
+
if (memberId === session.user.id) {
|
|
61
|
+
return NextResponse.json({ error: "不能移除自己" }, { status: 400 });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let requester;
|
|
65
|
+
try {
|
|
66
|
+
requester = await assertOrganizationAccess(deps.db, session.user.id, orgId);
|
|
67
|
+
} catch {
|
|
68
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!requester || requester.role !== "admin") {
|
|
72
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const [target] = await deps.db
|
|
76
|
+
.select()
|
|
77
|
+
.from(organizationMembers)
|
|
78
|
+
.where(
|
|
79
|
+
and(
|
|
80
|
+
eq(organizationMembers.organizationId, orgId),
|
|
81
|
+
eq(organizationMembers.userId, memberId),
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
.limit(1);
|
|
85
|
+
|
|
86
|
+
if (!target) {
|
|
87
|
+
return NextResponse.json({ error: "Member not found" }, { status: 404 });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const [adminCount] = await deps.db
|
|
91
|
+
.select({ count: count() })
|
|
92
|
+
.from(organizationMembers)
|
|
93
|
+
.where(
|
|
94
|
+
and(
|
|
95
|
+
eq(organizationMembers.organizationId, orgId),
|
|
96
|
+
eq(organizationMembers.role, "admin"),
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (target.role === "admin" && adminCount.count === 1) {
|
|
101
|
+
return NextResponse.json(
|
|
102
|
+
{ error: "组织至少需要一个管理员" },
|
|
103
|
+
{ status: 400 },
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await deps.db
|
|
108
|
+
.delete(organizationMembers)
|
|
109
|
+
.where(
|
|
110
|
+
and(
|
|
111
|
+
eq(organizationMembers.organizationId, orgId),
|
|
112
|
+
eq(organizationMembers.userId, memberId),
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return NextResponse.json({ message: "成员已移除" }, { status: 200 });
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type UpdateMemberRoleDeps = Omit<RemoveMemberDeps, "db"> & {
|
|
121
|
+
db: {
|
|
122
|
+
select: Database["select"];
|
|
123
|
+
update: Database["update"];
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export function buildUpdateMemberRoleHandler(deps: UpdateMemberRoleDeps) {
|
|
128
|
+
return async function PUT(req: Request, context: RemoveMemberContext) {
|
|
129
|
+
const session = await deps.auth.api.getSession({ headers: req.headers });
|
|
130
|
+
if (!session) {
|
|
131
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let body: unknown;
|
|
135
|
+
try {
|
|
136
|
+
body = await req.json();
|
|
137
|
+
} catch {
|
|
138
|
+
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const parsed = updateRoleSchema.safeParse(body);
|
|
142
|
+
if (!parsed.success) {
|
|
143
|
+
return NextResponse.json(
|
|
144
|
+
{ error: "Invalid request body", details: parsed.error.issues },
|
|
145
|
+
{ status: 400 },
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { role } = parsed.data;
|
|
150
|
+
const { orgId, memberId } = await Promise.resolve(context.params);
|
|
151
|
+
|
|
152
|
+
let requester;
|
|
153
|
+
try {
|
|
154
|
+
requester = await assertOrganizationAccess(deps.db, session.user.id, orgId);
|
|
155
|
+
} catch {
|
|
156
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!requester || requester.role !== "admin") {
|
|
160
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const [target] = await deps.db
|
|
164
|
+
.select()
|
|
165
|
+
.from(organizationMembers)
|
|
166
|
+
.where(
|
|
167
|
+
and(
|
|
168
|
+
eq(organizationMembers.organizationId, orgId),
|
|
169
|
+
eq(organizationMembers.userId, memberId),
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
.limit(1);
|
|
173
|
+
|
|
174
|
+
if (!target) {
|
|
175
|
+
return NextResponse.json({ error: "Member not found" }, { status: 404 });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const [adminCount] = await deps.db
|
|
179
|
+
.select({ count: count() })
|
|
180
|
+
.from(organizationMembers)
|
|
181
|
+
.where(
|
|
182
|
+
and(
|
|
183
|
+
eq(organizationMembers.organizationId, orgId),
|
|
184
|
+
eq(organizationMembers.role, "admin"),
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (target.role === "admin" && role !== "admin" && adminCount.count === 1) {
|
|
189
|
+
return NextResponse.json(
|
|
190
|
+
{ error: "组织至少需要一个管理员" },
|
|
191
|
+
{ status: 400 },
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const [updated] = await deps.db
|
|
196
|
+
.update(organizationMembers)
|
|
197
|
+
.set({ role })
|
|
198
|
+
.where(
|
|
199
|
+
and(
|
|
200
|
+
eq(organizationMembers.organizationId, orgId),
|
|
201
|
+
eq(organizationMembers.userId, memberId),
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
.returning();
|
|
205
|
+
|
|
206
|
+
return NextResponse.json({ data: updated }, { status: 200 });
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Echo Team
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { buildRemoveMemberHandler, buildUpdateMemberRoleHandler } from "./handler";
|
|
19
|
+
import { auth } from "@/lib/auth/config";
|
|
20
|
+
import { db } from "@/lib/db";
|
|
21
|
+
|
|
22
|
+
export const dynamic = "force-dynamic";
|
|
23
|
+
export const runtime = "nodejs";
|
|
24
|
+
|
|
25
|
+
if (!db) {
|
|
26
|
+
throw new Error("Database not initialized");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const DELETE = buildRemoveMemberHandler({ auth, db });
|
|
30
|
+
export const PUT = buildUpdateMemberRoleHandler({ auth, db });
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Echo Team
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { NextResponse } from "next/server";
|
|
19
|
+
import { eq } from "drizzle-orm";
|
|
20
|
+
import type { db as database } from "@/lib/db";
|
|
21
|
+
import { assertOrganizationAccess } from "@/lib/auth/organization";
|
|
22
|
+
import { organizationMembers, user } from "@/lib/db/schema";
|
|
23
|
+
|
|
24
|
+
type Database = NonNullable<typeof database>;
|
|
25
|
+
|
|
26
|
+
type ListMembersDeps = {
|
|
27
|
+
auth: {
|
|
28
|
+
api: {
|
|
29
|
+
getSession: (args: { headers: Headers }) => Promise<{ user: { id: string } } | null>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
db: {
|
|
33
|
+
select: Database["select"];
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type ListMembersParams = { orgId: string };
|
|
38
|
+
|
|
39
|
+
type ListMembersContext = {
|
|
40
|
+
params: ListMembersParams | Promise<ListMembersParams>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function buildListMembersHandler(deps: ListMembersDeps) {
|
|
44
|
+
return async function GET(req: Request, context: ListMembersContext) {
|
|
45
|
+
const session = await deps.auth.api.getSession({ headers: req.headers });
|
|
46
|
+
if (!session) {
|
|
47
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { orgId } = await Promise.resolve(context.params);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await assertOrganizationAccess(deps.db, session.user.id, orgId);
|
|
54
|
+
} catch {
|
|
55
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const rows = await deps.db
|
|
59
|
+
.select({
|
|
60
|
+
userId: organizationMembers.userId,
|
|
61
|
+
displayName: user.name,
|
|
62
|
+
email: user.email,
|
|
63
|
+
avatarUrl: user.image,
|
|
64
|
+
})
|
|
65
|
+
.from(organizationMembers)
|
|
66
|
+
.innerJoin(user, eq(organizationMembers.userId, user.id))
|
|
67
|
+
.where(eq(organizationMembers.organizationId, orgId));
|
|
68
|
+
|
|
69
|
+
const data = rows.map((row) => ({
|
|
70
|
+
userId: row.userId,
|
|
71
|
+
displayName: row.displayName ?? row.email ?? "未知成员",
|
|
72
|
+
avatarUrl: row.avatarUrl ?? null,
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
return NextResponse.json({ data }, { status: 200 });
|
|
76
|
+
};
|
|
77
|
+
}
|