Skip to content

Commit 54bc8b1

Browse files
committed
feat(api, web): update organization membership management to use email instead of user ID
1 parent 5e2f545 commit 54bc8b1

5 files changed

Lines changed: 223 additions & 129 deletions

File tree

apps/api/src/db/queries.ts

Lines changed: 155 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ import {
2929
executions,
3030
type ExecutionStatusType,
3131
type MembershipInsert,
32+
type MembershipRow,
3233
memberships,
3334
type OrganizationInsert,
3435
OrganizationRole,
36+
type OrganizationRoleType,
3537
organizations,
3638
Plan,
3739
type PlanType,
@@ -1703,67 +1705,117 @@ export async function deleteOrganization(
17031705
}
17041706

17051707
/**
1706-
* Add or update a user's membership in an organization (only admins and owners can do this)
1708+
* Check if a user is the owner of an organization
1709+
*
1710+
* @param db Database instance
1711+
* @param organizationId Organization ID
1712+
* @param userId User ID to check
1713+
* @returns True if user is the organization owner, false otherwise
1714+
*/
1715+
export async function isOrganizationOwner(
1716+
db: ReturnType<typeof createDatabase>,
1717+
organizationId: string,
1718+
userId: string
1719+
): Promise<boolean> {
1720+
const [user] = await db
1721+
.select()
1722+
.from(users)
1723+
.where(eq(users.id, userId))
1724+
.limit(1);
1725+
1726+
return user?.organizationId === organizationId;
1727+
}
1728+
1729+
/**
1730+
* Add or update a user's membership in an organization
1731+
*
1732+
* Role-based permissions:
1733+
* - Only owners and admins can add/update memberships
1734+
* - Only owners can assign admin roles
1735+
* - Only owners can assign owner roles (but owner role cannot be changed)
1736+
* - Members cannot add/update memberships
17071737
*
17081738
* @param db Database instance
17091739
* @param organizationIdOrHandle Organization ID or handle
1710-
* @param targetUserId User ID to add/update membership for
1740+
* @param targetUserEmail Email of the user to add/update membership for
17111741
* @param role Role to assign (member, admin, owner)
17121742
* @param adminUserId User ID of the admin/owner making the change
1713-
* @returns The created or updated membership record, or null if permission denied
1743+
* @returns The created or updated membership record, or null if permission denied or user not found
17141744
*/
17151745
export async function addOrUpdateMembership(
17161746
db: ReturnType<typeof createDatabase>,
17171747
organizationIdOrHandle: string,
1718-
targetUserId: string,
1748+
targetUserEmail: string,
17191749
role: OrganizationRoleType,
17201750
adminUserId: string
17211751
): Promise<MembershipRow | null> {
1722-
// First, verify the admin user has permission (admin or owner)
1723-
const [adminMembership] = await db
1752+
// Get the organization ID first
1753+
const [organization] = await db
17241754
.select()
1725-
.from(memberships)
1726-
.innerJoin(organizations, eq(memberships.organizationId, organizations.id))
1727-
.where(
1728-
and(
1729-
eq(memberships.userId, adminUserId),
1730-
getOrganizationCondition(organizationIdOrHandle),
1731-
inArray(memberships.role, [
1732-
OrganizationRole.ADMIN,
1733-
OrganizationRole.OWNER,
1734-
])
1735-
)
1736-
)
1755+
.from(organizations)
1756+
.where(getOrganizationCondition(organizationIdOrHandle))
17371757
.limit(1);
17381758

1739-
if (!adminMembership) {
1740-
return null; // Admin user doesn't have permission
1759+
if (!organization) {
1760+
return null; // Organization not found
17411761
}
17421762

1743-
// Additional check: only owners can assign the owner role
1744-
if (
1745-
role === OrganizationRole.OWNER &&
1746-
adminMembership.memberships.role !== OrganizationRole.OWNER
1747-
) {
1748-
return null; // Only owners can assign owner role
1763+
const organizationId = organization.id;
1764+
1765+
// Check if the admin user is the organization owner
1766+
const isAdminOwner = await isOrganizationOwner(db, organizationId, adminUserId);
1767+
1768+
// If not the owner, check if they have admin role
1769+
let hasAdminRole = false;
1770+
if (!isAdminOwner) {
1771+
const [adminMembership] = await db
1772+
.select()
1773+
.from(memberships)
1774+
.where(
1775+
and(
1776+
eq(memberships.userId, adminUserId),
1777+
eq(memberships.organizationId, organizationId),
1778+
eq(memberships.role, OrganizationRole.ADMIN)
1779+
)
1780+
)
1781+
.limit(1);
1782+
hasAdminRole = !!adminMembership;
17491783
}
17501784

1751-
const organizationId = adminMembership.organizations.id;
1752-
const now = new Date();
1785+
// Permission check: Only owners and admins can add/update memberships
1786+
if (!isAdminOwner && !hasAdminRole) {
1787+
return null; // User doesn't have permission
1788+
}
1789+
1790+
// Role assignment restrictions
1791+
if (role === OrganizationRole.OWNER) {
1792+
return null; // Owner role cannot be assigned - there's only one owner (the creator)
1793+
}
17531794

1754-
// CRITICAL: Prevent changing the role of the main owner in their personal organization
1755-
// Check if the target user is the main owner of their personal organization
1795+
if (role === OrganizationRole.ADMIN && !isAdminOwner) {
1796+
return null; // Only owners can assign admin roles
1797+
}
1798+
1799+
// Look up the target user by email
17561800
const [targetUser] = await db
17571801
.select()
17581802
.from(users)
1759-
.where(eq(users.id, targetUserId))
1803+
.where(eq(users.email, targetUserEmail))
17601804
.limit(1);
17611805

1762-
if (targetUser && targetUser.organizationId === organizationId) {
1763-
// This is the user's personal organization - their role cannot be changed
1764-
return null; // Cannot change role of main owner in their personal organization
1806+
if (!targetUser) {
1807+
return null; // User not found with this email
17651808
}
17661809

1810+
const targetUserId = targetUser.id;
1811+
1812+
// Prevent adding the organization owner as a member (they're already the owner)
1813+
if (targetUser.organizationId === organizationId) {
1814+
return null; // Cannot add/change role of the organization owner
1815+
}
1816+
1817+
const now = new Date();
1818+
17671819
// Check if the target user is already a member
17681820
const [existingMembership] = await db
17691821
.select()
@@ -1813,39 +1865,85 @@ export async function addOrUpdateMembership(
18131865
}
18141866

18151867
/**
1816-
* Delete a user's membership from an organization (only admins and owners can do this)
1868+
* Delete a user's membership from an organization
1869+
*
1870+
* Role-based permissions:
1871+
* - Only owners and admins can remove memberships
1872+
* - The organization owner cannot be removed
1873+
* - Users cannot remove themselves
1874+
* - Only owners can remove admins
18171875
*
18181876
* @param db Database instance
18191877
* @param organizationIdOrHandle Organization ID or handle
1820-
* @param targetUserId User ID to remove from the organization
1878+
* @param targetUserEmail Email of the user to remove from the organization
18211879
* @param adminUserId User ID of the admin/owner making the change
18221880
* @returns True if membership was deleted, false if permission denied or not found
18231881
*/
18241882
export async function deleteMembership(
18251883
db: ReturnType<typeof createDatabase>,
18261884
organizationIdOrHandle: string,
1827-
targetUserId: string,
1885+
targetUserEmail: string,
18281886
adminUserId: string
18291887
): Promise<boolean> {
1830-
// First, verify the admin user has permission (admin or owner)
1831-
const [adminMembership] = await db
1888+
// Get the organization ID first
1889+
const [organization] = await db
18321890
.select()
1833-
.from(memberships)
1834-
.innerJoin(organizations, eq(memberships.organizationId, organizations.id))
1835-
.where(
1836-
and(
1837-
eq(memberships.userId, adminUserId),
1838-
getOrganizationCondition(organizationIdOrHandle),
1839-
inArray(memberships.role, [
1840-
OrganizationRole.ADMIN,
1841-
OrganizationRole.OWNER,
1842-
])
1891+
.from(organizations)
1892+
.where(getOrganizationCondition(organizationIdOrHandle))
1893+
.limit(1);
1894+
1895+
if (!organization) {
1896+
return false; // Organization not found
1897+
}
1898+
1899+
const organizationId = organization.id;
1900+
1901+
// Check if the admin user is the organization owner
1902+
const isAdminOwner = await isOrganizationOwner(db, organizationId, adminUserId);
1903+
1904+
// If not the owner, check if they have admin role
1905+
let hasAdminRole = false;
1906+
if (!isAdminOwner) {
1907+
const [adminMembership] = await db
1908+
.select()
1909+
.from(memberships)
1910+
.where(
1911+
and(
1912+
eq(memberships.userId, adminUserId),
1913+
eq(memberships.organizationId, organizationId),
1914+
eq(memberships.role, OrganizationRole.ADMIN)
1915+
)
18431916
)
1844-
)
1917+
.limit(1);
1918+
hasAdminRole = !!adminMembership;
1919+
}
1920+
1921+
// Permission check: Only owners and admins can remove memberships
1922+
if (!isAdminOwner && !hasAdminRole) {
1923+
return false; // User doesn't have permission
1924+
}
1925+
1926+
// Look up the target user by email
1927+
const [targetUser] = await db
1928+
.select()
1929+
.from(users)
1930+
.where(eq(users.email, targetUserEmail))
18451931
.limit(1);
18461932

1847-
if (!adminMembership) {
1848-
return false; // Admin user doesn't have permission
1933+
if (!targetUser) {
1934+
return false; // User not found with this email
1935+
}
1936+
1937+
const targetUserId = targetUser.id;
1938+
1939+
// Prevent removing the organization owner
1940+
if (targetUser.organizationId === organizationId) {
1941+
return false; // Cannot remove the organization owner
1942+
}
1943+
1944+
// Prevent users from removing themselves
1945+
if (targetUserId === adminUserId) {
1946+
return false; // Users cannot remove their own membership
18491947
}
18501948

18511949
// Get the target user's membership to check their role
@@ -1855,7 +1953,7 @@ export async function deleteMembership(
18551953
.where(
18561954
and(
18571955
eq(memberships.userId, targetUserId),
1858-
eq(memberships.organizationId, adminMembership.organizations.id)
1956+
eq(memberships.organizationId, organizationId)
18591957
)
18601958
)
18611959
.limit(1);
@@ -1864,33 +1962,9 @@ export async function deleteMembership(
18641962
return false; // Target user is not a member
18651963
}
18661964

1867-
// Additional check: only owners can delete other owners
1868-
if (
1869-
targetMembership.role === OrganizationRole.OWNER &&
1870-
adminMembership.memberships.role !== OrganizationRole.OWNER
1871-
) {
1872-
return false; // Only owners can delete other owners
1873-
}
1874-
1875-
// Prevent users from deleting themselves
1876-
if (targetUserId === adminUserId) {
1877-
return false; // Users cannot delete their own membership
1878-
}
1879-
1880-
// CRITICAL: Prevent removing the main owner from their personal organization
1881-
// Check if the target user is the main owner of their personal organization
1882-
const [targetUser] = await db
1883-
.select()
1884-
.from(users)
1885-
.where(eq(users.id, targetUserId))
1886-
.limit(1);
1887-
1888-
if (
1889-
targetUser &&
1890-
targetUser.organizationId === adminMembership.organizations.id
1891-
) {
1892-
// This is the user's personal organization - they cannot be removed as the main owner
1893-
return false; // Cannot remove main owner from their personal organization
1965+
// Only owners can remove admins
1966+
if (targetMembership.role === OrganizationRole.ADMIN && !isAdminOwner) {
1967+
return false; // Only owners can remove admins
18941968
}
18951969

18961970
// Delete the membership
@@ -1899,7 +1973,7 @@ export async function deleteMembership(
18991973
.where(
19001974
and(
19011975
eq(memberships.userId, targetUserId),
1902-
eq(memberships.organizationId, adminMembership.organizations.id)
1976+
eq(memberships.organizationId, organizationId)
19031977
)
19041978
)
19051979
.returning({ id: memberships.userId });

0 commit comments

Comments
 (0)