feat: add check for existing organization members to prevent duplicates during member creation
This commit is contained in:
@@ -1,16 +1,17 @@
|
|||||||
import { AppError, ErrorCodes } from '@api/utils/errors';
|
import { AppError, ErrorCodes } from '@api/utils/errors';
|
||||||
import { getProfileByEmail, getProfileById, updateProfile } from '@cio/db/queries/auth';
|
|
||||||
import { getLicenseStatus } from '@api/services/license';
|
|
||||||
|
|
||||||
import type { OrganizationWithMemberAndPlans } from '@cio/db/queries/organization/types';
|
|
||||||
import type { TProfile } from '@cio/db/types';
|
|
||||||
import {
|
import {
|
||||||
createOrganizationMember,
|
createOrganizationMember,
|
||||||
getFirstOrganization,
|
getFirstOrganization,
|
||||||
getOrganizationByProfileId
|
getOrganizationByProfileId,
|
||||||
|
hasOrgMemberByProfileIdOrEmail
|
||||||
} from '@cio/db/queries/organization';
|
} from '@cio/db/queries/organization';
|
||||||
import { env } from '@api/config/env';
|
import { getProfileByEmail, getProfileById, updateProfile } from '@cio/db/queries/auth';
|
||||||
|
|
||||||
|
import type { OrganizationWithMemberAndPlans } from '@cio/db/queries/organization/types';
|
||||||
import { ROLE } from '@cio/utils/constants';
|
import { ROLE } from '@cio/utils/constants';
|
||||||
|
import type { TProfile } from '@cio/db/types';
|
||||||
|
import { env } from '@api/config/env';
|
||||||
|
import { getLicenseStatus } from '@api/services/license';
|
||||||
|
|
||||||
export type GetAccountDataResult = {
|
export type GetAccountDataResult = {
|
||||||
profile: TProfile;
|
profile: TProfile;
|
||||||
@@ -35,17 +36,23 @@ export async function getAccountData(userId: string): Promise<GetAccountDataResu
|
|||||||
throw new AppError(`Account not found for user ID: ${userId}`, ErrorCodes.ACCOUNT_NOT_FOUND, 404);
|
throw new AppError(`Account not found for user ID: ${userId}`, ErrorCodes.ACCOUNT_NOT_FOUND, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Self-hosted: auto-add user as student to the single org if they are not a member
|
// Self-hosted: auto-add user as student to the single org if they are not a member.
|
||||||
|
// Skip if they already have membership (by profileId) or a pending invite (by email).
|
||||||
if (env.PUBLIC_IS_SELFHOSTED === 'true' && organizations.length === 0) {
|
if (env.PUBLIC_IS_SELFHOSTED === 'true' && organizations.length === 0) {
|
||||||
const firstOrg = await getFirstOrganization();
|
const firstOrg = await getFirstOrganization();
|
||||||
if (firstOrg) {
|
if (firstOrg) {
|
||||||
await createOrganizationMember({
|
const alreadyInOrg = await hasOrgMemberByProfileIdOrEmail(firstOrg.id, userId, profile.email ?? undefined);
|
||||||
organizationId: firstOrg.id,
|
|
||||||
profileId: userId,
|
if (!alreadyInOrg) {
|
||||||
roleId: ROLE.STUDENT,
|
await createOrganizationMember({
|
||||||
verified: true
|
organizationId: firstOrg.id,
|
||||||
});
|
profileId: userId,
|
||||||
organizations = await getOrganizationByProfileId(userId);
|
roleId: ROLE.STUDENT,
|
||||||
|
verified: true
|
||||||
|
});
|
||||||
|
|
||||||
|
organizations = await getOrganizationByProfileId(userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -380,7 +380,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
<Popover.Content align="start" class="w-44 p-3">
|
<Popover.Content align="start" class="z-250! w-44 p-3">
|
||||||
<div class="grid grid-cols-5 gap-2">
|
<div class="grid grid-cols-5 gap-2">
|
||||||
{#each TAG_COLOR_OPTIONS as color (color)}
|
{#each TAG_COLOR_OPTIONS as color (color)}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -165,6 +165,31 @@ export const checkEmailExistsInOrg = async (orgId: string, email: string): Promi
|
|||||||
return result.length > 0;
|
return result.length > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an org has a member matching the given profileId or email.
|
||||||
|
* Used to avoid creating duplicate members (e.g. when user has a pending invite).
|
||||||
|
*/
|
||||||
|
export const hasOrgMemberByProfileIdOrEmail = async (
|
||||||
|
orgId: string,
|
||||||
|
profileId: string,
|
||||||
|
email: string | null | undefined
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const conditions = [eq(schema.organizationmember.organizationId, orgId)];
|
||||||
|
|
||||||
|
const profileOrEmailMatch = or(
|
||||||
|
eq(schema.organizationmember.profileId, profileId),
|
||||||
|
...(email ? [eq(schema.organizationmember.email, email.toLowerCase().trim())] : [])
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await db
|
||||||
|
.select({ id: schema.organizationmember.id })
|
||||||
|
.from(schema.organizationmember)
|
||||||
|
.where(and(...conditions, profileOrEmailMatch))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return result.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks which emails already exist as team members in an organization (bulk check)
|
* Checks which emails already exist as team members in an organization (bulk check)
|
||||||
* Checks both organizationmember.email and profile.email (via organizationmember.profileId)
|
* Checks both organizationmember.email and profile.email (via organizationmember.profileId)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
unique,
|
unique,
|
||||||
|
uniqueIndex,
|
||||||
uuid,
|
uuid,
|
||||||
varchar
|
varchar
|
||||||
} from 'drizzle-orm/pg-core';
|
} from 'drizzle-orm/pg-core';
|
||||||
@@ -1467,7 +1468,10 @@ export const organizationmember = pgTable(
|
|||||||
}),
|
}),
|
||||||
index('idx_organizationmember_profile_id').on(table.profileId),
|
index('idx_organizationmember_profile_id').on(table.profileId),
|
||||||
index('idx_organizationmember_organization_id').on(table.organizationId),
|
index('idx_organizationmember_organization_id').on(table.organizationId),
|
||||||
index('idx_organizationmember_profile_org').on(table.profileId, table.organizationId)
|
index('idx_organizationmember_profile_org').on(table.profileId, table.organizationId),
|
||||||
|
uniqueIndex('organizationmember_org_profile_unique')
|
||||||
|
.on(table.organizationId, table.profileId)
|
||||||
|
.where(sql`${table.profileId} IS NOT NULL`)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user