@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,100 @@
|
|
|
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 { db } from "@/lib/db";
|
|
20
|
+
import { feedback } from "@/lib/db/schema";
|
|
21
|
+
import { eq, isNull, and } from "drizzle-orm";
|
|
22
|
+
import { buildSimilarResponse } from "@/lib/feedback/find-similar";
|
|
23
|
+
import { getOrgContext } from "@/lib/auth/org-context";
|
|
24
|
+
|
|
25
|
+
export const dynamic = "force-dynamic";
|
|
26
|
+
export const runtime = "nodejs";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* GET /api/feedback/similar
|
|
30
|
+
* Returns a list of feedback entries similar to the provided title/description
|
|
31
|
+
*/
|
|
32
|
+
export async function GET(req: NextRequest) {
|
|
33
|
+
if (!db) {
|
|
34
|
+
return NextResponse.json(
|
|
35
|
+
{ error: "Database not configured", code: "DATABASE_NOT_CONFIGURED" },
|
|
36
|
+
{ status: 500 }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const searchParams = req.nextUrl.searchParams;
|
|
42
|
+
const title = searchParams.get("title") || "";
|
|
43
|
+
const description = searchParams.get("description") || "";
|
|
44
|
+
const context = await getOrgContext({ request: req, db, requireMembership: false });
|
|
45
|
+
|
|
46
|
+
if (!title.trim()) {
|
|
47
|
+
return NextResponse.json({ suggestions: [] });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Fetch active feedback for the organization
|
|
51
|
+
const existingFeedback = await db
|
|
52
|
+
.select({
|
|
53
|
+
feedbackId: feedback.feedbackId,
|
|
54
|
+
title: feedback.title,
|
|
55
|
+
description: feedback.description,
|
|
56
|
+
})
|
|
57
|
+
.from(feedback)
|
|
58
|
+
.where(
|
|
59
|
+
and(
|
|
60
|
+
eq(feedback.organizationId, context.organizationId),
|
|
61
|
+
isNull(feedback.deletedAt)
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
.limit(100);
|
|
65
|
+
|
|
66
|
+
const suggestions = buildSimilarResponse(
|
|
67
|
+
title,
|
|
68
|
+
description,
|
|
69
|
+
existingFeedback.map((f) => ({
|
|
70
|
+
feedbackId: f.feedbackId,
|
|
71
|
+
title: f.title ?? "",
|
|
72
|
+
description: f.description,
|
|
73
|
+
}))
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return NextResponse.json({
|
|
77
|
+
suggestions: suggestions.slice(0, 5),
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof Error) {
|
|
81
|
+
if (error.message === "Missing organization") {
|
|
82
|
+
return NextResponse.json(
|
|
83
|
+
{ error: "Organization ID is required", code: "MISSING_ORG_ID" },
|
|
84
|
+
{ status: 400 }
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (error.message === "Access denied") {
|
|
88
|
+
return NextResponse.json(
|
|
89
|
+
{ error: "Insufficient permissions", code: "FORBIDDEN" },
|
|
90
|
+
{ status: 403 }
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
console.error("Error finding similar feedback:", error);
|
|
95
|
+
return NextResponse.json(
|
|
96
|
+
{ error: "Internal server error", code: "INTERNAL_ERROR" },
|
|
97
|
+
{ status: 500 }
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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 { describe, it, expect } from "bun:test";
|
|
19
|
+
import { NextRequest } from "next/server";
|
|
20
|
+
import { GET } from "./route";
|
|
21
|
+
|
|
22
|
+
describe("GET /api/health", () => {
|
|
23
|
+
it("returns health status with database check", async () => {
|
|
24
|
+
const previousDatabaseUrl = process.env.DATABASE_URL;
|
|
25
|
+
process.env.DATABASE_URL = "";
|
|
26
|
+
const req = new NextRequest("http://localhost/api/health");
|
|
27
|
+
|
|
28
|
+
const res = await GET(req);
|
|
29
|
+
expect(res.status).toBe(503);
|
|
30
|
+
|
|
31
|
+
const body = await res.json();
|
|
32
|
+
expect(body.status).toBe("unhealthy");
|
|
33
|
+
expect(typeof body.timestamp).toBe("string");
|
|
34
|
+
expect(typeof body.uptime).toBe("number");
|
|
35
|
+
expect(body.checks).toBeDefined();
|
|
36
|
+
expect(body.checks.database).toBeDefined();
|
|
37
|
+
expect(body.checks.database.status).toBe("fail");
|
|
38
|
+
expect(typeof body.checks.database.error).toBe("string");
|
|
39
|
+
|
|
40
|
+
if (previousDatabaseUrl === undefined) {
|
|
41
|
+
delete process.env.DATABASE_URL;
|
|
42
|
+
} else {
|
|
43
|
+
process.env.DATABASE_URL = previousDatabaseUrl;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("includes cache and timing headers", async () => {
|
|
48
|
+
const previousDatabaseUrl = process.env.DATABASE_URL;
|
|
49
|
+
process.env.DATABASE_URL = "";
|
|
50
|
+
const req = new NextRequest("http://localhost/api/health");
|
|
51
|
+
|
|
52
|
+
const res = await GET(req);
|
|
53
|
+
expect(res.headers.get("Cache-Control")).toBe(
|
|
54
|
+
"no-cache, no-store, must-revalidate"
|
|
55
|
+
);
|
|
56
|
+
expect(res.headers.get("X-Health-Check-Duration")).toMatch(/\d+ms/);
|
|
57
|
+
|
|
58
|
+
if (previousDatabaseUrl === undefined) {
|
|
59
|
+
delete process.env.DATABASE_URL;
|
|
60
|
+
} else {
|
|
61
|
+
process.env.DATABASE_URL = previousDatabaseUrl;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
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 { createRequestLogger } from "@/lib/logger";
|
|
20
|
+
import { db } from "@/lib/db";
|
|
21
|
+
import { sql } from "drizzle-orm";
|
|
22
|
+
|
|
23
|
+
export const dynamic = "force-dynamic";
|
|
24
|
+
export const runtime = "nodejs";
|
|
25
|
+
|
|
26
|
+
interface HealthStatus {
|
|
27
|
+
status: "healthy" | "unhealthy" | "degraded";
|
|
28
|
+
timestamp: string;
|
|
29
|
+
uptime: number;
|
|
30
|
+
version?: string;
|
|
31
|
+
checks: {
|
|
32
|
+
database: {
|
|
33
|
+
status: "pass" | "fail";
|
|
34
|
+
latency?: number;
|
|
35
|
+
error?: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function GET(req: NextRequest) {
|
|
41
|
+
const reqId = req.headers.get("x-request-id") || "unknown";
|
|
42
|
+
const log = createRequestLogger(reqId);
|
|
43
|
+
const startTime = Date.now();
|
|
44
|
+
|
|
45
|
+
const health: HealthStatus = {
|
|
46
|
+
status: "healthy",
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
uptime: process.uptime(),
|
|
49
|
+
version: process.env.npm_package_version || process.env.APP_VERSION || "0.0.0",
|
|
50
|
+
checks: {
|
|
51
|
+
database: { status: "pass" },
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (!process.env.DATABASE_URL || !db) {
|
|
56
|
+
health.status = "unhealthy";
|
|
57
|
+
health.checks.database = {
|
|
58
|
+
status: "fail",
|
|
59
|
+
error: "DATABASE_URL not set",
|
|
60
|
+
};
|
|
61
|
+
} else {
|
|
62
|
+
try {
|
|
63
|
+
const dbStart = Date.now();
|
|
64
|
+
await db.execute(sql`SELECT 1`);
|
|
65
|
+
const dbLatency = Date.now() - dbStart;
|
|
66
|
+
health.checks.database.latency = dbLatency;
|
|
67
|
+
|
|
68
|
+
if (dbLatency > 500) {
|
|
69
|
+
health.status = "degraded";
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
health.status = "unhealthy";
|
|
73
|
+
health.checks.database = {
|
|
74
|
+
status: "fail",
|
|
75
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const checkTime = Date.now() - startTime;
|
|
81
|
+
const statusCode = health.status === "unhealthy" ? 503 : 200;
|
|
82
|
+
|
|
83
|
+
log.info({ status: health.status, durationMs: checkTime }, "Health check");
|
|
84
|
+
|
|
85
|
+
return NextResponse.json(health, {
|
|
86
|
+
status: statusCode,
|
|
87
|
+
headers: {
|
|
88
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
89
|
+
"X-Health-Check-Duration": `${checkTime}ms`,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -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 { parseJwtIdentity } from "@/lib/auth/jwt-identity";
|
|
19
|
+
import { NextResponse } from "next/server";
|
|
20
|
+
|
|
21
|
+
export async function POST(request: Request) {
|
|
22
|
+
const body = await request.json();
|
|
23
|
+
const token = body.token as string;
|
|
24
|
+
const identity = parseJwtIdentity(token);
|
|
25
|
+
if (!identity) {
|
|
26
|
+
return NextResponse.json({ error: "Invalid token" }, { status: 400 });
|
|
27
|
+
}
|
|
28
|
+
return NextResponse.json(identity);
|
|
29
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
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 { auth } from "@/lib/auth/config";
|
|
20
|
+
import { db } from "@/lib/db";
|
|
21
|
+
import { githubIntegrations } from "@/lib/db/schema";
|
|
22
|
+
import { eq } from "drizzle-orm";
|
|
23
|
+
import { GitHubClient } from "@/lib/integrations/github";
|
|
24
|
+
import { randomBytes } from "crypto";
|
|
25
|
+
import { getCurrentOrganizationId } from "@/lib/auth/organization";
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import { apiError, validationError } from "@/lib/api/errors";
|
|
28
|
+
|
|
29
|
+
export const dynamic = "force-dynamic";
|
|
30
|
+
export const runtime = "nodejs";
|
|
31
|
+
|
|
32
|
+
const createGitHubIntegrationSchema = z.object({
|
|
33
|
+
accessToken: z.string().min(1),
|
|
34
|
+
owner: z.string().min(1),
|
|
35
|
+
repo: z.string().min(1),
|
|
36
|
+
autoSync: z.boolean().optional().default(true),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export async function GET(req: NextRequest) {
|
|
40
|
+
const session = await auth.api.getSession({ headers: req.headers });
|
|
41
|
+
if (!session?.user?.id) {
|
|
42
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const organizationId = await getCurrentOrganizationId(session.user.id);
|
|
46
|
+
if (!organizationId) {
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{ error: "No organization selected" },
|
|
49
|
+
{ status: 400 },
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!db) {
|
|
54
|
+
return NextResponse.json(
|
|
55
|
+
{ error: "Database not configured" },
|
|
56
|
+
{ status: 500 },
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const config = await db
|
|
62
|
+
.select()
|
|
63
|
+
.from(githubIntegrations)
|
|
64
|
+
.where(eq(githubIntegrations.organizationId, organizationId))
|
|
65
|
+
.limit(1)
|
|
66
|
+
.then((rows) => rows[0]);
|
|
67
|
+
|
|
68
|
+
if (!config) {
|
|
69
|
+
return NextResponse.json({ configured: false });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return NextResponse.json({
|
|
73
|
+
configured: true,
|
|
74
|
+
owner: config.owner,
|
|
75
|
+
repo: config.repo,
|
|
76
|
+
autoSync: config.autoSync,
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return apiError(error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function POST(req: NextRequest) {
|
|
84
|
+
const session = await auth.api.getSession({ headers: req.headers });
|
|
85
|
+
if (!session?.user?.id) {
|
|
86
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const organizationId = await getCurrentOrganizationId(session.user.id);
|
|
90
|
+
if (!organizationId) {
|
|
91
|
+
return NextResponse.json(
|
|
92
|
+
{ error: "No organization selected" },
|
|
93
|
+
{ status: 400 },
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!db) {
|
|
98
|
+
return NextResponse.json(
|
|
99
|
+
{ error: "Database not configured" },
|
|
100
|
+
{ status: 500 },
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const body = await req.json();
|
|
106
|
+
const validated = createGitHubIntegrationSchema.parse(body);
|
|
107
|
+
|
|
108
|
+
const client = new GitHubClient({
|
|
109
|
+
accessToken: validated.accessToken,
|
|
110
|
+
owner: validated.owner,
|
|
111
|
+
repo: validated.repo,
|
|
112
|
+
});
|
|
113
|
+
const isValid = await client.validateToken();
|
|
114
|
+
|
|
115
|
+
if (!isValid) {
|
|
116
|
+
return NextResponse.json(
|
|
117
|
+
{ error: "Invalid token or repository" },
|
|
118
|
+
{ status: 400 },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const webhookSecret = randomBytes(32).toString("hex");
|
|
123
|
+
|
|
124
|
+
const existing = await db
|
|
125
|
+
.select()
|
|
126
|
+
.from(githubIntegrations)
|
|
127
|
+
.where(eq(githubIntegrations.organizationId, organizationId))
|
|
128
|
+
.limit(1)
|
|
129
|
+
.then((rows) => rows[0]);
|
|
130
|
+
|
|
131
|
+
if (existing) {
|
|
132
|
+
await db
|
|
133
|
+
.update(githubIntegrations)
|
|
134
|
+
.set({
|
|
135
|
+
accessToken: validated.accessToken,
|
|
136
|
+
owner: validated.owner,
|
|
137
|
+
repo: validated.repo,
|
|
138
|
+
autoSync: validated.autoSync,
|
|
139
|
+
webhookSecret,
|
|
140
|
+
})
|
|
141
|
+
.where(eq(githubIntegrations.id, existing.id));
|
|
142
|
+
} else {
|
|
143
|
+
await db.insert(githubIntegrations).values({
|
|
144
|
+
organizationId,
|
|
145
|
+
accessToken: validated.accessToken,
|
|
146
|
+
owner: validated.owner,
|
|
147
|
+
repo: validated.repo,
|
|
148
|
+
autoSync: validated.autoSync,
|
|
149
|
+
webhookSecret,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return NextResponse.json({
|
|
154
|
+
success: true,
|
|
155
|
+
webhookUrl: `${process.env.NEXT_PUBLIC_APP_URL}/api/webhooks/github`,
|
|
156
|
+
webhookSecret,
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error instanceof z.ZodError) {
|
|
160
|
+
return validationError(error.issues);
|
|
161
|
+
}
|
|
162
|
+
return apiError(error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function DELETE(req: NextRequest) {
|
|
167
|
+
const session = await auth.api.getSession({ headers: req.headers });
|
|
168
|
+
if (!session?.user?.id) {
|
|
169
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const organizationId = await getCurrentOrganizationId(session.user.id);
|
|
173
|
+
if (!organizationId) {
|
|
174
|
+
return NextResponse.json(
|
|
175
|
+
{ error: "No organization selected" },
|
|
176
|
+
{ status: 400 },
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!db) {
|
|
181
|
+
return NextResponse.json(
|
|
182
|
+
{ error: "Database not configured" },
|
|
183
|
+
{ status: 500 },
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await db
|
|
189
|
+
.delete(githubIntegrations)
|
|
190
|
+
.where(eq(githubIntegrations.organizationId, organizationId));
|
|
191
|
+
|
|
192
|
+
return NextResponse.json({ success: true });
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return apiError(error);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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 { db } from "@/lib/db";
|
|
20
|
+
import { organizationSettings, organizations } from "@/lib/db/schema";
|
|
21
|
+
import { eq } from "drizzle-orm";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Internal API for domain lookup (used by middleware)
|
|
25
|
+
* GET /api/internal/domain-lookup?domain=feedback.acme.com
|
|
26
|
+
*/
|
|
27
|
+
export async function GET(req: NextRequest) {
|
|
28
|
+
// Verify middleware secret to prevent external access
|
|
29
|
+
const secret = req.headers.get("x-middleware-secret");
|
|
30
|
+
if (secret !== process.env.MIDDLEWARE_SECRET) {
|
|
31
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const domain = req.nextUrl.searchParams.get("domain");
|
|
35
|
+
if (!domain) {
|
|
36
|
+
return NextResponse.json({ error: "Domain required" }, { status: 400 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!db) {
|
|
40
|
+
return NextResponse.json({ error: "Database not configured" }, { status: 500 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const [organization] = await db
|
|
45
|
+
.select({
|
|
46
|
+
organizationId: organizations.id,
|
|
47
|
+
orgSlug: organizations.slug,
|
|
48
|
+
})
|
|
49
|
+
.from(organizationSettings)
|
|
50
|
+
.innerJoin(organizations, eq(organizationSettings.organizationId, organizations.id))
|
|
51
|
+
.where(eq(organizationSettings.customDomain, domain))
|
|
52
|
+
.limit(1);
|
|
53
|
+
|
|
54
|
+
if (!organization) {
|
|
55
|
+
return NextResponse.json({ found: false });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return NextResponse.json({
|
|
59
|
+
found: true,
|
|
60
|
+
orgSlug: organization.orgSlug,
|
|
61
|
+
organizationId: organization.organizationId,
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error("Domain lookup error:", error);
|
|
65
|
+
return NextResponse.json({ error: "Lookup failed" }, { status: 500 });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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 { invitations, organizationMembers } from "@/lib/db/schema";
|
|
22
|
+
|
|
23
|
+
const TOKEN_REQUIRED = "Token is required";
|
|
24
|
+
|
|
25
|
+
type Database = NonNullable<typeof database>;
|
|
26
|
+
|
|
27
|
+
type AcceptInvitationDeps = {
|
|
28
|
+
auth: {
|
|
29
|
+
api: {
|
|
30
|
+
getSession: (args: { headers: Headers }) => Promise<{ user: { id: string } } | null>;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
db: {
|
|
34
|
+
select: Database["select"];
|
|
35
|
+
transaction: Database["transaction"];
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type NowProvider = () => Date;
|
|
40
|
+
|
|
41
|
+
export function buildAcceptInvitationHandler(
|
|
42
|
+
deps: AcceptInvitationDeps,
|
|
43
|
+
nowProvider: NowProvider = () => new Date(),
|
|
44
|
+
) {
|
|
45
|
+
return async function POST(req: Request) {
|
|
46
|
+
const session = await deps.auth.api.getSession({ headers: req.headers });
|
|
47
|
+
if (!session) {
|
|
48
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let body: unknown;
|
|
52
|
+
try {
|
|
53
|
+
body = await req.json();
|
|
54
|
+
} catch {
|
|
55
|
+
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!body || typeof body !== "object" || !("token" in body)) {
|
|
59
|
+
return NextResponse.json({ error: TOKEN_REQUIRED }, { status: 400 });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const token = (body as { token?: unknown }).token;
|
|
63
|
+
if (typeof token !== "string" || token.trim().length === 0) {
|
|
64
|
+
return NextResponse.json({ error: TOKEN_REQUIRED }, { status: 400 });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const [invitation] = await deps.db
|
|
68
|
+
.select()
|
|
69
|
+
.from(invitations)
|
|
70
|
+
.where(eq(invitations.token, token))
|
|
71
|
+
.limit(1);
|
|
72
|
+
|
|
73
|
+
if (!invitation) {
|
|
74
|
+
return NextResponse.json({ error: "Invitation not found" }, { status: 404 });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (invitation.acceptedAt) {
|
|
78
|
+
return NextResponse.json({ error: "Invitation already accepted" }, { status: 409 });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const now = nowProvider();
|
|
82
|
+
if (invitation.expiresAt <= now) {
|
|
83
|
+
return NextResponse.json({ error: "邀请已过期" }, { status: 410 });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await deps.db.transaction(async (tx) => {
|
|
87
|
+
await tx.insert(organizationMembers).values({
|
|
88
|
+
organizationId: invitation.organizationId,
|
|
89
|
+
userId: session.user.id,
|
|
90
|
+
role: invitation.role,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await tx
|
|
94
|
+
.update(invitations)
|
|
95
|
+
.set({ acceptedAt: now })
|
|
96
|
+
.where(eq(invitations.id, invitation.id));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return NextResponse.json({ message: "Invitation accepted" }, { status: 200 });
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -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 { buildAcceptInvitationHandler } 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 = buildAcceptInvitationHandler({ auth, db });
|