@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,71 @@
|
|
|
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 { assertOrganizationAccess } from "@/lib/auth/organization";
|
|
19
|
+
import type { db as database } from "@/lib/db";
|
|
20
|
+
|
|
21
|
+
type Database = NonNullable<typeof database>;
|
|
22
|
+
|
|
23
|
+
type OrgContextSource = "query" | "header" | "cookie" | "explicit";
|
|
24
|
+
|
|
25
|
+
export type OrgContext = {
|
|
26
|
+
organizationId: string;
|
|
27
|
+
memberRole: string | null;
|
|
28
|
+
source: OrgContextSource;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type OrgContextRequest = {
|
|
32
|
+
nextUrl: URL;
|
|
33
|
+
headers: Headers;
|
|
34
|
+
cookies?: { get: (name: string) => { value: string } | undefined };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export async function getOrgContext(options: {
|
|
38
|
+
request: OrgContextRequest;
|
|
39
|
+
db: Pick<Database, "select">;
|
|
40
|
+
userId?: string | null;
|
|
41
|
+
organizationId?: string | null;
|
|
42
|
+
requireMembership?: boolean;
|
|
43
|
+
}): Promise<OrgContext> {
|
|
44
|
+
const { request, db, userId, organizationId, requireMembership } = options;
|
|
45
|
+
const queryOrgId = request.nextUrl.searchParams.get("organizationId");
|
|
46
|
+
const headerOrgId = request.headers.get("x-organization-id");
|
|
47
|
+
const cookieOrgId = request.cookies?.get("orgId")?.value ?? null;
|
|
48
|
+
|
|
49
|
+
const resolved = organizationId || queryOrgId || headerOrgId || cookieOrgId;
|
|
50
|
+
const source: OrgContextSource = organizationId
|
|
51
|
+
? "explicit"
|
|
52
|
+
: queryOrgId
|
|
53
|
+
? "query"
|
|
54
|
+
: headerOrgId
|
|
55
|
+
? "header"
|
|
56
|
+
: "cookie";
|
|
57
|
+
|
|
58
|
+
if (!resolved) {
|
|
59
|
+
throw new Error("Missing organization");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let memberRole: string | null = null;
|
|
63
|
+
if (userId) {
|
|
64
|
+
const member = await assertOrganizationAccess(db, userId, resolved);
|
|
65
|
+
memberRole = member.role;
|
|
66
|
+
} else if (requireMembership) {
|
|
67
|
+
throw new Error("Access denied");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { organizationId: resolved, memberRole, source };
|
|
71
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
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 { and, eq } from "drizzle-orm";
|
|
19
|
+
import { db } from "@/lib/db";
|
|
20
|
+
import type { db as database } from "@/lib/db";
|
|
21
|
+
import { organizationMembers, organizations } from "@/lib/db/schema";
|
|
22
|
+
|
|
23
|
+
type Database = NonNullable<typeof database>;
|
|
24
|
+
|
|
25
|
+
export interface UserOrganization {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
slug: string;
|
|
29
|
+
description: string | null;
|
|
30
|
+
role: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function getUserOrganizations(
|
|
34
|
+
db: Database,
|
|
35
|
+
userId: string
|
|
36
|
+
): Promise<UserOrganization[]> {
|
|
37
|
+
return db
|
|
38
|
+
.select({
|
|
39
|
+
id: organizations.id,
|
|
40
|
+
name: organizations.name,
|
|
41
|
+
slug: organizations.slug,
|
|
42
|
+
description: organizations.description,
|
|
43
|
+
role: organizationMembers.role,
|
|
44
|
+
})
|
|
45
|
+
.from(organizationMembers)
|
|
46
|
+
.innerJoin(organizations, eq(organizations.id, organizationMembers.organizationId))
|
|
47
|
+
.where(eq(organizationMembers.userId, userId));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function getUserOrganization(
|
|
51
|
+
db: Database,
|
|
52
|
+
userId: string
|
|
53
|
+
): Promise<UserOrganization | null> {
|
|
54
|
+
const result = await db
|
|
55
|
+
.select({
|
|
56
|
+
id: organizations.id,
|
|
57
|
+
name: organizations.name,
|
|
58
|
+
slug: organizations.slug,
|
|
59
|
+
description: organizations.description,
|
|
60
|
+
role: organizationMembers.role,
|
|
61
|
+
})
|
|
62
|
+
.from(organizationMembers)
|
|
63
|
+
.innerJoin(organizations, eq(organizations.id, organizationMembers.organizationId))
|
|
64
|
+
.where(eq(organizationMembers.userId, userId))
|
|
65
|
+
.limit(1);
|
|
66
|
+
|
|
67
|
+
return result[0] || null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface OrganizationMember {
|
|
71
|
+
organizationId: string;
|
|
72
|
+
userId: string;
|
|
73
|
+
role: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type MinimalDb = {
|
|
77
|
+
select: Database["select"];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export async function assertOrganizationAccess(
|
|
81
|
+
db: MinimalDb,
|
|
82
|
+
userId: string,
|
|
83
|
+
organizationId: string
|
|
84
|
+
): Promise<OrganizationMember> {
|
|
85
|
+
const [member] = await db
|
|
86
|
+
.select()
|
|
87
|
+
.from(organizationMembers)
|
|
88
|
+
.where(
|
|
89
|
+
and(
|
|
90
|
+
eq(organizationMembers.organizationId, organizationId),
|
|
91
|
+
eq(organizationMembers.userId, userId)
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
.limit(1);
|
|
95
|
+
|
|
96
|
+
if (!member) {
|
|
97
|
+
throw new Error("Access denied");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return member;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function getCurrentOrganizationId(userId: string): Promise<string | null> {
|
|
104
|
+
if (!db) return null;
|
|
105
|
+
const org = await getUserOrganization(db, userId);
|
|
106
|
+
return org?.id || null;
|
|
107
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
export type UserRole =
|
|
19
|
+
| "admin"
|
|
20
|
+
| "product_manager"
|
|
21
|
+
| "developer"
|
|
22
|
+
| "customer_support"
|
|
23
|
+
| "customer";
|
|
24
|
+
|
|
25
|
+
export const PERMISSIONS = {
|
|
26
|
+
CREATE_FEEDBACK: "create_feedback",
|
|
27
|
+
SUBMIT_ON_BEHALF: "submit_on_behalf",
|
|
28
|
+
DELETE_FEEDBACK: "delete_feedback",
|
|
29
|
+
MANAGE_ORG: "manage_org",
|
|
30
|
+
UPDATE_FEEDBACK_STATUS: "update_feedback_status",
|
|
31
|
+
BACKUP_CREATE: "backup_create",
|
|
32
|
+
BACKUP_VIEW: "backup_view",
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
export type Permission = (typeof PERMISSIONS)[keyof typeof PERMISSIONS];
|
|
36
|
+
|
|
37
|
+
export const ROLE_PERMISSIONS: Record<UserRole, Permission[]> = {
|
|
38
|
+
admin: [
|
|
39
|
+
PERMISSIONS.CREATE_FEEDBACK,
|
|
40
|
+
PERMISSIONS.SUBMIT_ON_BEHALF,
|
|
41
|
+
PERMISSIONS.DELETE_FEEDBACK,
|
|
42
|
+
PERMISSIONS.MANAGE_ORG,
|
|
43
|
+
PERMISSIONS.UPDATE_FEEDBACK_STATUS,
|
|
44
|
+
PERMISSIONS.BACKUP_CREATE,
|
|
45
|
+
PERMISSIONS.BACKUP_VIEW,
|
|
46
|
+
],
|
|
47
|
+
product_manager: [
|
|
48
|
+
PERMISSIONS.CREATE_FEEDBACK,
|
|
49
|
+
PERMISSIONS.SUBMIT_ON_BEHALF,
|
|
50
|
+
PERMISSIONS.DELETE_FEEDBACK,
|
|
51
|
+
PERMISSIONS.UPDATE_FEEDBACK_STATUS,
|
|
52
|
+
PERMISSIONS.BACKUP_VIEW,
|
|
53
|
+
],
|
|
54
|
+
developer: [PERMISSIONS.CREATE_FEEDBACK],
|
|
55
|
+
customer_support: [
|
|
56
|
+
PERMISSIONS.CREATE_FEEDBACK,
|
|
57
|
+
PERMISSIONS.SUBMIT_ON_BEHALF,
|
|
58
|
+
],
|
|
59
|
+
customer: [PERMISSIONS.CREATE_FEEDBACK],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export function hasPermission(role: UserRole, permission: Permission): boolean {
|
|
63
|
+
return ROLE_PERMISSIONS[role]?.includes(permission) ?? false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function hasAllPermissions(
|
|
67
|
+
role: UserRole,
|
|
68
|
+
permissions: Permission[],
|
|
69
|
+
): boolean {
|
|
70
|
+
return permissions.every((permission) => hasPermission(role, permission));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function canSubmitOnBehalf(role: UserRole): boolean {
|
|
74
|
+
return hasPermission(role, PERMISSIONS.SUBMIT_ON_BEHALF);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function canUpdateFeedbackStatus(role: UserRole): boolean {
|
|
78
|
+
return hasPermission(role, PERMISSIONS.UPDATE_FEEDBACK_STATUS);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function canDeleteFeedback(role: UserRole): boolean {
|
|
82
|
+
return hasPermission(role, PERMISSIONS.DELETE_FEEDBACK);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function canEditFeedback(role: UserRole): boolean {
|
|
86
|
+
return role === "admin" || role === "product_manager";
|
|
87
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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 type { NextRequest } from "next/server";
|
|
19
|
+
import { auth } from "@/lib/auth/config";
|
|
20
|
+
|
|
21
|
+
export async function getServerSession(req: NextRequest) {
|
|
22
|
+
return auth.api.getSession({ headers: req.headers });
|
|
23
|
+
}
|
|
@@ -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 { NextRequest } from 'next/server';
|
|
19
|
+
import { RateLimitConfig } from '@/lib/middleware/rate-limit';
|
|
20
|
+
import { apiKeyKeyGenerator } from '@/lib/middleware/rate-limit-keys';
|
|
21
|
+
|
|
22
|
+
export const RATE_LIMITS = {
|
|
23
|
+
public: {
|
|
24
|
+
windowMs: 60 * 15,
|
|
25
|
+
maxRequests: 100,
|
|
26
|
+
keyGenerator: (req: NextRequest) => {
|
|
27
|
+
const ip = req.headers.get('x-forwarded-for')?.split(',')[0] || 'unknown';
|
|
28
|
+
return Promise.resolve(`public:${ip}`);
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
authenticated: {
|
|
33
|
+
windowMs: 60,
|
|
34
|
+
maxRequests: 60,
|
|
35
|
+
keyGenerator: (req: NextRequest) => {
|
|
36
|
+
const userId = req.headers.get('x-user-id') || 'unknown';
|
|
37
|
+
return Promise.resolve(`user:${userId}`);
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
apiKey: {
|
|
42
|
+
windowMs: 60,
|
|
43
|
+
maxRequests: 100,
|
|
44
|
+
keyGenerator: (req: NextRequest) => Promise.resolve(apiKeyKeyGenerator(req)),
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
write: {
|
|
48
|
+
windowMs: 60,
|
|
49
|
+
maxRequests: 20,
|
|
50
|
+
keyGenerator: (req: NextRequest) => Promise.resolve(apiKeyKeyGenerator(req)),
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
webhook: {
|
|
54
|
+
windowMs: 60,
|
|
55
|
+
maxRequests: 200,
|
|
56
|
+
keyGenerator: (req: NextRequest) => {
|
|
57
|
+
const orgId = req.headers.get('x-organization-id') || 'unknown';
|
|
58
|
+
return Promise.resolve(`webhook:${orgId}`);
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
} satisfies Record<
|
|
62
|
+
string,
|
|
63
|
+
Omit<RateLimitConfig, 'skipFailedRequests' | 'storage'>
|
|
64
|
+
>;
|
|
@@ -0,0 +1,136 @@
|
|
|
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 { eq, and, gte, isNull, sql, count } from "drizzle-orm";
|
|
19
|
+
import type { db as database } from "@/lib/db";
|
|
20
|
+
import { feedback } from "@/lib/db/schema";
|
|
21
|
+
import type { UserRole } from "@/lib/auth/permissions";
|
|
22
|
+
|
|
23
|
+
type Database = NonNullable<typeof database>;
|
|
24
|
+
|
|
25
|
+
export interface DashboardStats {
|
|
26
|
+
totalFeedback: number;
|
|
27
|
+
pendingFeedback: number;
|
|
28
|
+
weeklyFeedback: number;
|
|
29
|
+
resolvedFeedback: number;
|
|
30
|
+
statusDistribution: { status: string; count: number }[];
|
|
31
|
+
recentFeedback: {
|
|
32
|
+
feedbackId: number;
|
|
33
|
+
title: string;
|
|
34
|
+
type: string;
|
|
35
|
+
status: string;
|
|
36
|
+
priority: string;
|
|
37
|
+
createdAt: Date;
|
|
38
|
+
}[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function getDashboardStats(
|
|
42
|
+
db: Database,
|
|
43
|
+
options: {
|
|
44
|
+
userId: string;
|
|
45
|
+
userRole: UserRole;
|
|
46
|
+
organizationId: string;
|
|
47
|
+
}
|
|
48
|
+
): Promise<DashboardStats> {
|
|
49
|
+
const { userId, userRole, organizationId } = options;
|
|
50
|
+
const sevenDaysAgo = new Date();
|
|
51
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
52
|
+
|
|
53
|
+
const isAdminOrPM = userRole === "admin" || userRole === "product_manager";
|
|
54
|
+
|
|
55
|
+
// Base condition: not deleted and belongs to organization
|
|
56
|
+
const baseCondition = and(
|
|
57
|
+
isNull(feedback.deletedAt),
|
|
58
|
+
eq(feedback.organizationId, organizationId)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// For non-admin/PM, filter by submittedBy
|
|
62
|
+
const userCondition = isAdminOrPM
|
|
63
|
+
? baseCondition
|
|
64
|
+
: and(baseCondition, eq(feedback.submittedBy, userId));
|
|
65
|
+
|
|
66
|
+
// Total feedback count
|
|
67
|
+
const [totalResult] = await db
|
|
68
|
+
.select({ count: count() })
|
|
69
|
+
.from(feedback)
|
|
70
|
+
.where(userCondition);
|
|
71
|
+
|
|
72
|
+
// Pending feedback (status = 'new' or 'in-progress')
|
|
73
|
+
const [pendingResult] = await db
|
|
74
|
+
.select({ count: count() })
|
|
75
|
+
.from(feedback)
|
|
76
|
+
.where(
|
|
77
|
+
and(
|
|
78
|
+
userCondition,
|
|
79
|
+
sql`${feedback.status} IN ('new', 'in-progress')`
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Weekly feedback (last 7 days)
|
|
84
|
+
const [weeklyResult] = await db
|
|
85
|
+
.select({ count: count() })
|
|
86
|
+
.from(feedback)
|
|
87
|
+
.where(and(userCondition, gte(feedback.createdAt, sevenDaysAgo)));
|
|
88
|
+
|
|
89
|
+
// Resolved feedback (completed + closed)
|
|
90
|
+
const [resolvedResult] = await db
|
|
91
|
+
.select({ count: count() })
|
|
92
|
+
.from(feedback)
|
|
93
|
+
.where(
|
|
94
|
+
and(
|
|
95
|
+
userCondition,
|
|
96
|
+
sql`${feedback.status} IN ('completed', 'closed')`
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Status distribution
|
|
101
|
+
const statusDistribution = await db
|
|
102
|
+
.select({
|
|
103
|
+
status: feedback.status,
|
|
104
|
+
count: count(),
|
|
105
|
+
})
|
|
106
|
+
.from(feedback)
|
|
107
|
+
.where(userCondition)
|
|
108
|
+
.groupBy(feedback.status);
|
|
109
|
+
|
|
110
|
+
// Recent feedback (last 5)
|
|
111
|
+
const recentFeedback = await db
|
|
112
|
+
.select({
|
|
113
|
+
feedbackId: feedback.feedbackId,
|
|
114
|
+
title: feedback.title,
|
|
115
|
+
type: feedback.type,
|
|
116
|
+
status: feedback.status,
|
|
117
|
+
priority: feedback.priority,
|
|
118
|
+
createdAt: feedback.createdAt,
|
|
119
|
+
})
|
|
120
|
+
.from(feedback)
|
|
121
|
+
.where(userCondition)
|
|
122
|
+
.orderBy(sql`${feedback.createdAt} DESC`)
|
|
123
|
+
.limit(5);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
totalFeedback: totalResult?.count ?? 0,
|
|
127
|
+
pendingFeedback: pendingResult?.count ?? 0,
|
|
128
|
+
weeklyFeedback: weeklyResult?.count ?? 0,
|
|
129
|
+
resolvedFeedback: resolvedResult?.count ?? 0,
|
|
130
|
+
statusDistribution: statusDistribution.map((s) => ({
|
|
131
|
+
status: s.status,
|
|
132
|
+
count: Number(s.count),
|
|
133
|
+
})),
|
|
134
|
+
recentFeedback,
|
|
135
|
+
};
|
|
136
|
+
}
|
package/lib/db/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
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 { drizzle } from "drizzle-orm/node-postgres";
|
|
19
|
+
import { Pool } from "pg";
|
|
20
|
+
import * as schema from "./schema";
|
|
21
|
+
|
|
22
|
+
const databaseUrl = process.env.DATABASE_URL || "postgres://postgres:postgres@localhost:5432/postgres";
|
|
23
|
+
|
|
24
|
+
const globalForDb = globalThis as unknown as {
|
|
25
|
+
pool: Pool | undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const pool =
|
|
29
|
+
globalForDb.pool ??
|
|
30
|
+
(databaseUrl
|
|
31
|
+
? new Pool({
|
|
32
|
+
connectionString: databaseUrl,
|
|
33
|
+
max: 10, // Default to 10 connections
|
|
34
|
+
})
|
|
35
|
+
: undefined);
|
|
36
|
+
|
|
37
|
+
if (process.env.NODE_ENV !== "production") {
|
|
38
|
+
globalForDb.pool = pool;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const db = pool ? drizzle(pool, { schema }) : null;
|
|
@@ -0,0 +1,49 @@
|
|
|
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, expect, it } from "bun:test";
|
|
19
|
+
import { runMigrations } from "./migrate";
|
|
20
|
+
|
|
21
|
+
describe("runMigrations", () => {
|
|
22
|
+
it("throws when database is missing", async () => {
|
|
23
|
+
await expect(runMigrations({ database: null })).rejects.toThrow(
|
|
24
|
+
"Database connection not configured"
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("calls migrator with default migrations folder", async () => {
|
|
29
|
+
const previousUrl = process.env.DATABASE_URL;
|
|
30
|
+
process.env.DATABASE_URL = "postgres://test";
|
|
31
|
+
let called = false;
|
|
32
|
+
const migrateFn = async (_db: unknown, config: { migrationsFolder: string }) => {
|
|
33
|
+
called = true;
|
|
34
|
+
expect(config.migrationsFolder).toBe("./lib/db/migrations");
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
await runMigrations({
|
|
38
|
+
database: {} as unknown,
|
|
39
|
+
migrateFn,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(called).toBe(true);
|
|
43
|
+
if (previousUrl === undefined) {
|
|
44
|
+
delete process.env.DATABASE_URL;
|
|
45
|
+
} else {
|
|
46
|
+
process.env.DATABASE_URL = previousUrl;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
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 { migrate } from "drizzle-orm/node-postgres/migrator";
|
|
19
|
+
import { db } from "@/lib/db";
|
|
20
|
+
import { logger } from "@/lib/logger";
|
|
21
|
+
|
|
22
|
+
type Database = NonNullable<typeof db>;
|
|
23
|
+
|
|
24
|
+
type RunMigrationsOptions = {
|
|
25
|
+
database?: Database | null;
|
|
26
|
+
migrateFn?: typeof migrate;
|
|
27
|
+
migrationsFolder?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export async function runMigrations(options: RunMigrationsOptions = {}) {
|
|
31
|
+
const database = options.database === undefined ? db : options.database;
|
|
32
|
+
const migrateFn = options.migrateFn ?? migrate;
|
|
33
|
+
const migrationsFolder = options.migrationsFolder ?? "./lib/db/migrations";
|
|
34
|
+
|
|
35
|
+
if (!database || !process.env.DATABASE_URL) {
|
|
36
|
+
logger.error("Database connection not configured");
|
|
37
|
+
throw new Error("Database connection not configured");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
logger.info("Running database migrations...");
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await migrateFn(database, { migrationsFolder });
|
|
44
|
+
logger.info("Migrations completed successfully");
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger.error({ err: error }, "Migration failed");
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
if ((import.meta as any).main) {
|
|
53
|
+
runMigrations()
|
|
54
|
+
.then(() => {
|
|
55
|
+
console.log("✅ Migrations completed");
|
|
56
|
+
process.exit(0);
|
|
57
|
+
})
|
|
58
|
+
.catch((error) => {
|
|
59
|
+
console.error("❌ Migration failed:", error);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
File without changes
|