@nexttylabs/echo 0.4.0 → 0.6.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.
Files changed (262) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/app/(dashboard)/admin/feedback/[id]/edit/page.tsx +12 -6
  3. package/app/(dashboard)/admin/feedback/new/page.tsx +19 -17
  4. package/app/(dashboard)/admin/layout.tsx +16 -6
  5. package/app/(dashboard)/layout.tsx +4 -2
  6. package/app/(dashboard)/settings/api-keys/page.tsx +13 -3
  7. package/app/(dashboard)/settings/layout.tsx +25 -2
  8. package/app/(dashboard)/settings/organization/page.tsx +8 -9
  9. package/app/(public)/[organizationSlug]/roadmap/page.tsx +19 -1
  10. package/app/api/admin/backup/route.ts +22 -4
  11. package/app/api/auth/register/handler.ts +1 -2
  12. package/app/api/feedback/[id]/comments/[commentId]/route.ts +13 -4
  13. package/app/api/feedback/[id]/reclassify/route.ts +4 -4
  14. package/app/api/organizations/handler.ts +2 -4
  15. package/components/settings/settings-sidebar.tsx +4 -4
  16. package/hooks/use-organization.tsx +116 -0
  17. package/hooks/use-permissions.ts +24 -11
  18. package/lib/auth/config.ts +0 -7
  19. package/lib/auth/organization.ts +20 -0
  20. package/lib/auth/permissions.ts +10 -0
  21. package/lib/db/migrations/0000_needy_leech.sql +335 -0
  22. package/lib/db/migrations/meta/0000_snapshot.json +2186 -1
  23. package/lib/db/migrations/meta/_journal.json +2 -135
  24. package/lib/db/schema/auth.ts +0 -1
  25. package/lib/db/schema/index.ts +0 -1
  26. package/lib/portal/public-context.tsx +5 -0
  27. package/package.json +20 -1
  28. package/.changeset/README.md +0 -21
  29. package/.changeset/config.json +0 -11
  30. package/.changeset/cozy-ghosts-care.md +0 -5
  31. package/.changeset/sharp-lines-stand.md +0 -5
  32. package/.changeset/sour-doodles-eat.md +0 -5
  33. package/.changeset/tender-moose-shop.md +0 -5
  34. package/.github/pull_request_template.md +0 -13
  35. package/.github/workflows/ci.yml +0 -41
  36. package/.github/workflows/publish.yml +0 -44
  37. package/.github/workflows/release.yml +0 -73
  38. package/AGENTS.md +0 -92
  39. package/Dockerfile +0 -57
  40. package/Makefile +0 -77
  41. package/bun.lock +0 -2503
  42. package/components/portal/project-switcher.tsx +0 -20
  43. package/docker-compose.dev.yml +0 -26
  44. package/docker-compose.yml +0 -98
  45. package/docs/architecture.md +0 -259
  46. package/docs/component-inventory.md +0 -261
  47. package/docs/database-migrations.md +0 -76
  48. package/docs/development-guide.md +0 -209
  49. package/docs/e2e-user-flows.csv +0 -31
  50. package/docs/er-diagram-feedback.mmd +0 -138
  51. package/docs/er-diagram.mmd +0 -281
  52. package/docs/i18n-check-report.md +0 -296
  53. package/docs/index.md +0 -214
  54. package/docs/logic-chain.md +0 -94
  55. package/docs/plans/2026-01-02-database-migration-scripts.md +0 -496
  56. package/docs/plans/2026-01-02-user-login-design.md +0 -37
  57. package/docs/plans/2026-01-02-user-login.md +0 -437
  58. package/docs/plans/2026-01-02-user-registration-design.md +0 -47
  59. package/docs/plans/2026-01-02-user-registration.md +0 -628
  60. package/docs/plans/2026-01-03-roles-permissions-design.md +0 -20
  61. package/docs/plans/2026-01-03-roles-permissions.md +0 -266
  62. package/docs/plans/2026-01-05-authentication-middleware.md +0 -207
  63. package/docs/plans/2026-01-05-member-removal.md +0 -186
  64. package/docs/plans/2026-01-05-organization-creation.md +0 -374
  65. package/docs/plans/2026-01-05-rbac-middleware.md +0 -112
  66. package/docs/plans/2026-01-05-role-configuration.md +0 -441
  67. package/docs/plans/2026-01-06-file-upload-support.md +0 -804
  68. package/docs/plans/2026-01-06-permission-check-hook.md +0 -155
  69. package/docs/plans/2026-01-06-resource-ownership-check.md +0 -231
  70. package/docs/plans/2026-01-07-feedback-tracking-link.md +0 -459
  71. package/docs/plans/2026-01-09-logout-redirect-design.md +0 -52
  72. package/docs/plans/2026-01-09-phase2-3-plan.md +0 -654
  73. package/docs/plans/2026-01-09-portal-execution-plan.md +0 -408
  74. package/docs/plans/2026-01-09-project-delete-feature-design.md +0 -163
  75. package/docs/plans/2026-01-09-project-delete-implementation.md +0 -451
  76. package/docs/plans/2026-01-09-project-edit-delete-design.md +0 -52
  77. package/docs/plans/2026-01-09-settings-center-design.md +0 -114
  78. package/docs/plans/2026-01-09-settings-center.md +0 -948
  79. package/docs/plans/2026-01-10-organization-only-design.md +0 -66
  80. package/docs/plans/2026-01-10-organization-only-implementation.md +0 -433
  81. package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +0 -18
  82. package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +0 -296
  83. package/docs/plans/2026-01-14-e2e-playwright-feedback.md +0 -173
  84. package/docs/plans/2026-01-15-feedback-management-org-context-design.md +0 -82
  85. package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +0 -521
  86. package/docs/plans/2026-01-16-admin-feedback-filters-design.md +0 -75
  87. package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +0 -293
  88. package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +0 -180
  89. package/docs/plans/2026-01-16-e2e-test-fixes.md +0 -158
  90. package/docs/plans/2026-01-17-admin-feedback-filters.md +0 -214
  91. package/docs/plans/2026-01-17-admin-feedback-improvements.md +0 -453
  92. package/docs/plans/2026-01-18-changesets-design.md +0 -40
  93. package/docs/product_changes.md +0 -37
  94. package/docs/project-overview.md +0 -159
  95. package/docs/project-scan-report.json +0 -104
  96. package/docs/route-role-visibility.md +0 -51
  97. package/docs/source-tree-analysis.md +0 -150
  98. package/docs/testing/delete-project-manual-tests.md +0 -18
  99. package/docs/user-story-tracking.md +0 -191
  100. package/eslint.config.mjs +0 -19
  101. package/lib/db/migrations/.gitkeep +0 -0
  102. package/lib/db/migrations/0000_cynical_gladiator.sql +0 -53
  103. package/lib/db/migrations/0001_wandering_sunfire.sql +0 -27
  104. package/lib/db/migrations/0002_shallow_speedball.sql +0 -1
  105. package/lib/db/migrations/0003_add_org_description.sql +0 -1
  106. package/lib/db/migrations/0003_boring_wild_pack.sql +0 -13
  107. package/lib/db/migrations/0004_windy_tyrannus.sql +0 -27
  108. package/lib/db/migrations/0005_perpetual_doorman.sql +0 -5
  109. package/lib/db/migrations/0006_aberrant_captain_midlands.sql +0 -13
  110. package/lib/db/migrations/0007_clever_captain_cross.sql +0 -14
  111. package/lib/db/migrations/0008_sparkling_pandemic.sql +0 -2
  112. package/lib/db/migrations/0009_happy_black_tom.sql +0 -29
  113. package/lib/db/migrations/0010_kind_junta.sql +0 -8
  114. package/lib/db/migrations/0011_mute_squadron_supreme.sql +0 -25
  115. package/lib/db/migrations/0012_giant_power_man.sql +0 -24
  116. package/lib/db/migrations/0013_damp_titanium_man.sql +0 -17
  117. package/lib/db/migrations/0014_blue_alice.sql +0 -18
  118. package/lib/db/migrations/0015_webhook_tables.sql +0 -41
  119. package/lib/db/migrations/0016_github_integration.sql +0 -30
  120. package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +0 -22
  121. package/lib/db/migrations/0017_slimy_inhumans.sql +0 -6
  122. package/lib/db/migrations/0018_same_spitfire.sql +0 -1
  123. package/lib/db/migrations/0019_jittery_loners.sql +0 -16
  124. package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +0 -14
  125. package/lib/db/migrations/meta/0001_snapshot.json +0 -553
  126. package/lib/db/migrations/meta/0002_snapshot.json +0 -560
  127. package/lib/db/migrations/meta/0003_snapshot.json +0 -650
  128. package/lib/db/migrations/meta/0004_snapshot.json +0 -852
  129. package/lib/db/migrations/meta/0005_snapshot.json +0 -900
  130. package/lib/db/migrations/meta/0006_snapshot.json +0 -1011
  131. package/lib/db/migrations/meta/0007_snapshot.json +0 -1125
  132. package/lib/db/migrations/meta/0008_snapshot.json +0 -1146
  133. package/lib/db/migrations/meta/0009_snapshot.json +0 -1386
  134. package/lib/db/migrations/meta/0010_snapshot.json +0 -1419
  135. package/lib/db/migrations/meta/0011_snapshot.json +0 -1615
  136. package/lib/db/migrations/meta/0012_snapshot.json +0 -1805
  137. package/lib/db/migrations/meta/0013_snapshot.json +0 -1948
  138. package/lib/db/migrations/meta/0014_snapshot.json +0 -2082
  139. package/lib/db/migrations/meta/0015_snapshot.json +0 -2476
  140. package/lib/db/migrations/meta/0016_snapshot.json +0 -2633
  141. package/lib/db/migrations/meta/0017_snapshot.json +0 -2680
  142. package/lib/db/migrations/meta/0018_snapshot.json +0 -2686
  143. package/lib/db/migrations/meta/0019_snapshot.json +0 -2741
  144. package/lib/db/schema/projects.ts +0 -145
  145. package/lib/db/schema/user-profiles.ts +0 -31
  146. package/lib/validations/projects.ts +0 -49
  147. package/next-env.d.ts +0 -6
  148. package/playwright.config.ts +0 -44
  149. package/proxy.test.ts +0 -131
  150. package/proxy.ts +0 -116
  151. package/scripts/backup-db.sh +0 -57
  152. package/scripts/backup-db.ts +0 -24
  153. package/scripts/generate-openapi.ts +0 -22
  154. package/scripts/migration-helper.ts +0 -39
  155. package/scripts/pre-deploy.ts +0 -75
  156. package/scripts/restore-db.sh +0 -60
  157. package/scripts/rollback.ts +0 -72
  158. package/scripts/seed-tags.ts +0 -48
  159. package/tests/api/feedback-bulk.test.ts +0 -47
  160. package/tests/api/feedback-by-id.test.ts +0 -67
  161. package/tests/api/feedback-comments-route-import.test.ts +0 -26
  162. package/tests/api/feedback-create.test.ts +0 -71
  163. package/tests/api/feedback-delete.test.ts +0 -160
  164. package/tests/api/feedback-filter.test.ts +0 -250
  165. package/tests/api/feedback-list.test.ts +0 -234
  166. package/tests/api/feedback-route-assignee-condition.test.ts +0 -32
  167. package/tests/api/feedback-similar.test.ts +0 -46
  168. package/tests/api/feedback-sort.test.ts +0 -261
  169. package/tests/api/feedback-status-enum.test.ts +0 -49
  170. package/tests/api/feedback-status-filter.test.ts +0 -117
  171. package/tests/api/feedback-submit-on-behalf.test.ts +0 -269
  172. package/tests/api/feedback.test.ts +0 -175
  173. package/tests/api/identify-jwt.test.ts +0 -25
  174. package/tests/api/invitation-accept.test.ts +0 -213
  175. package/tests/api/organization-invitations.test.ts +0 -186
  176. package/tests/api/organization-members-list.test.ts +0 -79
  177. package/tests/api/organization-members.test.ts +0 -340
  178. package/tests/api/organizations.test.ts +0 -149
  179. package/tests/api/register.test.ts +0 -112
  180. package/tests/api/upload.test.ts +0 -103
  181. package/tests/api/vote.test.ts +0 -82
  182. package/tests/app/admin-feedback-detail-page.test.tsx +0 -25
  183. package/tests/app/admin-feedback-list-page.test.tsx +0 -25
  184. package/tests/app/admin-feedback-new-page.test.tsx +0 -25
  185. package/tests/app/health-route-helpers.test.ts +0 -27
  186. package/tests/app/login-page.test.ts +0 -26
  187. package/tests/app/portal-page.test.ts +0 -29
  188. package/tests/app/project-portal-overview.test.tsx +0 -25
  189. package/tests/app/widget-page-import.test.ts +0 -25
  190. package/tests/components/create-post-dialog-defaults.test.ts +0 -43
  191. package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +0 -27
  192. package/tests/components/feedback/embedded-feedback-form.test.tsx +0 -96
  193. package/tests/components/feedback/feedback-detail.test.tsx +0 -25
  194. package/tests/components/feedback/feedback-stats.test.tsx +0 -49
  195. package/tests/components/feedback-bulk-actions.test.tsx +0 -39
  196. package/tests/components/feedback-i18n-keys.test.ts +0 -70
  197. package/tests/components/feedback-list-controls-compile.test.ts +0 -25
  198. package/tests/components/feedback-list-controls.test.tsx +0 -204
  199. package/tests/components/feedback-list-item.test.tsx +0 -67
  200. package/tests/components/landing/hero.test.tsx +0 -46
  201. package/tests/components/layout/language-switcher.test.tsx +0 -25
  202. package/tests/components/layout/sidebar.test.tsx +0 -157
  203. package/tests/components/login-form.test.ts +0 -25
  204. package/tests/components/organization-form.test.ts +0 -32
  205. package/tests/components/organization-switcher.test.ts +0 -25
  206. package/tests/components/pagination.test.tsx +0 -43
  207. package/tests/components/portal-overview.test.tsx +0 -25
  208. package/tests/components/profile-form.test.tsx +0 -139
  209. package/tests/components/role-selector.test.ts +0 -31
  210. package/tests/components/status-chart.test.tsx +0 -90
  211. package/tests/e2e/auth.e2e.ts +0 -323
  212. package/tests/e2e/feedback-actions.e2e.ts +0 -471
  213. package/tests/e2e/feedback-attachment.e2e.ts +0 -168
  214. package/tests/e2e/feedback-customer.e2e.ts +0 -226
  215. package/tests/e2e/feedback-management.e2e.ts +0 -565
  216. package/tests/e2e/feedback-submit.e2e.ts +0 -133
  217. package/tests/e2e/feedback-view.e2e.ts +0 -297
  218. package/tests/e2e/fixtures/test-data.ts +0 -235
  219. package/tests/e2e/health-check.e2e.ts +0 -230
  220. package/tests/e2e/helpers/test-utils-helpers.test.ts +0 -43
  221. package/tests/e2e/helpers/test-utils.ts +0 -298
  222. package/tests/e2e/integration-placeholders.e2e.ts +0 -199
  223. package/tests/e2e/organization.e2e.ts +0 -292
  224. package/tests/e2e/permissions.e2e.ts +0 -424
  225. package/tests/e2e/project-widget.e2e.ts +0 -63
  226. package/tests/feedback/filters.test.ts +0 -29
  227. package/tests/hooks/use-permissions.test.ts +0 -52
  228. package/tests/lib/ai/classifier.test.ts +0 -104
  229. package/tests/lib/ai/duplicate-detector.test.ts +0 -234
  230. package/tests/lib/attachments-schema.test.ts +0 -30
  231. package/tests/lib/auth/session.test.ts +0 -49
  232. package/tests/lib/auth-client.test.ts +0 -37
  233. package/tests/lib/auth-config.test.ts +0 -26
  234. package/tests/lib/feedback-prefill.test.ts +0 -52
  235. package/tests/lib/feedback-processor.test.ts +0 -41
  236. package/tests/lib/feedback-schema.test.ts +0 -33
  237. package/tests/lib/file-validator.test.ts +0 -48
  238. package/tests/lib/get-feedback-by-id.test.ts +0 -37
  239. package/tests/lib/invitations.test.ts +0 -35
  240. package/tests/lib/login-schema.test.ts +0 -36
  241. package/tests/lib/org-context.test.ts +0 -95
  242. package/tests/lib/organization-access.test.ts +0 -44
  243. package/tests/lib/organization-member-role-schema.test.ts +0 -41
  244. package/tests/lib/permissions.test.ts +0 -88
  245. package/tests/lib/portal-analytics.test.ts +0 -25
  246. package/tests/lib/portal-contributors.test.ts +0 -25
  247. package/tests/lib/portal-copy.test.ts +0 -27
  248. package/tests/lib/portal-i18n.test.ts +0 -30
  249. package/tests/lib/portal-leaderboard-settings.test.ts +0 -25
  250. package/tests/lib/portal-modules.test.ts +0 -25
  251. package/tests/lib/portal-seo.test.ts +0 -25
  252. package/tests/lib/portal-sharing.test.ts +0 -25
  253. package/tests/lib/portal-sorting.test.ts +0 -25
  254. package/tests/lib/portal-theme.test.ts +0 -25
  255. package/tests/lib/rate-limit.test.ts +0 -142
  256. package/tests/lib/resolve-locale.test.ts +0 -34
  257. package/tests/lib/services/backup.test.ts +0 -145
  258. package/tests/lib/user-organizations.test.ts +0 -42
  259. package/tests/lib/user-role-schema.test.ts +0 -33
  260. package/tests/lib/user-schema.test.ts +0 -25
  261. package/tests/setup.ts +0 -74
  262. package/vercel.json +0 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @nexttylabs/echo
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cc04d9f: Use the organization member role uniformly.
8
+ - 5cfdeb7: feat: support editing user profile
9
+
10
+ ### Patch Changes
11
+
12
+ - 7dba561: remove legacy changeset files
13
+ - 1e798f1: remove unuse tables and published files
14
+ - e3c4e1c: fix workflow errors
15
+ - daf9abb: first release
16
+
17
+ ## 0.5.0
18
+
19
+ ### Minor Changes
20
+
21
+ - 5cfdeb7: feat: support editing user profile
22
+
23
+ ### Patch Changes
24
+
25
+ - 7dba561: remove legacy changeset files
26
+ - 1e798f1: remove unuse tables and published files
27
+ - e3c4e1c: fix workflow errors
28
+ - daf9abb: first release
29
+
3
30
  ## 0.4.0
4
31
 
5
32
  ### Minor Changes
@@ -22,7 +22,7 @@ import { auth } from "@/lib/auth/config";
22
22
  import { db } from "@/lib/db";
23
23
  import { feedback } from "@/lib/db/schema";
24
24
  import { FeedbackEditForm } from "@/components/feedback/feedback-edit-form";
25
- import { canEditFeedback, type UserRole } from "@/lib/auth/permissions";
25
+ import { canEditFeedback } from "@/lib/auth/permissions";
26
26
  import { getOrgContext } from "@/lib/auth/org-context";
27
27
  import { getRequestUrl } from "@/lib/http/get-request-url";
28
28
 
@@ -39,11 +39,6 @@ export default async function FeedbackEditPage({ params }: PageProps) {
39
39
  redirect("/login");
40
40
  }
41
41
 
42
- const userRole = (session.user as { role?: string }).role as UserRole | undefined;
43
- if (!userRole || !canEditFeedback(userRole)) {
44
- redirect("/admin/feedback");
45
- }
46
-
47
42
  const { id } = await params;
48
43
  const feedbackId = parseInt(id);
49
44
 
@@ -55,6 +50,7 @@ export default async function FeedbackEditPage({ params }: PageProps) {
55
50
  throw new Error("Database not configured");
56
51
  }
57
52
 
53
+ // Get organization context first
58
54
  let organizationId: string | null = null;
59
55
  try {
60
56
  const url = getRequestUrl(
@@ -72,6 +68,16 @@ export default async function FeedbackEditPage({ params }: PageProps) {
72
68
  notFound();
73
69
  }
74
70
 
71
+ // Get user role from organization membership
72
+ const { getUserRoleInOrganization } = await import("@/lib/auth/organization");
73
+ const userRole = organizationId
74
+ ? await getUserRoleInOrganization(db, session.user.id, organizationId)
75
+ : null;
76
+
77
+ if (!userRole || !canEditFeedback(userRole)) {
78
+ redirect("/admin/feedback");
79
+ }
80
+
75
81
  const [row] = await db
76
82
  .select({
77
83
  title: feedback.title,
@@ -30,17 +30,21 @@ export default async function NewFeedbackPage() {
30
30
  redirect("/login");
31
31
  }
32
32
 
33
- const userRole = ((session.user as { role?: string }).role ?? "customer") as UserRole;
34
- const hasSubmitOnBehalfPermission = canSubmitOnBehalf(userRole);
33
+ if (!db) {
34
+ throw new Error("Database not configured");
35
+ }
35
36
 
36
- if (!hasSubmitOnBehalfPermission) {
37
+ // Get user's organization first (which includes their role)
38
+ const organization = await getUserOrganization(db, session.user.id);
39
+
40
+ if (!organization) {
37
41
  return (
38
42
  <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 px-4 py-12">
39
43
  <div className="mx-auto flex w-full max-w-2xl flex-col gap-6">
40
- <div className="rounded-lg border border-red-200 bg-red-50 p-6">
41
- <h1 className="text-xl font-semibold text-red-800">权限不足</h1>
42
- <p className="mt-2 text-sm text-red-600">
43
- 您没有代客户提交反馈的权限。请联系管理员获取相应权限。
44
+ <div className="rounded-lg border border-amber-200 bg-amber-50 p-6">
45
+ <h1 className="text-xl font-semibold text-amber-800">未找到组织</h1>
46
+ <p className="mt-2 text-sm text-amber-700">
47
+ 请先加入组织后再代客户提交反馈。
44
48
  </p>
45
49
  </div>
46
50
  </div>
@@ -48,20 +52,18 @@ export default async function NewFeedbackPage() {
48
52
  );
49
53
  }
50
54
 
51
- if (!db) {
52
- throw new Error("Database not configured");
53
- }
54
-
55
- const organization = await getUserOrganization(db, session.user.id);
55
+ // Get user role from organization membership
56
+ const userRole = (organization.role as UserRole) || "customer";
57
+ const hasSubmitOnBehalfPermission = canSubmitOnBehalf(userRole);
56
58
 
57
- if (!organization) {
59
+ if (!hasSubmitOnBehalfPermission) {
58
60
  return (
59
61
  <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 px-4 py-12">
60
62
  <div className="mx-auto flex w-full max-w-2xl flex-col gap-6">
61
- <div className="rounded-lg border border-amber-200 bg-amber-50 p-6">
62
- <h1 className="text-xl font-semibold text-amber-800">未找到组织</h1>
63
- <p className="mt-2 text-sm text-amber-700">
64
- 请先加入组织后再代客户提交反馈。
63
+ <div className="rounded-lg border border-red-200 bg-red-50 p-6">
64
+ <h1 className="text-xl font-semibold text-red-800">权限不足</h1>
65
+ <p className="mt-2 text-sm text-red-600">
66
+ 您没有代客户提交反馈的权限。请联系管理员获取相应权限。
65
67
  </p>
66
68
  </div>
67
69
  </div>
@@ -15,10 +15,11 @@
15
15
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
  */
17
17
 
18
- import { headers } from "next/headers";
18
+ import { cookies, headers } from "next/headers";
19
19
  import { redirect } from "next/navigation";
20
20
  import { auth } from "@/lib/auth/config";
21
- import type { UserRole } from "@/lib/auth/permissions";
21
+ import { db } from "@/lib/db";
22
+ import { getUserRoleInOrganization } from "@/lib/auth/organization";
22
23
 
23
24
  export default async function AdminLayout({
24
25
  children,
@@ -36,13 +37,22 @@ export default async function AdminLayout({
36
37
  redirect("/login");
37
38
  }
38
39
 
39
- const role = (session.user as { role?: string }).role as
40
- | UserRole
41
- | undefined;
40
+ // Get current organization ID from cookie
41
+ const cookieStore = await cookies();
42
+ const currentOrgId = cookieStore.get("orgId")?.value;
42
43
 
43
- if (role !== "admin") {
44
+ if (!db || !currentOrgId) {
45
+ redirect("/no-access");
46
+ }
47
+
48
+ // Get user's role in the current organization
49
+ const role = await getUserRoleInOrganization(db, session.user.id, currentOrgId);
50
+
51
+ // Only admin or owner can access admin pages
52
+ if (role !== "admin" && role !== "owner" && role !== "product_manager") {
44
53
  redirect("/no-access");
45
54
  }
46
55
 
47
56
  return <>{children}</>;
48
57
  }
58
+
@@ -39,8 +39,6 @@ export default async function DashboardRootLayout({
39
39
  redirect("/login");
40
40
  }
41
41
 
42
- const userRole = (session.user as { role?: string }).role as UserRole || "customer";
43
-
44
42
  // Fetch organizations
45
43
  let organizations: Array<{ id: string; name: string; slug: string; role: string }> = [];
46
44
  let currentOrgId: string | null = null;
@@ -52,6 +50,10 @@ export default async function DashboardRootLayout({
52
50
  currentOrgId = cookieOrgId || organizations[0]?.id || null;
53
51
  }
54
52
 
53
+ // Get user role from current organization membership
54
+ const currentOrg = organizations.find((org) => org.id === currentOrgId);
55
+ const userRole = (currentOrg?.role as UserRole) || "customer";
56
+
55
57
  return (
56
58
  <DashboardLayout
57
59
  user={{
@@ -15,11 +15,13 @@
15
15
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
  */
17
17
 
18
- import { headers } from "next/headers";
18
+ import { cookies, headers } from "next/headers";
19
19
  import { redirect } from "next/navigation";
20
20
  import { getTranslations } from "next-intl/server";
21
21
  import { auth } from "@/lib/auth/config";
22
+ import { db } from "@/lib/db";
22
23
  import { ApiKeysList } from "@/components/settings/api-keys-list";
24
+ import { getUserRoleInOrganization } from "@/lib/auth/organization";
23
25
  import type { UserRole } from "@/lib/auth/permissions";
24
26
 
25
27
  export async function generateMetadata() {
@@ -37,9 +39,17 @@ export default async function ApiKeysSettingsPage() {
37
39
  redirect("/login");
38
40
  }
39
41
 
40
- const userRole = (session.user as { role?: string }).role as UserRole || "customer";
42
+ // Get user role from current organization
43
+ const cookieStore = await cookies();
44
+ const currentOrgId = cookieStore.get("orgId")?.value;
41
45
 
42
- if (userRole !== "admin" && userRole !== "product_manager") {
46
+ let userRole: UserRole = "customer";
47
+ if (db && currentOrgId) {
48
+ const role = await getUserRoleInOrganization(db, session.user.id, currentOrgId);
49
+ userRole = role || "customer";
50
+ }
51
+
52
+ if (userRole !== "owner" && userRole !== "admin" && userRole !== "product_manager") {
43
53
  redirect("/settings/profile");
44
54
  }
45
55
 
@@ -15,10 +15,12 @@
15
15
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
  */
17
17
 
18
- import { headers } from "next/headers";
18
+ import { cookies, headers } from "next/headers";
19
19
  import { redirect } from "next/navigation";
20
20
  import { auth } from "@/lib/auth/config";
21
+ import { db } from "@/lib/db";
21
22
  import { SettingsSidebar } from "@/components/settings";
23
+ import { getUserOrganizations } from "@/lib/auth/organization";
22
24
  import type { UserRole } from "@/lib/auth/permissions";
23
25
 
24
26
  export default async function SettingsLayout({
@@ -32,7 +34,27 @@ export default async function SettingsLayout({
32
34
  redirect("/login");
33
35
  }
34
36
 
35
- const userRole = (session.user as { role?: string }).role as UserRole || "customer";
37
+ // Get user role from current organization (same logic as dashboard layout)
38
+ let userRole: UserRole = "customer";
39
+
40
+ if (db) {
41
+ const organizations = await getUserOrganizations(db, session.user.id);
42
+ const cookieStore = await cookies();
43
+ const cookieOrgId = cookieStore.get("orgId")?.value ?? null;
44
+
45
+ // Check if the cookie org exists in user's organizations
46
+ // If not, fall back to the first available organization
47
+ let currentOrg = cookieOrgId
48
+ ? organizations.find((org) => org.id === cookieOrgId)
49
+ : null;
50
+
51
+ // Fallback to first org if cookie org is not found (stale cookie)
52
+ if (!currentOrg && organizations.length > 0) {
53
+ currentOrg = organizations[0];
54
+ }
55
+
56
+ userRole = (currentOrg?.role as UserRole) || "customer";
57
+ }
36
58
 
37
59
  return (
38
60
  <div className="flex min-h-[calc(100vh-3.5rem)]">
@@ -41,3 +63,4 @@ export default async function SettingsLayout({
41
63
  </div>
42
64
  );
43
65
  }
66
+
@@ -38,30 +38,29 @@ export default async function OrganizationSettingsPage() {
38
38
  redirect("/login");
39
39
  }
40
40
 
41
- const userRole = (session.user as { role?: string }).role as UserRole || "customer";
42
-
43
- if (userRole !== "admin") {
44
- redirect("/settings/profile");
45
- }
46
-
47
41
  if (!db) {
48
42
  throw new Error("Database connection not available");
49
43
  }
50
44
 
51
- // Get all user organizations
45
+ // Get all user organizations and find current one
52
46
  const organizations = await getUserOrganizations(db, session.user.id);
53
47
 
54
48
  if (organizations.length === 0) {
55
49
  redirect("/settings/organizations/new");
56
50
  }
57
51
 
58
- // Get current organization from cookie (same logic as dashboard layout)
52
+ // Get current organization from cookie
59
53
  const cookieStore = await cookies();
60
54
  const cookieOrgId = cookieStore.get("orgId")?.value ?? null;
61
55
  const currentOrgId = cookieOrgId || organizations[0]?.id || null;
62
56
 
63
- // Find the current organization
57
+ // Find the current organization and get role from it
64
58
  const organization = organizations.find(org => org.id === currentOrgId) || organizations[0];
59
+ const userRole = (organization?.role as UserRole) || "customer";
60
+
61
+ if (userRole !== "owner" && userRole !== "admin") {
62
+ redirect("/settings/profile");
63
+ }
65
64
 
66
65
  if (!organization) {
67
66
  redirect("/settings/organizations/new");
@@ -19,6 +19,7 @@ import Link from "next/link";
19
19
  import { notFound } from "next/navigation";
20
20
  import { and, desc, eq, isNull, sql } from "drizzle-orm";
21
21
  import { headers } from "next/headers";
22
+ import { organizationMembers } from "@/lib/db/schema";
22
23
  import { auth } from "@/lib/auth/config";
23
24
  import { PortalLayout } from "@/components/portal/portal-layout";
24
25
  import { RoadmapBoard, type RoadmapStatus } from "@/components/portal/roadmap-board";
@@ -108,7 +109,24 @@ export default async function OrganizationRoadmapPage({ params }: PageProps) {
108
109
 
109
110
  // Check if user is admin
110
111
  const session = await auth.api.getSession({ headers: await headers() });
111
- const isAdmin = session?.user?.role === "admin" || session?.user?.role === "owner";
112
+ let isAdmin = false;
113
+
114
+ if (session?.user?.id) {
115
+ const { db } = await import("@/lib/db");
116
+ if (db) {
117
+ const [member] = await db
118
+ .select({ role: organizationMembers.role })
119
+ .from(organizationMembers)
120
+ .where(
121
+ and(
122
+ eq(organizationMembers.organizationId, organization.id),
123
+ eq(organizationMembers.userId, session.user.id)
124
+ )
125
+ )
126
+ .limit(1);
127
+ isAdmin = member?.role === "admin" || member?.role === "owner";
128
+ }
129
+ }
112
130
 
113
131
  const items = await getRoadmapItems(organization.id);
114
132
  const sections = getPortalSections(organizationSlug, context.modules);
@@ -24,6 +24,8 @@ import {
24
24
  getBackupConfig,
25
25
  } from "@/lib/services/backup";
26
26
  import { apiError } from "@/lib/api/errors";
27
+ import { db } from "@/lib/db";
28
+ import { getUserOrganizations } from "@/lib/auth/organization";
27
29
 
28
30
  export async function POST(req: NextRequest) {
29
31
  try {
@@ -32,8 +34,16 @@ export async function POST(req: NextRequest) {
32
34
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
33
35
  }
34
36
 
35
- const role = (session.user as { role: UserRole }).role;
36
- if (!role || !hasPermission(role, PERMISSIONS.BACKUP_CREATE)) {
37
+ if (!db) {
38
+ return NextResponse.json({ error: "Database not available" }, { status: 503 });
39
+ }
40
+
41
+ const orgs = await getUserOrganizations(db, session.user.id);
42
+ const hasAccess = orgs.some(org =>
43
+ hasPermission(org.role as UserRole, PERMISSIONS.BACKUP_CREATE)
44
+ );
45
+
46
+ if (!hasAccess) {
37
47
  return NextResponse.json({ error: "Forbidden" }, { status: 403 });
38
48
  }
39
49
 
@@ -57,8 +67,16 @@ export async function GET(req: NextRequest) {
57
67
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
58
68
  }
59
69
 
60
- const role = (session.user as { role: UserRole }).role;
61
- if (!role || !hasPermission(role, PERMISSIONS.BACKUP_VIEW)) {
70
+ if (!db) {
71
+ return NextResponse.json({ error: "Database not available" }, { status: 503 });
72
+ }
73
+
74
+ const orgs = await getUserOrganizations(db, session.user.id);
75
+ const hasAccess = orgs.some(org =>
76
+ hasPermission(org.role as UserRole, PERMISSIONS.BACKUP_VIEW)
77
+ );
78
+
79
+ if (!hasAccess) {
62
80
  return NextResponse.json({ error: "Forbidden" }, { status: 403 });
63
81
  }
64
82
 
@@ -22,7 +22,7 @@ import { APIError } from "better-auth/api";
22
22
  import type { db as database } from "@/lib/db";
23
23
  import { registerSchema } from "@/lib/validations/auth";
24
24
  import { generateSlug } from "@/lib/utils/slug";
25
- import { user, organizations, organizationMembers, userProfiles } from "@/lib/db/schema";
25
+ import { user, organizations, organizationMembers } from "@/lib/db/schema";
26
26
  import { logger } from "@/lib/logger";
27
27
 
28
28
  type Database = NonNullable<typeof database>;
@@ -98,7 +98,6 @@ export function buildRegisterHandler(deps: RegisterDeps) {
98
98
 
99
99
  try {
100
100
  await deps.db.transaction(async (tx) => {
101
- await tx.insert(userProfiles).values({ userId, name });
102
101
  await tx
103
102
  .insert(organizations)
104
103
  .values({ id: organizationId, name: orgName, slug: orgSlug });
@@ -60,9 +60,17 @@ export async function DELETE(req: NextRequest, { params }: RouteParams) {
60
60
  }
61
61
 
62
62
  const userId = session.user.id;
63
- const userRole = (session.user as { role?: string }).role as
64
- | UserRole
65
- | undefined;
63
+
64
+ // Get user role from organization membership
65
+ const { cookies } = await import("next/headers");
66
+ const cookieStore = await cookies();
67
+ const currentOrgId = cookieStore.get("orgId")?.value;
68
+
69
+ let userRole: UserRole | null = null;
70
+ if (currentOrgId) {
71
+ const { getUserRoleInOrganization } = await import("@/lib/auth/organization");
72
+ userRole = await getUserRoleInOrganization(db, userId, currentOrgId);
73
+ }
66
74
 
67
75
  const [existingComment] = await db
68
76
  .select({
@@ -80,8 +88,9 @@ export async function DELETE(req: NextRequest, { params }: RouteParams) {
80
88
  );
81
89
  }
82
90
 
91
+ // Users can delete their own comments, or admins/owners can delete any
83
92
  const canDelete =
84
- existingComment.userId === userId || userRole === "admin";
93
+ existingComment.userId === userId || userRole === "admin" || userRole === "owner";
85
94
 
86
95
  if (!canDelete) {
87
96
  return NextResponse.json(
@@ -21,7 +21,7 @@ import { eq, and } from "drizzle-orm";
21
21
  import { db } from "@/lib/db";
22
22
  import { feedback } from "@/lib/db/schema";
23
23
  import { auth } from "@/lib/auth/config";
24
- import { canUpdateFeedbackStatus, type UserRole } from "@/lib/auth/permissions";
24
+ import { canUpdateFeedbackStatus } from "@/lib/auth/permissions";
25
25
  import { classifyFeedback } from "@/lib/services/ai/classifier";
26
26
  import { apiError } from "@/lib/api/errors";
27
27
  import { getOrgContext } from "@/lib/auth/org-context";
@@ -81,9 +81,9 @@ export async function POST(
81
81
  );
82
82
  }
83
83
 
84
- const userRole = (session.user as { role?: string }).role as
85
- | UserRole
86
- | undefined;
84
+ // Get user role from organization membership (via context)
85
+ const { getUserRoleInOrganization } = await import("@/lib/auth/organization");
86
+ const userRole = await getUserRoleInOrganization(db, session.user.id, context.organizationId);
87
87
 
88
88
  if (!userRole || !canUpdateFeedbackStatus(userRole)) {
89
89
  return NextResponse.json(
@@ -43,9 +43,7 @@ export function buildCreateOrganizationHandler(deps: CreateOrganizationDeps) {
43
43
  if (!session) {
44
44
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
45
45
  }
46
- if (session.user.role !== "admin") {
47
- return NextResponse.json({ error: "Forbidden" }, { status: 403 });
48
- }
46
+ // Any authenticated user can create organizations - they become the owner
49
47
 
50
48
  let body: unknown;
51
49
  try {
@@ -87,7 +85,7 @@ export function buildCreateOrganizationHandler(deps: CreateOrganizationDeps) {
87
85
  await tx.insert(organizationMembers).values({
88
86
  organizationId,
89
87
  userId: session.user.id,
90
- role: "admin",
88
+ role: "owner",
91
89
  });
92
90
  return [created];
93
91
  });
@@ -32,15 +32,15 @@ interface SettingsSidebarProps {
32
32
  export function SettingsSidebar({ userRole }: SettingsSidebarProps) {
33
33
  const pathname = usePathname();
34
34
  const t = useTranslations("settings");
35
- const isAdmin = userRole === "admin";
36
- const isAdminOrPM = isAdmin || userRole === "product_manager";
35
+ const isOwnerOrAdmin = userRole === "owner" || userRole === "admin";
36
+ const isAdminOrPM = isOwnerOrAdmin || userRole === "product_manager";
37
37
 
38
38
  const groupedItems = [
39
39
  {
40
40
  title: t("groups.general"),
41
41
  items: [
42
42
  { href: "/settings/profile", label: t("items.profile"), icon: User },
43
- { href: "/settings/organization", label: t("items.organization"), icon: Users, show: isAdmin },
43
+ { href: "/settings/organization", label: t("items.organization"), icon: Users, show: isOwnerOrAdmin },
44
44
  ],
45
45
  },
46
46
  {
@@ -71,7 +71,7 @@ export function SettingsSidebar({ userRole }: SettingsSidebarProps) {
71
71
  { href: "/settings/widgets", label: t("items.widgetsEmbeds"), icon: LayoutGrid, show: isAdminOrPM },
72
72
  { href: "/settings/integrations", label: t("items.integrations"), icon: Plug, show: isAdminOrPM },
73
73
 
74
- { href: "/settings/danger-zone", label: t("items.dangerZone"), icon: AlertTriangle, show: isAdmin },
74
+ { href: "/settings/danger-zone", label: t("items.dangerZone"), icon: AlertTriangle, show: isOwnerOrAdmin },
75
75
  ],
76
76
  },
77
77
  ];
@@ -0,0 +1,116 @@
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
+ "use client";
19
+
20
+ import {
21
+ createContext,
22
+ useContext,
23
+ useState,
24
+ useCallback,
25
+ type ReactNode,
26
+ } from "react";
27
+ import type { UserRole } from "@/lib/auth/permissions";
28
+
29
+ export interface Organization {
30
+ id: string;
31
+ name: string;
32
+ slug: string;
33
+ role: UserRole;
34
+ }
35
+
36
+ interface OrganizationContextValue {
37
+ currentOrganization: Organization | null;
38
+ organizations: Organization[];
39
+ setOrganizations: (orgs: Organization[], currentId?: string | null) => void;
40
+ setCurrentOrganization: (orgId: string) => void;
41
+ }
42
+
43
+ const OrganizationContext = createContext<OrganizationContextValue | null>(
44
+ null
45
+ );
46
+
47
+ interface OrganizationProviderProps {
48
+ children: ReactNode;
49
+ initialOrganizations?: Organization[];
50
+ initialCurrentOrgId?: string | null;
51
+ }
52
+
53
+ export function OrganizationProvider({
54
+ children,
55
+ initialOrganizations = [],
56
+ initialCurrentOrgId,
57
+ }: OrganizationProviderProps) {
58
+ const [organizations, setOrganizationsState] =
59
+ useState<Organization[]>(initialOrganizations);
60
+ const [currentOrganization, setCurrentOrganizationState] =
61
+ useState<Organization | null>(() => {
62
+ if (initialCurrentOrgId) {
63
+ return (
64
+ initialOrganizations.find((o) => o.id === initialCurrentOrgId) || null
65
+ );
66
+ }
67
+ return initialOrganizations[0] || null;
68
+ });
69
+
70
+ const setOrganizations = useCallback(
71
+ (orgs: Organization[], currentId?: string | null) => {
72
+ setOrganizationsState(orgs);
73
+ const current = currentId ? orgs.find((o) => o.id === currentId) : orgs[0];
74
+ setCurrentOrganizationState(current || null);
75
+ },
76
+ []
77
+ );
78
+
79
+ const setCurrentOrganization = useCallback(
80
+ (orgId: string) => {
81
+ const org = organizations.find((o) => o.id === orgId);
82
+ if (org) {
83
+ setCurrentOrganizationState(org);
84
+ }
85
+ },
86
+ [organizations]
87
+ );
88
+
89
+ return (
90
+ <OrganizationContext.Provider
91
+ value={{
92
+ currentOrganization,
93
+ organizations,
94
+ setOrganizations,
95
+ setCurrentOrganization,
96
+ }}
97
+ >
98
+ {children}
99
+ </OrganizationContext.Provider>
100
+ );
101
+ }
102
+
103
+ export function useOrganization(): OrganizationContextValue {
104
+ const context = useContext(OrganizationContext);
105
+ if (!context) {
106
+ throw new Error(
107
+ "useOrganization must be used within an OrganizationProvider"
108
+ );
109
+ }
110
+ return context;
111
+ }
112
+
113
+ export function useCurrentRole(): UserRole | null {
114
+ const { currentOrganization } = useOrganization();
115
+ return currentOrganization?.role || null;
116
+ }