@nexttylabs/echo 0.5.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
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
+
3
17
  ## 0.5.0
4
18
 
5
19
  ### 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");
@@ -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
+ }
@@ -18,34 +18,46 @@
18
18
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
19
  */
20
20
 
21
- import { authClient } from "@/lib/auth/client";
22
21
  import {
23
22
  hasAllPermissions,
24
23
  hasPermission,
25
24
  type Permission,
26
25
  type UserRole,
27
26
  } from "@/lib/auth/permissions";
27
+ import { useCurrentRole } from "@/hooks/use-organization";
28
28
 
29
- type Session = (typeof authClient)["$Infer"]["Session"];
30
-
29
+ /**
30
+ * Check if the current user has a specific permission.
31
+ * Uses the user's role in the current organization from OrganizationProvider context.
32
+ *
33
+ * @param permission - The permission to check
34
+ * @param roleOverride - Optional role to use instead of the context role
35
+ * @returns true if the user has the permission
36
+ */
31
37
  export function useCan(
32
38
  permission: Permission,
33
- sessionOverride?: Session | null,
39
+ roleOverride?: UserRole | null,
34
40
  ): boolean {
35
- const session =
36
- sessionOverride === undefined ? authClient.useSession().data : sessionOverride;
37
- const role = (session?.user as { role?: UserRole })?.role;
41
+ const contextRole = useCurrentRole();
42
+ const role = roleOverride ?? contextRole;
38
43
 
39
44
  return role ? hasPermission(role, permission) : false;
40
45
  }
41
46
 
47
+ /**
48
+ * Check if the current user has all of the specified permissions.
49
+ * Uses the user's role in the current organization from OrganizationProvider context.
50
+ *
51
+ * @param permissions - Single permission or array of permissions to check
52
+ * @param roleOverride - Optional role to use instead of the context role
53
+ * @returns true if the user has all permissions
54
+ */
42
55
  export function useHasPermission(
43
56
  permissions: Permission | Permission[],
44
- sessionOverride?: Session | null,
57
+ roleOverride?: UserRole | null,
45
58
  ): boolean {
46
- const session =
47
- sessionOverride === undefined ? authClient.useSession().data : sessionOverride;
48
- const role = (session?.user as { role?: UserRole })?.role;
59
+ const contextRole = useCurrentRole();
60
+ const role = roleOverride ?? contextRole;
49
61
 
50
62
  if (!role) {
51
63
  return false;
@@ -54,3 +66,4 @@ export function useHasPermission(
54
66
  const list = Array.isArray(permissions) ? permissions : [permissions];
55
67
  return hasAllPermissions(role, list);
56
68
  }
69
+
@@ -19,6 +19,7 @@ import { and, eq } from "drizzle-orm";
19
19
  import { db } from "@/lib/db";
20
20
  import type { db as database } from "@/lib/db";
21
21
  import { organizationMembers, organizations } from "@/lib/db/schema";
22
+ import type { UserRole } from "@/lib/auth/permissions";
22
23
 
23
24
  type Database = NonNullable<typeof database>;
24
25
 
@@ -105,3 +106,22 @@ export async function getCurrentOrganizationId(userId: string): Promise<string |
105
106
  const org = await getUserOrganization(db, userId);
106
107
  return org?.id || null;
107
108
  }
109
+
110
+ export async function getUserRoleInOrganization(
111
+ db: Database,
112
+ userId: string,
113
+ organizationId: string
114
+ ): Promise<UserRole | null> {
115
+ const [member] = await db
116
+ .select({ role: organizationMembers.role })
117
+ .from(organizationMembers)
118
+ .where(
119
+ and(
120
+ eq(organizationMembers.userId, userId),
121
+ eq(organizationMembers.organizationId, organizationId)
122
+ )
123
+ )
124
+ .limit(1);
125
+
126
+ return (member?.role as UserRole) || null;
127
+ }
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  export type UserRole =
19
+ | "owner"
19
20
  | "admin"
20
21
  | "product_manager"
21
22
  | "developer"
@@ -35,6 +36,15 @@ export const PERMISSIONS = {
35
36
  export type Permission = (typeof PERMISSIONS)[keyof typeof PERMISSIONS];
36
37
 
37
38
  export const ROLE_PERMISSIONS: Record<UserRole, Permission[]> = {
39
+ owner: [
40
+ PERMISSIONS.CREATE_FEEDBACK,
41
+ PERMISSIONS.SUBMIT_ON_BEHALF,
42
+ PERMISSIONS.DELETE_FEEDBACK,
43
+ PERMISSIONS.MANAGE_ORG,
44
+ PERMISSIONS.UPDATE_FEEDBACK_STATUS,
45
+ PERMISSIONS.BACKUP_CREATE,
46
+ PERMISSIONS.BACKUP_VIEW,
47
+ ],
38
48
  admin: [
39
49
  PERMISSIONS.CREATE_FEEDBACK,
40
50
  PERMISSIONS.SUBMIT_ON_BEHALF,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexttylabs/echo",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "license": "AGPL-3.0",
5
5
  "private": false,
6
6
  "publishConfig": {