@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,948 @@
|
|
|
1
|
+
# Settings Center Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Consolidate all settings into a unified Settings Center accessible from user dropdown menu.
|
|
6
|
+
|
|
7
|
+
**Architecture:**
|
|
8
|
+
- Remove settings links from main navigation, add user dropdown menu with Settings/Logout
|
|
9
|
+
- Create `/settings` layout with left sidebar navigation and right content area
|
|
10
|
+
- Reuse existing notification preferences component, create new profile/appearance pages
|
|
11
|
+
|
|
12
|
+
**Tech Stack:** Next.js App Router, Radix UI DropdownMenu, Tailwind CSS
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Task 1: Simplify Sidebar Navigation
|
|
17
|
+
|
|
18
|
+
**Files:**
|
|
19
|
+
- Modify: `components/layout/sidebar.tsx`
|
|
20
|
+
|
|
21
|
+
**Step 1: Remove settings-related nav items from navigation**
|
|
22
|
+
|
|
23
|
+
Replace lines 55-63 with:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
const navItems = [
|
|
27
|
+
{ href: "/dashboard", label: "仪表盘", icon: LayoutDashboard },
|
|
28
|
+
{ href: "/feedback", label: "反馈管理", icon: MessageSquare },
|
|
29
|
+
];
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Remove `adminNavItems` array entirely and its rendering (lines 61-63 and 110-124).
|
|
33
|
+
|
|
34
|
+
**Step 2: Run build to verify no errors**
|
|
35
|
+
|
|
36
|
+
Run: `bun run build 2>&1 | tail -10`
|
|
37
|
+
Expected: Build succeeds
|
|
38
|
+
|
|
39
|
+
**Step 3: Commit**
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
git add components/layout/sidebar.tsx
|
|
43
|
+
git commit -m "refactor: remove settings links from main navigation"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Task 2: Add User Dropdown Menu to Sidebar
|
|
49
|
+
|
|
50
|
+
**Files:**
|
|
51
|
+
- Modify: `components/layout/sidebar.tsx`
|
|
52
|
+
|
|
53
|
+
**Step 1: Add dropdown menu imports**
|
|
54
|
+
|
|
55
|
+
Add to imports:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import {
|
|
59
|
+
DropdownMenu,
|
|
60
|
+
DropdownMenuContent,
|
|
61
|
+
DropdownMenuItem,
|
|
62
|
+
DropdownMenuSeparator,
|
|
63
|
+
DropdownMenuTrigger,
|
|
64
|
+
} from "@/components/ui/dropdown-menu";
|
|
65
|
+
import { ChevronUp } from "lucide-react";
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Step 2: Replace user profile section (lines 165-191)**
|
|
69
|
+
|
|
70
|
+
Replace the entire `{/* User Profile */}` section with:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
{/* User Profile Dropdown */}
|
|
74
|
+
<div className="border-t p-4">
|
|
75
|
+
<DropdownMenu>
|
|
76
|
+
<DropdownMenuTrigger asChild>
|
|
77
|
+
<button className="flex w-full items-center gap-3 rounded-lg px-2 py-2 hover:bg-muted transition-colors">
|
|
78
|
+
<Avatar className="h-9 w-9">
|
|
79
|
+
<AvatarImage src={user.image || undefined} alt={user.name} />
|
|
80
|
+
<AvatarFallback>{user.name.slice(0, 2).toUpperCase()}</AvatarFallback>
|
|
81
|
+
</Avatar>
|
|
82
|
+
<div className="flex-1 min-w-0 text-left">
|
|
83
|
+
<p className="text-sm font-medium truncate">{user.name}</p>
|
|
84
|
+
<p className="text-xs text-muted-foreground truncate">{user.email}</p>
|
|
85
|
+
</div>
|
|
86
|
+
<ChevronUp className="h-4 w-4 text-muted-foreground" />
|
|
87
|
+
</button>
|
|
88
|
+
</DropdownMenuTrigger>
|
|
89
|
+
<DropdownMenuContent align="end" className="w-56">
|
|
90
|
+
<DropdownMenuItem asChild>
|
|
91
|
+
<Link href="/settings" onClick={handleLinkClick}>
|
|
92
|
+
<Settings className="mr-2 h-4 w-4" />
|
|
93
|
+
设置
|
|
94
|
+
</Link>
|
|
95
|
+
</DropdownMenuItem>
|
|
96
|
+
<DropdownMenuSeparator />
|
|
97
|
+
<DropdownMenuItem onClick={handleLogout}>
|
|
98
|
+
<LogOut className="mr-2 h-4 w-4" />
|
|
99
|
+
退出登录
|
|
100
|
+
</DropdownMenuItem>
|
|
101
|
+
</DropdownMenuContent>
|
|
102
|
+
</DropdownMenu>
|
|
103
|
+
</div>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Step 3: Remove unused imports**
|
|
107
|
+
|
|
108
|
+
Remove `Badge` and `Building2` from imports (no longer used).
|
|
109
|
+
|
|
110
|
+
**Step 4: Remove unused code**
|
|
111
|
+
|
|
112
|
+
Remove `roleLabels` object and `isAdmin` variable if no longer used.
|
|
113
|
+
|
|
114
|
+
**Step 5: Run build to verify**
|
|
115
|
+
|
|
116
|
+
Run: `bun run build 2>&1 | tail -10`
|
|
117
|
+
Expected: Build succeeds
|
|
118
|
+
|
|
119
|
+
**Step 6: Commit**
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
git add components/layout/sidebar.tsx
|
|
123
|
+
git commit -m "feat: add user dropdown menu with settings and logout"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Task 3: Create Settings Layout with Sidebar
|
|
129
|
+
|
|
130
|
+
**Files:**
|
|
131
|
+
- Create: `app/(dashboard)/settings/layout.tsx`
|
|
132
|
+
- Create: `components/settings/settings-sidebar.tsx`
|
|
133
|
+
- Create: `components/settings/index.ts`
|
|
134
|
+
|
|
135
|
+
**Step 1: Create settings sidebar component**
|
|
136
|
+
|
|
137
|
+
Create `components/settings/settings-sidebar.tsx`:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
"use client";
|
|
141
|
+
|
|
142
|
+
import Link from "next/link";
|
|
143
|
+
import { usePathname } from "next/navigation";
|
|
144
|
+
import { cn } from "@/lib/utils";
|
|
145
|
+
import { User, Bell, Palette, Building2, Key } from "lucide-react";
|
|
146
|
+
import type { UserRole } from "@/lib/auth/permissions";
|
|
147
|
+
|
|
148
|
+
interface SettingsSidebarProps {
|
|
149
|
+
userRole: UserRole;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function SettingsSidebar({ userRole }: SettingsSidebarProps) {
|
|
153
|
+
const pathname = usePathname();
|
|
154
|
+
const isAdmin = userRole === "admin";
|
|
155
|
+
const isAdminOrPM = isAdmin || userRole === "product_manager";
|
|
156
|
+
|
|
157
|
+
const menuItems = [
|
|
158
|
+
{ href: "/settings/profile", label: "个人资料", icon: User },
|
|
159
|
+
{ href: "/settings/notifications", label: "通知设置", icon: Bell },
|
|
160
|
+
{ href: "/settings/appearance", label: "外观设置", icon: Palette },
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
const adminItems = [
|
|
164
|
+
{ href: "/settings/organization", label: "组织管理", icon: Building2, show: isAdmin },
|
|
165
|
+
{ href: "/settings/api-keys", label: "API 密钥", icon: Key, show: isAdminOrPM },
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<nav className="w-56 shrink-0 border-r bg-muted/30 p-4">
|
|
170
|
+
<h2 className="mb-4 px-2 text-lg font-semibold">设置</h2>
|
|
171
|
+
<div className="space-y-1">
|
|
172
|
+
{menuItems.map((item) => (
|
|
173
|
+
<Link
|
|
174
|
+
key={item.href}
|
|
175
|
+
href={item.href}
|
|
176
|
+
className={cn(
|
|
177
|
+
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors hover:bg-muted",
|
|
178
|
+
pathname === item.href && "bg-muted font-medium"
|
|
179
|
+
)}
|
|
180
|
+
>
|
|
181
|
+
<item.icon className="h-4 w-4" />
|
|
182
|
+
{item.label}
|
|
183
|
+
</Link>
|
|
184
|
+
))}
|
|
185
|
+
{adminItems.filter(item => item.show).map((item) => (
|
|
186
|
+
<Link
|
|
187
|
+
key={item.href}
|
|
188
|
+
href={item.href}
|
|
189
|
+
className={cn(
|
|
190
|
+
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors hover:bg-muted",
|
|
191
|
+
pathname === item.href && "bg-muted font-medium"
|
|
192
|
+
)}
|
|
193
|
+
>
|
|
194
|
+
<item.icon className="h-4 w-4" />
|
|
195
|
+
{item.label}
|
|
196
|
+
</Link>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
</nav>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Step 2: Create settings index export**
|
|
205
|
+
|
|
206
|
+
Create `components/settings/index.ts`:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
export { SettingsSidebar } from "./settings-sidebar";
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Step 3: Create settings layout**
|
|
213
|
+
|
|
214
|
+
Create `app/(dashboard)/settings/layout.tsx`:
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
import { headers } from "next/headers";
|
|
218
|
+
import { redirect } from "next/navigation";
|
|
219
|
+
import { auth } from "@/lib/auth/config";
|
|
220
|
+
import { SettingsSidebar } from "@/components/settings";
|
|
221
|
+
import type { UserRole } from "@/lib/auth/permissions";
|
|
222
|
+
|
|
223
|
+
export default async function SettingsLayout({
|
|
224
|
+
children,
|
|
225
|
+
}: {
|
|
226
|
+
children: React.ReactNode;
|
|
227
|
+
}) {
|
|
228
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
229
|
+
|
|
230
|
+
if (!session?.user) {
|
|
231
|
+
redirect("/login");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const userRole = (session.user as { role?: string }).role as UserRole || "customer";
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<div className="flex min-h-[calc(100vh-3.5rem)]">
|
|
238
|
+
<SettingsSidebar userRole={userRole} />
|
|
239
|
+
<main className="flex-1 p-6">{children}</main>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Step 4: Run build to verify**
|
|
246
|
+
|
|
247
|
+
Run: `bun run build 2>&1 | tail -10`
|
|
248
|
+
Expected: Build succeeds
|
|
249
|
+
|
|
250
|
+
**Step 5: Commit**
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
git add components/settings/settings-sidebar.tsx components/settings/index.ts app/\(dashboard\)/settings/layout.tsx
|
|
254
|
+
git commit -m "feat: add settings layout with sidebar navigation"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Task 4: Create Settings Root Page (Redirect)
|
|
260
|
+
|
|
261
|
+
**Files:**
|
|
262
|
+
- Create: `app/(dashboard)/settings/page.tsx`
|
|
263
|
+
|
|
264
|
+
**Step 1: Create redirect page**
|
|
265
|
+
|
|
266
|
+
Create `app/(dashboard)/settings/page.tsx`:
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
import { redirect } from "next/navigation";
|
|
270
|
+
|
|
271
|
+
export default function SettingsPage() {
|
|
272
|
+
redirect("/settings/profile");
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Step 2: Run build to verify**
|
|
277
|
+
|
|
278
|
+
Run: `bun run build 2>&1 | tail -10`
|
|
279
|
+
Expected: Build succeeds
|
|
280
|
+
|
|
281
|
+
**Step 3: Commit**
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
git add app/\(dashboard\)/settings/page.tsx
|
|
285
|
+
git commit -m "feat: add settings root page with redirect to profile"
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Task 5: Create Profile Settings Page
|
|
291
|
+
|
|
292
|
+
**Files:**
|
|
293
|
+
- Create: `app/(dashboard)/settings/profile/page.tsx`
|
|
294
|
+
- Create: `components/settings/profile-form.tsx`
|
|
295
|
+
|
|
296
|
+
**Step 1: Create profile form component**
|
|
297
|
+
|
|
298
|
+
Create `components/settings/profile-form.tsx`:
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
"use client";
|
|
302
|
+
|
|
303
|
+
import { useState } from "react";
|
|
304
|
+
import { Button } from "@/components/ui/button";
|
|
305
|
+
import { Input } from "@/components/ui/input";
|
|
306
|
+
import { Label } from "@/components/ui/label";
|
|
307
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
308
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
309
|
+
|
|
310
|
+
interface ProfileFormProps {
|
|
311
|
+
user: {
|
|
312
|
+
id: string;
|
|
313
|
+
name: string;
|
|
314
|
+
email: string;
|
|
315
|
+
image?: string | null;
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function ProfileForm({ user }: ProfileFormProps) {
|
|
320
|
+
const [name, setName] = useState(user.name);
|
|
321
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
322
|
+
|
|
323
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
324
|
+
e.preventDefault();
|
|
325
|
+
setIsLoading(true);
|
|
326
|
+
// TODO: Implement profile update API
|
|
327
|
+
setTimeout(() => setIsLoading(false), 1000);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<div className="space-y-6">
|
|
332
|
+
<Card>
|
|
333
|
+
<CardHeader>
|
|
334
|
+
<CardTitle>个人资料</CardTitle>
|
|
335
|
+
<CardDescription>管理您的账户信息</CardDescription>
|
|
336
|
+
</CardHeader>
|
|
337
|
+
<CardContent>
|
|
338
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
339
|
+
<div className="flex items-center gap-4">
|
|
340
|
+
<Avatar className="h-20 w-20">
|
|
341
|
+
<AvatarImage src={user.image || undefined} alt={user.name} />
|
|
342
|
+
<AvatarFallback className="text-lg">
|
|
343
|
+
{user.name.slice(0, 2).toUpperCase()}
|
|
344
|
+
</AvatarFallback>
|
|
345
|
+
</Avatar>
|
|
346
|
+
<Button type="button" variant="outline" disabled>
|
|
347
|
+
更换头像
|
|
348
|
+
</Button>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
<div className="space-y-2">
|
|
352
|
+
<Label htmlFor="name">姓名</Label>
|
|
353
|
+
<Input
|
|
354
|
+
id="name"
|
|
355
|
+
value={name}
|
|
356
|
+
onChange={(e) => setName(e.target.value)}
|
|
357
|
+
placeholder="您的姓名"
|
|
358
|
+
/>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<div className="space-y-2">
|
|
362
|
+
<Label htmlFor="email">邮箱</Label>
|
|
363
|
+
<Input
|
|
364
|
+
id="email"
|
|
365
|
+
value={user.email}
|
|
366
|
+
disabled
|
|
367
|
+
className="bg-muted"
|
|
368
|
+
/>
|
|
369
|
+
<p className="text-xs text-muted-foreground">邮箱地址不可修改</p>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
<Button type="submit" disabled={isLoading}>
|
|
373
|
+
{isLoading ? "保存中..." : "保存更改"}
|
|
374
|
+
</Button>
|
|
375
|
+
</form>
|
|
376
|
+
</CardContent>
|
|
377
|
+
</Card>
|
|
378
|
+
|
|
379
|
+
<Card>
|
|
380
|
+
<CardHeader>
|
|
381
|
+
<CardTitle>修改密码</CardTitle>
|
|
382
|
+
<CardDescription>更新您的登录密码</CardDescription>
|
|
383
|
+
</CardHeader>
|
|
384
|
+
<CardContent>
|
|
385
|
+
<Button variant="outline" disabled>
|
|
386
|
+
修改密码
|
|
387
|
+
</Button>
|
|
388
|
+
<p className="mt-2 text-xs text-muted-foreground">密码修改功能即将推出</p>
|
|
389
|
+
</CardContent>
|
|
390
|
+
</Card>
|
|
391
|
+
</div>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
**Step 2: Create profile page**
|
|
397
|
+
|
|
398
|
+
Create `app/(dashboard)/settings/profile/page.tsx`:
|
|
399
|
+
|
|
400
|
+
```tsx
|
|
401
|
+
import { headers } from "next/headers";
|
|
402
|
+
import { redirect } from "next/navigation";
|
|
403
|
+
import { auth } from "@/lib/auth/config";
|
|
404
|
+
import { ProfileForm } from "@/components/settings/profile-form";
|
|
405
|
+
|
|
406
|
+
export const metadata = {
|
|
407
|
+
title: "个人资料 - Echo",
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
export default async function ProfileSettingsPage() {
|
|
411
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
412
|
+
|
|
413
|
+
if (!session?.user) {
|
|
414
|
+
redirect("/login");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<div className="max-w-2xl">
|
|
419
|
+
<div className="mb-6">
|
|
420
|
+
<h1 className="text-2xl font-semibold">个人资料</h1>
|
|
421
|
+
<p className="text-muted-foreground">管理您的账户信息和偏好设置</p>
|
|
422
|
+
</div>
|
|
423
|
+
<ProfileForm user={session.user} />
|
|
424
|
+
</div>
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**Step 3: Update settings index export**
|
|
430
|
+
|
|
431
|
+
Update `components/settings/index.ts`:
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
export { SettingsSidebar } from "./settings-sidebar";
|
|
435
|
+
export { ProfileForm } from "./profile-form";
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**Step 4: Run build to verify**
|
|
439
|
+
|
|
440
|
+
Run: `bun run build 2>&1 | tail -10`
|
|
441
|
+
Expected: Build succeeds
|
|
442
|
+
|
|
443
|
+
**Step 5: Commit**
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
git add components/settings/profile-form.tsx components/settings/index.ts app/\(dashboard\)/settings/profile/page.tsx
|
|
447
|
+
git commit -m "feat: add profile settings page"
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Task 6: Update Notifications Page to Use New Layout
|
|
453
|
+
|
|
454
|
+
**Files:**
|
|
455
|
+
- Modify: `app/(dashboard)/settings/notifications/page.tsx`
|
|
456
|
+
|
|
457
|
+
**Step 1: Update notifications page**
|
|
458
|
+
|
|
459
|
+
Replace `app/(dashboard)/settings/notifications/page.tsx`:
|
|
460
|
+
|
|
461
|
+
```tsx
|
|
462
|
+
import { headers } from "next/headers";
|
|
463
|
+
import { redirect } from "next/navigation";
|
|
464
|
+
import { auth } from "@/lib/auth/config";
|
|
465
|
+
import { NotificationPreferences } from "@/components/settings/notification-preferences";
|
|
466
|
+
|
|
467
|
+
export const metadata = {
|
|
468
|
+
title: "通知设置 - Echo",
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
export default async function NotificationSettingsPage() {
|
|
472
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
473
|
+
|
|
474
|
+
if (!session?.user) {
|
|
475
|
+
redirect("/login");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return (
|
|
479
|
+
<div className="max-w-2xl">
|
|
480
|
+
<div className="mb-6">
|
|
481
|
+
<h1 className="text-2xl font-semibold">通知设置</h1>
|
|
482
|
+
<p className="text-muted-foreground">选择您希望接收的通知类型</p>
|
|
483
|
+
</div>
|
|
484
|
+
<NotificationPreferences />
|
|
485
|
+
</div>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**Step 2: Run build to verify**
|
|
491
|
+
|
|
492
|
+
Run: `bun run build 2>&1 | tail -10`
|
|
493
|
+
Expected: Build succeeds
|
|
494
|
+
|
|
495
|
+
**Step 3: Commit**
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
git add app/\(dashboard\)/settings/notifications/page.tsx
|
|
499
|
+
git commit -m "refactor: update notifications page to use settings layout"
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Task 7: Create Appearance Settings Page
|
|
505
|
+
|
|
506
|
+
**Files:**
|
|
507
|
+
- Create: `app/(dashboard)/settings/appearance/page.tsx`
|
|
508
|
+
- Create: `components/settings/appearance-form.tsx`
|
|
509
|
+
|
|
510
|
+
**Step 1: Create appearance form component**
|
|
511
|
+
|
|
512
|
+
Create `components/settings/appearance-form.tsx`:
|
|
513
|
+
|
|
514
|
+
```tsx
|
|
515
|
+
"use client";
|
|
516
|
+
|
|
517
|
+
import { useState } from "react";
|
|
518
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
519
|
+
import { Label } from "@/components/ui/label";
|
|
520
|
+
import { cn } from "@/lib/utils";
|
|
521
|
+
import { Monitor, Moon, Sun } from "lucide-react";
|
|
522
|
+
|
|
523
|
+
type Theme = "light" | "dark" | "system";
|
|
524
|
+
|
|
525
|
+
export function AppearanceForm() {
|
|
526
|
+
const [theme, setTheme] = useState<Theme>("system");
|
|
527
|
+
|
|
528
|
+
const themes: { value: Theme; label: string; icon: React.ElementType }[] = [
|
|
529
|
+
{ value: "light", label: "浅色", icon: Sun },
|
|
530
|
+
{ value: "dark", label: "深色", icon: Moon },
|
|
531
|
+
{ value: "system", label: "跟随系统", icon: Monitor },
|
|
532
|
+
];
|
|
533
|
+
|
|
534
|
+
return (
|
|
535
|
+
<Card>
|
|
536
|
+
<CardHeader>
|
|
537
|
+
<CardTitle>外观设置</CardTitle>
|
|
538
|
+
<CardDescription>自定义应用的显示风格</CardDescription>
|
|
539
|
+
</CardHeader>
|
|
540
|
+
<CardContent>
|
|
541
|
+
<div className="space-y-4">
|
|
542
|
+
<Label>主题</Label>
|
|
543
|
+
<div className="grid grid-cols-3 gap-4">
|
|
544
|
+
{themes.map((t) => (
|
|
545
|
+
<button
|
|
546
|
+
key={t.value}
|
|
547
|
+
onClick={() => setTheme(t.value)}
|
|
548
|
+
className={cn(
|
|
549
|
+
"flex flex-col items-center gap-2 rounded-lg border-2 p-4 transition-colors hover:bg-muted",
|
|
550
|
+
theme === t.value ? "border-primary" : "border-transparent"
|
|
551
|
+
)}
|
|
552
|
+
>
|
|
553
|
+
<t.icon className="h-6 w-6" />
|
|
554
|
+
<span className="text-sm font-medium">{t.label}</span>
|
|
555
|
+
</button>
|
|
556
|
+
))}
|
|
557
|
+
</div>
|
|
558
|
+
<p className="text-xs text-muted-foreground">
|
|
559
|
+
主题切换功能即将推出
|
|
560
|
+
</p>
|
|
561
|
+
</div>
|
|
562
|
+
</CardContent>
|
|
563
|
+
</Card>
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
**Step 2: Create appearance page**
|
|
569
|
+
|
|
570
|
+
Create `app/(dashboard)/settings/appearance/page.tsx`:
|
|
571
|
+
|
|
572
|
+
```tsx
|
|
573
|
+
import { AppearanceForm } from "@/components/settings/appearance-form";
|
|
574
|
+
|
|
575
|
+
export const metadata = {
|
|
576
|
+
title: "外观设置 - Echo",
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
export default function AppearanceSettingsPage() {
|
|
580
|
+
return (
|
|
581
|
+
<div className="max-w-2xl">
|
|
582
|
+
<div className="mb-6">
|
|
583
|
+
<h1 className="text-2xl font-semibold">外观设置</h1>
|
|
584
|
+
<p className="text-muted-foreground">自定义应用的显示风格</p>
|
|
585
|
+
</div>
|
|
586
|
+
<AppearanceForm />
|
|
587
|
+
</div>
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Step 3: Update settings index export**
|
|
593
|
+
|
|
594
|
+
Update `components/settings/index.ts`:
|
|
595
|
+
|
|
596
|
+
```ts
|
|
597
|
+
export { SettingsSidebar } from "./settings-sidebar";
|
|
598
|
+
export { ProfileForm } from "./profile-form";
|
|
599
|
+
export { AppearanceForm } from "./appearance-form";
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
**Step 4: Run build to verify**
|
|
603
|
+
|
|
604
|
+
Run: `bun run build 2>&1 | tail -10`
|
|
605
|
+
Expected: Build succeeds
|
|
606
|
+
|
|
607
|
+
**Step 5: Commit**
|
|
608
|
+
|
|
609
|
+
```bash
|
|
610
|
+
git add components/settings/appearance-form.tsx components/settings/index.ts app/\(dashboard\)/settings/appearance/page.tsx
|
|
611
|
+
git commit -m "feat: add appearance settings page"
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## Task 8: Create Organization Settings Page
|
|
617
|
+
|
|
618
|
+
**Files:**
|
|
619
|
+
- Create: `app/(dashboard)/settings/organization/page.tsx`
|
|
620
|
+
|
|
621
|
+
**Step 1: Create organization settings page**
|
|
622
|
+
|
|
623
|
+
Create `app/(dashboard)/settings/organization/page.tsx`:
|
|
624
|
+
|
|
625
|
+
```tsx
|
|
626
|
+
import { headers } from "next/headers";
|
|
627
|
+
import { redirect } from "next/navigation";
|
|
628
|
+
import { auth } from "@/lib/auth/config";
|
|
629
|
+
import { db } from "@/lib/db";
|
|
630
|
+
import { getUserOrganization } from "@/lib/auth/organization";
|
|
631
|
+
import { OrganizationForm } from "@/components/settings/organization-form";
|
|
632
|
+
import { OrganizationMembersList } from "@/components/settings/organization-members-list";
|
|
633
|
+
import { InviteMemberForm } from "@/components/settings/invite-member-form";
|
|
634
|
+
import type { UserRole } from "@/lib/auth/permissions";
|
|
635
|
+
|
|
636
|
+
export const metadata = {
|
|
637
|
+
title: "组织管理 - Echo",
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
export default async function OrganizationSettingsPage() {
|
|
641
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
642
|
+
|
|
643
|
+
if (!session?.user) {
|
|
644
|
+
redirect("/login");
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const userRole = (session.user as { role?: string }).role as UserRole || "customer";
|
|
648
|
+
|
|
649
|
+
if (userRole !== "admin") {
|
|
650
|
+
redirect("/settings/profile");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
let organization = null;
|
|
654
|
+
if (db) {
|
|
655
|
+
organization = await getUserOrganization(db, session.user.id);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (!organization) {
|
|
659
|
+
redirect("/settings/organizations/new");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return (
|
|
663
|
+
<div className="max-w-4xl space-y-6">
|
|
664
|
+
<div>
|
|
665
|
+
<h1 className="text-2xl font-semibold">组织管理</h1>
|
|
666
|
+
<p className="text-muted-foreground">管理您的组织信息和成员</p>
|
|
667
|
+
</div>
|
|
668
|
+
|
|
669
|
+
<OrganizationForm
|
|
670
|
+
organizationId={organization.id}
|
|
671
|
+
initialName={organization.name}
|
|
672
|
+
initialSlug={organization.slug}
|
|
673
|
+
/>
|
|
674
|
+
|
|
675
|
+
<InviteMemberForm organizationId={organization.id} />
|
|
676
|
+
|
|
677
|
+
<OrganizationMembersList organizationId={organization.id} />
|
|
678
|
+
</div>
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
**Step 2: Run build to verify**
|
|
684
|
+
|
|
685
|
+
Run: `bun run build 2>&1 | tail -10`
|
|
686
|
+
Expected: Build succeeds
|
|
687
|
+
|
|
688
|
+
**Step 3: Commit**
|
|
689
|
+
|
|
690
|
+
```bash
|
|
691
|
+
git add app/\(dashboard\)/settings/organization/page.tsx
|
|
692
|
+
git commit -m "feat: add organization settings page"
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
---
|
|
696
|
+
|
|
697
|
+
## Task 9: Create API Keys Settings Page
|
|
698
|
+
|
|
699
|
+
**Files:**
|
|
700
|
+
- Create: `app/(dashboard)/settings/api-keys/page.tsx`
|
|
701
|
+
- Create: `components/settings/api-keys-list.tsx`
|
|
702
|
+
|
|
703
|
+
**Step 1: Create API keys list component**
|
|
704
|
+
|
|
705
|
+
Create `components/settings/api-keys-list.tsx`:
|
|
706
|
+
|
|
707
|
+
```tsx
|
|
708
|
+
"use client";
|
|
709
|
+
|
|
710
|
+
import { useState, useEffect } from "react";
|
|
711
|
+
import { Button } from "@/components/ui/button";
|
|
712
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
713
|
+
import { Input } from "@/components/ui/input";
|
|
714
|
+
import { Label } from "@/components/ui/label";
|
|
715
|
+
import { Key, Copy, Trash2, Plus } from "lucide-react";
|
|
716
|
+
|
|
717
|
+
interface ApiKey {
|
|
718
|
+
keyId: string;
|
|
719
|
+
name: string;
|
|
720
|
+
prefix: string;
|
|
721
|
+
displayKey: string;
|
|
722
|
+
createdAt: string;
|
|
723
|
+
lastUsed: string | null;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export function ApiKeysList() {
|
|
727
|
+
const [keys, setKeys] = useState<ApiKey[]>([]);
|
|
728
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
729
|
+
const [newKeyName, setNewKeyName] = useState("");
|
|
730
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
731
|
+
|
|
732
|
+
useEffect(() => {
|
|
733
|
+
fetchKeys();
|
|
734
|
+
}, []);
|
|
735
|
+
|
|
736
|
+
const fetchKeys = async () => {
|
|
737
|
+
try {
|
|
738
|
+
const res = await fetch("/api/api-keys");
|
|
739
|
+
if (res.ok) {
|
|
740
|
+
const data = await res.json();
|
|
741
|
+
setKeys(data.data || []);
|
|
742
|
+
}
|
|
743
|
+
} catch (error) {
|
|
744
|
+
console.error("Failed to fetch API keys:", error);
|
|
745
|
+
} finally {
|
|
746
|
+
setIsLoading(false);
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
const handleCreate = async (e: React.FormEvent) => {
|
|
751
|
+
e.preventDefault();
|
|
752
|
+
if (!newKeyName.trim()) return;
|
|
753
|
+
|
|
754
|
+
setIsCreating(true);
|
|
755
|
+
try {
|
|
756
|
+
const res = await fetch("/api/api-keys", {
|
|
757
|
+
method: "POST",
|
|
758
|
+
headers: { "Content-Type": "application/json" },
|
|
759
|
+
body: JSON.stringify({ name: newKeyName }),
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
if (res.ok) {
|
|
763
|
+
const data = await res.json();
|
|
764
|
+
alert(`新密钥已创建: ${data.data.key}\n\n请立即保存此密钥,它将不会再次显示。`);
|
|
765
|
+
setNewKeyName("");
|
|
766
|
+
fetchKeys();
|
|
767
|
+
}
|
|
768
|
+
} catch (error) {
|
|
769
|
+
console.error("Failed to create API key:", error);
|
|
770
|
+
} finally {
|
|
771
|
+
setIsCreating(false);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
const handleDelete = async (keyId: string) => {
|
|
776
|
+
if (!confirm("确定要删除此 API 密钥吗?此操作不可撤销。")) return;
|
|
777
|
+
|
|
778
|
+
try {
|
|
779
|
+
const res = await fetch(`/api/api-keys/${keyId}`, { method: "DELETE" });
|
|
780
|
+
if (res.ok) {
|
|
781
|
+
fetchKeys();
|
|
782
|
+
}
|
|
783
|
+
} catch (error) {
|
|
784
|
+
console.error("Failed to delete API key:", error);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
if (isLoading) {
|
|
789
|
+
return <div className="text-muted-foreground">加载中...</div>;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return (
|
|
793
|
+
<div className="space-y-6">
|
|
794
|
+
<Card>
|
|
795
|
+
<CardHeader>
|
|
796
|
+
<CardTitle>创建新密钥</CardTitle>
|
|
797
|
+
<CardDescription>创建新的 API 密钥用于外部集成</CardDescription>
|
|
798
|
+
</CardHeader>
|
|
799
|
+
<CardContent>
|
|
800
|
+
<form onSubmit={handleCreate} className="flex gap-4">
|
|
801
|
+
<div className="flex-1">
|
|
802
|
+
<Label htmlFor="keyName" className="sr-only">密钥名称</Label>
|
|
803
|
+
<Input
|
|
804
|
+
id="keyName"
|
|
805
|
+
value={newKeyName}
|
|
806
|
+
onChange={(e) => setNewKeyName(e.target.value)}
|
|
807
|
+
placeholder="输入密钥名称"
|
|
808
|
+
/>
|
|
809
|
+
</div>
|
|
810
|
+
<Button type="submit" disabled={isCreating || !newKeyName.trim()}>
|
|
811
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
812
|
+
创建密钥
|
|
813
|
+
</Button>
|
|
814
|
+
</form>
|
|
815
|
+
</CardContent>
|
|
816
|
+
</Card>
|
|
817
|
+
|
|
818
|
+
<Card>
|
|
819
|
+
<CardHeader>
|
|
820
|
+
<CardTitle>已有密钥</CardTitle>
|
|
821
|
+
<CardDescription>管理您的 API 密钥</CardDescription>
|
|
822
|
+
</CardHeader>
|
|
823
|
+
<CardContent>
|
|
824
|
+
{keys.length === 0 ? (
|
|
825
|
+
<p className="text-muted-foreground py-4">暂无 API 密钥</p>
|
|
826
|
+
) : (
|
|
827
|
+
<div className="space-y-4">
|
|
828
|
+
{keys.map((key) => (
|
|
829
|
+
<div key={key.keyId} className="flex items-center justify-between p-4 border rounded-lg">
|
|
830
|
+
<div className="flex items-center gap-3">
|
|
831
|
+
<Key className="h-5 w-5 text-muted-foreground" />
|
|
832
|
+
<div>
|
|
833
|
+
<p className="font-medium">{key.name}</p>
|
|
834
|
+
<p className="text-sm text-muted-foreground font-mono">{key.displayKey}</p>
|
|
835
|
+
</div>
|
|
836
|
+
</div>
|
|
837
|
+
<div className="flex items-center gap-2">
|
|
838
|
+
<Button variant="ghost" size="icon" onClick={() => navigator.clipboard.writeText(key.prefix)}>
|
|
839
|
+
<Copy className="h-4 w-4" />
|
|
840
|
+
</Button>
|
|
841
|
+
<Button variant="ghost" size="icon" onClick={() => handleDelete(key.keyId)}>
|
|
842
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
843
|
+
</Button>
|
|
844
|
+
</div>
|
|
845
|
+
</div>
|
|
846
|
+
))}
|
|
847
|
+
</div>
|
|
848
|
+
)}
|
|
849
|
+
</CardContent>
|
|
850
|
+
</Card>
|
|
851
|
+
</div>
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
**Step 2: Create API keys page**
|
|
857
|
+
|
|
858
|
+
Create `app/(dashboard)/settings/api-keys/page.tsx`:
|
|
859
|
+
|
|
860
|
+
```tsx
|
|
861
|
+
import { headers } from "next/headers";
|
|
862
|
+
import { redirect } from "next/navigation";
|
|
863
|
+
import { auth } from "@/lib/auth/config";
|
|
864
|
+
import { ApiKeysList } from "@/components/settings/api-keys-list";
|
|
865
|
+
import type { UserRole } from "@/lib/auth/permissions";
|
|
866
|
+
|
|
867
|
+
export const metadata = {
|
|
868
|
+
title: "API 密钥 - Echo",
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
export default async function ApiKeysSettingsPage() {
|
|
872
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
873
|
+
|
|
874
|
+
if (!session?.user) {
|
|
875
|
+
redirect("/login");
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const userRole = (session.user as { role?: string }).role as UserRole || "customer";
|
|
879
|
+
|
|
880
|
+
if (userRole !== "admin" && userRole !== "product_manager") {
|
|
881
|
+
redirect("/settings/profile");
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return (
|
|
885
|
+
<div className="max-w-2xl">
|
|
886
|
+
<div className="mb-6">
|
|
887
|
+
<h1 className="text-2xl font-semibold">API 密钥</h1>
|
|
888
|
+
<p className="text-muted-foreground">管理您的 API 访问密钥</p>
|
|
889
|
+
</div>
|
|
890
|
+
<ApiKeysList />
|
|
891
|
+
</div>
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
**Step 3: Update settings index export**
|
|
897
|
+
|
|
898
|
+
Update `components/settings/index.ts`:
|
|
899
|
+
|
|
900
|
+
```ts
|
|
901
|
+
export { SettingsSidebar } from "./settings-sidebar";
|
|
902
|
+
export { ProfileForm } from "./profile-form";
|
|
903
|
+
export { AppearanceForm } from "./appearance-form";
|
|
904
|
+
export { ApiKeysList } from "./api-keys-list";
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
**Step 4: Run build to verify**
|
|
908
|
+
|
|
909
|
+
Run: `bun run build 2>&1 | tail -10`
|
|
910
|
+
Expected: Build succeeds
|
|
911
|
+
|
|
912
|
+
**Step 5: Commit**
|
|
913
|
+
|
|
914
|
+
```bash
|
|
915
|
+
git add components/settings/api-keys-list.tsx components/settings/index.ts app/\(dashboard\)/settings/api-keys/page.tsx
|
|
916
|
+
git commit -m "feat: add API keys settings page"
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
---
|
|
920
|
+
|
|
921
|
+
## Task 10: Final Verification and Cleanup
|
|
922
|
+
|
|
923
|
+
**Step 1: Run full build**
|
|
924
|
+
|
|
925
|
+
Run: `bun run build`
|
|
926
|
+
Expected: Build succeeds with no errors
|
|
927
|
+
|
|
928
|
+
**Step 2: Run linting**
|
|
929
|
+
|
|
930
|
+
Run: `bun run lint`
|
|
931
|
+
Expected: No errors (warnings acceptable)
|
|
932
|
+
|
|
933
|
+
**Step 3: Test manually**
|
|
934
|
+
|
|
935
|
+
1. Start dev server: `bun dev`
|
|
936
|
+
2. Login as admin user
|
|
937
|
+
3. Verify user dropdown appears in sidebar
|
|
938
|
+
4. Click "设置" → redirects to /settings/profile
|
|
939
|
+
5. Verify left sidebar shows all menu items
|
|
940
|
+
6. Navigate through each settings page
|
|
941
|
+
7. Verify non-admin users cannot access organization/api-keys pages
|
|
942
|
+
|
|
943
|
+
**Step 4: Final commit if any fixes needed**
|
|
944
|
+
|
|
945
|
+
```bash
|
|
946
|
+
git add -A
|
|
947
|
+
git commit -m "chore: cleanup and final adjustments"
|
|
948
|
+
```
|