@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,164 @@
|
|
|
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 { db } from "@/lib/db";
|
|
19
|
+
import { apiKeys } from "@/lib/db/schema";
|
|
20
|
+
import { eq, and } from "drizzle-orm";
|
|
21
|
+
import { randomBytes, createHash } from "crypto";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate API Key
|
|
25
|
+
* Format: echo_{orgId}_{random}
|
|
26
|
+
*/
|
|
27
|
+
export function generateApiKey(organizationId: string): string {
|
|
28
|
+
const random = randomBytes(32)
|
|
29
|
+
.toString("base64")
|
|
30
|
+
.replace(/[^a-zA-Z0-9]/g, "")
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.slice(0, 32);
|
|
33
|
+
return `echo_${organizationId.slice(0, 8)}_${random}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Hash API Key (SHA-256)
|
|
38
|
+
*/
|
|
39
|
+
export function hashApiKey(key: string): string {
|
|
40
|
+
return createHash("sha256").update(key).digest("hex");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract key prefix for display
|
|
45
|
+
*/
|
|
46
|
+
export function extractApiKeyPrefix(key: string): string {
|
|
47
|
+
return key.substring(0, 20);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create API Key
|
|
52
|
+
*/
|
|
53
|
+
export async function createApiKey(
|
|
54
|
+
organizationId: string,
|
|
55
|
+
name: string,
|
|
56
|
+
expiresInDays?: number,
|
|
57
|
+
) {
|
|
58
|
+
if (!db) {
|
|
59
|
+
throw new Error("Database not configured");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rawKey = generateApiKey(organizationId);
|
|
63
|
+
const hashedKey = hashApiKey(rawKey);
|
|
64
|
+
const prefix = extractApiKeyPrefix(rawKey);
|
|
65
|
+
|
|
66
|
+
let expiresAt: Date | undefined;
|
|
67
|
+
if (expiresInDays) {
|
|
68
|
+
expiresAt = new Date();
|
|
69
|
+
expiresAt.setDate(expiresAt.getDate() + expiresInDays);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const [record] = await db
|
|
73
|
+
.insert(apiKeys)
|
|
74
|
+
.values({
|
|
75
|
+
organizationId,
|
|
76
|
+
name,
|
|
77
|
+
hashedKey,
|
|
78
|
+
prefix,
|
|
79
|
+
})
|
|
80
|
+
.returning();
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
key: rawKey,
|
|
84
|
+
record,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* List organization's API Keys
|
|
90
|
+
*/
|
|
91
|
+
export async function listApiKeys(organizationId: string) {
|
|
92
|
+
if (!db) {
|
|
93
|
+
throw new Error("Database not configured");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return db
|
|
97
|
+
.select()
|
|
98
|
+
.from(apiKeys)
|
|
99
|
+
.where(eq(apiKeys.organizationId, organizationId))
|
|
100
|
+
.orderBy(apiKeys.createdAt);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Delete API Key
|
|
105
|
+
*/
|
|
106
|
+
export async function deleteApiKey(
|
|
107
|
+
keyId: number,
|
|
108
|
+
organizationId: string,
|
|
109
|
+
): Promise<void> {
|
|
110
|
+
if (!db) {
|
|
111
|
+
throw new Error("Database not configured");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await db
|
|
115
|
+
.delete(apiKeys)
|
|
116
|
+
.where(and(eq(apiKeys.keyId, keyId), eq(apiKeys.organizationId, organizationId)));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Toggle API Key disabled status
|
|
121
|
+
*/
|
|
122
|
+
export async function toggleApiKey(
|
|
123
|
+
keyId: number,
|
|
124
|
+
organizationId: string,
|
|
125
|
+
disabled: boolean,
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
if (!db) {
|
|
128
|
+
throw new Error("Database not configured");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await db
|
|
132
|
+
.update(apiKeys)
|
|
133
|
+
.set({ disabled })
|
|
134
|
+
.where(and(eq(apiKeys.keyId, keyId), eq(apiKeys.organizationId, organizationId)));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Verify API Key and return record if valid
|
|
139
|
+
*/
|
|
140
|
+
export async function verifyApiKey(key: string) {
|
|
141
|
+
if (!db) {
|
|
142
|
+
throw new Error("Database not configured");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const hashedKey = hashApiKey(key);
|
|
146
|
+
|
|
147
|
+
const keyRecord = await db
|
|
148
|
+
.select()
|
|
149
|
+
.from(apiKeys)
|
|
150
|
+
.where(eq(apiKeys.hashedKey, hashedKey))
|
|
151
|
+
.limit(1);
|
|
152
|
+
|
|
153
|
+
if (keyRecord.length === 0) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const record = keyRecord[0];
|
|
158
|
+
|
|
159
|
+
if (record.disabled) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return record;
|
|
164
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
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 { execSync } from "child_process";
|
|
19
|
+
import { readdirSync, statSync, unlinkSync, mkdirSync, existsSync } from "fs";
|
|
20
|
+
import { join } from "path";
|
|
21
|
+
import { logger } from "@/lib/logger";
|
|
22
|
+
|
|
23
|
+
export interface BackupConfig {
|
|
24
|
+
backupDir: string;
|
|
25
|
+
retentionDays: number;
|
|
26
|
+
databaseUrl: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BackupInfo {
|
|
30
|
+
file: string;
|
|
31
|
+
size: string;
|
|
32
|
+
date: Date;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function createBackup(config: BackupConfig): Promise<string> {
|
|
36
|
+
const { backupDir, databaseUrl } = config;
|
|
37
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
38
|
+
const backupFile = join(backupDir, `echo-${timestamp}.sql`);
|
|
39
|
+
|
|
40
|
+
logger.info({ backupFile }, "Creating database backup...");
|
|
41
|
+
|
|
42
|
+
if (!existsSync(backupDir)) {
|
|
43
|
+
mkdirSync(backupDir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
execSync(`pg_dump "${databaseUrl}" > "${backupFile}"`, {
|
|
48
|
+
stdio: "inherit",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
execSync(`gzip "${backupFile}"`, {
|
|
52
|
+
stdio: "inherit",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const compressedFile = `${backupFile}.gz`;
|
|
56
|
+
const stats = statSync(compressedFile);
|
|
57
|
+
|
|
58
|
+
logger.info(
|
|
59
|
+
{
|
|
60
|
+
backupFile: compressedFile,
|
|
61
|
+
size: `${(stats.size / 1024 / 1024).toFixed(2)} MB`,
|
|
62
|
+
},
|
|
63
|
+
"Backup created successfully"
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return compressedFile;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger.error({ err: error }, "Backup failed");
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function cleanupOldBackups(config: BackupConfig): Promise<number> {
|
|
74
|
+
const { backupDir, retentionDays } = config;
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const maxAge = retentionDays * 24 * 60 * 60 * 1000;
|
|
77
|
+
|
|
78
|
+
let deletedCount = 0;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
if (!existsSync(backupDir)) {
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const files = readdirSync(backupDir);
|
|
86
|
+
const backupFiles = files.filter(
|
|
87
|
+
(f) => f.startsWith("echo-") && f.endsWith(".sql.gz")
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
for (const file of backupFiles) {
|
|
91
|
+
const filePath = join(backupDir, file);
|
|
92
|
+
const stats = statSync(filePath);
|
|
93
|
+
const age = now - stats.mtimeMs;
|
|
94
|
+
|
|
95
|
+
if (age > maxAge) {
|
|
96
|
+
unlinkSync(filePath);
|
|
97
|
+
deletedCount++;
|
|
98
|
+
logger.info(
|
|
99
|
+
{ file, age: `${Math.floor(age / 86400000)} days` },
|
|
100
|
+
"Deleted old backup"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (deletedCount > 0) {
|
|
106
|
+
logger.info({ deletedCount }, "Cleanup completed");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return deletedCount;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
logger.error({ err: error }, "Cleanup failed");
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function listBackups(config: BackupConfig): Promise<BackupInfo[]> {
|
|
117
|
+
const { backupDir } = config;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
if (!existsSync(backupDir)) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const files = readdirSync(backupDir);
|
|
125
|
+
const backupFiles = files.filter(
|
|
126
|
+
(f) => f.startsWith("echo-") && f.endsWith(".sql.gz")
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return backupFiles
|
|
130
|
+
.map((file) => {
|
|
131
|
+
const filePath = join(backupDir, file);
|
|
132
|
+
const stats = statSync(filePath);
|
|
133
|
+
return {
|
|
134
|
+
file,
|
|
135
|
+
size: `${(stats.size / 1024 / 1024).toFixed(2)} MB`,
|
|
136
|
+
date: stats.mtime,
|
|
137
|
+
};
|
|
138
|
+
})
|
|
139
|
+
.sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
140
|
+
} catch (error) {
|
|
141
|
+
logger.error({ err: error }, "Failed to list backups");
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function backupDatabase(config: BackupConfig): Promise<void> {
|
|
147
|
+
logger.info("Starting backup process...");
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
await createBackup(config);
|
|
151
|
+
await cleanupOldBackups(config);
|
|
152
|
+
|
|
153
|
+
const backups = await listBackups(config);
|
|
154
|
+
logger.info(
|
|
155
|
+
{
|
|
156
|
+
totalBackups: backups.length,
|
|
157
|
+
backups: backups.slice(0, 5),
|
|
158
|
+
},
|
|
159
|
+
"Backup process completed"
|
|
160
|
+
);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
logger.error({ err: error }, "Backup process failed");
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function getBackupConfig(): BackupConfig {
|
|
168
|
+
return {
|
|
169
|
+
backupDir: process.env.BACKUP_DIR || "./backups",
|
|
170
|
+
retentionDays: parseInt(process.env.BACKUP_RETENTION_DAYS || "30"),
|
|
171
|
+
databaseUrl: process.env.DATABASE_URL!,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
const statusLabels: Record<string, string> = {
|
|
19
|
+
new: "新接收",
|
|
20
|
+
"in-progress": "处理中",
|
|
21
|
+
planned: "已规划",
|
|
22
|
+
completed: "已完成",
|
|
23
|
+
closed: "已关闭",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const baseStyles = `
|
|
27
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
28
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
29
|
+
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 10px 10px 0 0; }
|
|
30
|
+
.content { background: #f9f9f9; padding: 30px; border-radius: 0 0 10px 10px; }
|
|
31
|
+
.status-badge { display: inline-block; padding: 5px 15px; border-radius: 20px; font-size: 14px; font-weight: bold; }
|
|
32
|
+
.status-new { background: #dbeafe; color: #1e40af; }
|
|
33
|
+
.status-in-progress { background: #fef3c7; color: #92400e; }
|
|
34
|
+
.status-planned { background: #e9d5ff; color: #6b21a8; }
|
|
35
|
+
.status-completed { background: #d1fae5; color: #065f46; }
|
|
36
|
+
.status-closed { background: #e5e7eb; color: #374151; }
|
|
37
|
+
.button { display: inline-block; background: #667eea; color: white; padding: 12px 30px; border-radius: 5px; text-decoration: none; font-weight: bold; }
|
|
38
|
+
.comment-box { background: white; padding: 15px; border-left: 4px solid #667eea; border-radius: 4px; margin: 20px 0; }
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
interface StatusChangeEmailData {
|
|
42
|
+
feedbackTitle: string;
|
|
43
|
+
feedbackId: number;
|
|
44
|
+
oldStatus: string;
|
|
45
|
+
newStatus: string;
|
|
46
|
+
feedbackUrl: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function generateStatusChangeEmail(
|
|
50
|
+
data: StatusChangeEmailData,
|
|
51
|
+
): { subject: string; html: string } {
|
|
52
|
+
const subject = `反馈状态更新:${data.feedbackTitle}`;
|
|
53
|
+
|
|
54
|
+
const html = `
|
|
55
|
+
<!DOCTYPE html>
|
|
56
|
+
<html>
|
|
57
|
+
<head>
|
|
58
|
+
<meta charset="utf-8">
|
|
59
|
+
<style>${baseStyles}</style>
|
|
60
|
+
</head>
|
|
61
|
+
<body>
|
|
62
|
+
<div class="container">
|
|
63
|
+
<div class="header">
|
|
64
|
+
<h1>反馈状态更新</h1>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="content">
|
|
67
|
+
<p>您好,</p>
|
|
68
|
+
<p>您提交的反馈状态已更新。</p>
|
|
69
|
+
|
|
70
|
+
<div style="background: white; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
|
71
|
+
<h3 style="margin-top: 0;">${escapeHtml(data.feedbackTitle)}</h3>
|
|
72
|
+
<p style="color: #666;">
|
|
73
|
+
状态从
|
|
74
|
+
<span class="status-badge status-${data.oldStatus}">${statusLabels[data.oldStatus] || data.oldStatus}</span>
|
|
75
|
+
变更为
|
|
76
|
+
<span class="status-badge status-${data.newStatus}">${statusLabels[data.newStatus] || data.newStatus}</span>
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<p style="text-align: center; margin: 30px 0;">
|
|
81
|
+
<a href="${data.feedbackUrl}" class="button">查看反馈详情</a>
|
|
82
|
+
</p>
|
|
83
|
+
|
|
84
|
+
<p style="font-size: 12px; color: #999; text-align: center;">
|
|
85
|
+
如果您不想收到此类通知,请前往设置页面调整通知偏好。
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</body>
|
|
90
|
+
</html>
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
return { subject, html };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface NewCommentEmailData {
|
|
97
|
+
feedbackTitle: string;
|
|
98
|
+
feedbackId: number;
|
|
99
|
+
authorName: string;
|
|
100
|
+
commentContent: string;
|
|
101
|
+
feedbackUrl: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function generateNewCommentEmail(
|
|
105
|
+
data: NewCommentEmailData,
|
|
106
|
+
): { subject: string; html: string } {
|
|
107
|
+
const subject = `新评论:${data.feedbackTitle}`;
|
|
108
|
+
|
|
109
|
+
const html = `
|
|
110
|
+
<!DOCTYPE html>
|
|
111
|
+
<html>
|
|
112
|
+
<head>
|
|
113
|
+
<meta charset="utf-8">
|
|
114
|
+
<style>${baseStyles}</style>
|
|
115
|
+
</head>
|
|
116
|
+
<body>
|
|
117
|
+
<div class="container">
|
|
118
|
+
<div class="header">
|
|
119
|
+
<h1>新评论通知</h1>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="content">
|
|
122
|
+
<p>您好,</p>
|
|
123
|
+
<p><strong>${escapeHtml(data.authorName)}</strong> 在您关注的反馈中发表了新评论。</p>
|
|
124
|
+
|
|
125
|
+
<div style="background: white; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
|
126
|
+
<h3 style="margin-top: 0;">${escapeHtml(data.feedbackTitle)}</h3>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div class="comment-box">
|
|
130
|
+
<p style="margin: 0;">${escapeHtml(data.commentContent)}</p>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<p style="text-align: center; margin: 30px 0;">
|
|
134
|
+
<a href="${data.feedbackUrl}" class="button">查看并回复</a>
|
|
135
|
+
</p>
|
|
136
|
+
|
|
137
|
+
<p style="font-size: 12px; color: #999; text-align: center;">
|
|
138
|
+
如果您不想收到此类通知,请前往设置页面调整通知偏好。
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</body>
|
|
143
|
+
</html>
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
return { subject, html };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function escapeHtml(text: string): string {
|
|
150
|
+
const map: Record<string, string> = {
|
|
151
|
+
"&": "&",
|
|
152
|
+
"<": "<",
|
|
153
|
+
">": ">",
|
|
154
|
+
'"': """,
|
|
155
|
+
"'": "'",
|
|
156
|
+
};
|
|
157
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
158
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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 { Resend } from "resend";
|
|
19
|
+
import { logger } from "@/lib/logger";
|
|
20
|
+
|
|
21
|
+
const resend = process.env.RESEND_API_KEY
|
|
22
|
+
? new Resend(process.env.RESEND_API_KEY)
|
|
23
|
+
: null;
|
|
24
|
+
|
|
25
|
+
export type SendEmailPayload = {
|
|
26
|
+
to: string;
|
|
27
|
+
subject: string;
|
|
28
|
+
html: string;
|
|
29
|
+
text?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type SendEmailResult = {
|
|
33
|
+
success: boolean;
|
|
34
|
+
error?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export async function sendEmail(
|
|
38
|
+
payload: SendEmailPayload,
|
|
39
|
+
): Promise<SendEmailResult> {
|
|
40
|
+
const { to, subject, html, text } = payload;
|
|
41
|
+
|
|
42
|
+
if (process.env.NODE_ENV !== "test") {
|
|
43
|
+
logger.info({ to, subject }, "Email send requested");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!resend) {
|
|
47
|
+
if (process.env.NODE_ENV !== "test") {
|
|
48
|
+
logger.warn("RESEND_API_KEY not set, skipping email send");
|
|
49
|
+
}
|
|
50
|
+
return { success: true };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await resend.emails.send({
|
|
55
|
+
from: process.env.RESEND_FROM_EMAIL || "noreply@echo.app",
|
|
56
|
+
to,
|
|
57
|
+
subject,
|
|
58
|
+
html,
|
|
59
|
+
text,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return { success: true };
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
65
|
+
logger.error({ error: errorMsg, to, subject }, "Failed to send email");
|
|
66
|
+
return { success: false, error: errorMsg };
|
|
67
|
+
}
|
|
68
|
+
}
|