Enhance audience management features with new components and API integrations
- Added `AssignCoursesModal`, `AudienceMemberRow`, `AudienceTable`, and related components for improved audience management UI. - Implemented audience invite actions including resend and revoke functionality. - Updated audience API to support importing members and assigning courses. - Refactored audience data handling to include status and invite management. - Improved overall user experience with pagination and selection features in the audience table.
This commit is contained in:
@@ -37,7 +37,7 @@ async function isSelfHostedFirstSignup(): Promise<boolean> {
|
||||
* Self-hosted first signup: auto-verify email (no orgs exist yet, this user will create the only org).
|
||||
*/
|
||||
export const createProfileHook = async (user: User) => {
|
||||
console.debug('createProfileHook', user);
|
||||
console.log('[auth] createProfileHook: running', { userId: user.id });
|
||||
|
||||
const existingProfile = await db.select().from(schema.profile).where(eq(schema.profile.id, user.id)).limit(1);
|
||||
console.debug('existingProfile', existingProfile);
|
||||
|
||||
@@ -16,6 +16,7 @@ const afterHooks: Array<(context: AuthMiddlewareContext) => Promise<unknown>> =
|
||||
* This function will be called by better-auth after each request
|
||||
*/
|
||||
export const combineAfterHooks = async (context: AuthMiddlewareContext) => {
|
||||
console.log('[auth] combineAfterHooks: running');
|
||||
for (const hook of afterHooks) {
|
||||
try {
|
||||
await hook(context);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import * as schema from '@db/schema';
|
||||
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { and, eq, isNull } from 'drizzle-orm';
|
||||
|
||||
import { User } from 'better-auth';
|
||||
import { db } from '@db/drizzle';
|
||||
import { enrollUsersInCourseGroups } from '@db/queries/group';
|
||||
import { getOrgCourseGroups } from '@db/queries/course';
|
||||
import { markUserAndProfileEmailVerified } from '@db/queries/auth';
|
||||
|
||||
const DEFAULT_STUDENT_ROLE_ID = 3;
|
||||
@@ -21,13 +23,25 @@ export async function ensureOrgMembership(
|
||||
): Promise<void> {
|
||||
const emailLower = email.toLowerCase();
|
||||
|
||||
const [existing] = await db
|
||||
const [existingByProfile] = await db
|
||||
.select()
|
||||
.from(schema.organizationmember)
|
||||
.where(and(eq(schema.organizationmember.organizationId, orgId), eq(schema.organizationmember.profileId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (existing) {
|
||||
if (existingByProfile) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** Audience import pre-creates a row with org + email before profile exists; do not insert again. */
|
||||
const [existingByEmail] = await db
|
||||
.select()
|
||||
.from(schema.organizationmember)
|
||||
.where(and(eq(schema.organizationmember.organizationId, orgId), eq(schema.organizationmember.email, emailLower)))
|
||||
.limit(1);
|
||||
|
||||
if (existingByEmail?.profileId && existingByEmail.profileId !== userId) {
|
||||
console.error('ensureOrgMembership: org member email already linked to another profile');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -38,20 +52,33 @@ export async function ensureOrgMembership(
|
||||
and(
|
||||
eq(schema.organizationInvite.organizationId, orgId),
|
||||
eq(schema.organizationInvite.email, emailLower),
|
||||
eq(schema.organizationInvite.isRevoked, false)
|
||||
eq(schema.organizationInvite.isRevoked, false),
|
||||
isNull(schema.organizationInvite.acceptedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (invite) {
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(schema.organizationmember).values({
|
||||
organizationId: orgId,
|
||||
profileId: userId,
|
||||
email: emailLower,
|
||||
roleId: invite.roleId,
|
||||
verified: true
|
||||
});
|
||||
if (existingByEmail) {
|
||||
await tx
|
||||
.update(schema.organizationmember)
|
||||
.set({
|
||||
profileId: userId,
|
||||
email: emailLower,
|
||||
roleId: invite.roleId,
|
||||
verified: true
|
||||
})
|
||||
.where(eq(schema.organizationmember.id, existingByEmail.id));
|
||||
} else {
|
||||
await tx.insert(schema.organizationmember).values({
|
||||
organizationId: orgId,
|
||||
profileId: userId,
|
||||
email: emailLower,
|
||||
roleId: invite.roleId,
|
||||
verified: true
|
||||
});
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(schema.organizationInvite)
|
||||
@@ -65,10 +92,46 @@ export async function ensureOrgMembership(
|
||||
await markUserAndProfileEmailVerified(userId, tx);
|
||||
});
|
||||
|
||||
const metadata = invite.metadata as Record<string, unknown> | null;
|
||||
const courseIds = Array.isArray(metadata?.courseIds) ? (metadata.courseIds as string[]) : [];
|
||||
|
||||
if (courseIds.length > 0) {
|
||||
try {
|
||||
const courseGroups = await getOrgCourseGroups(orgId, courseIds);
|
||||
const validGroupIds = courseGroups.map((cg) => cg.groupId).filter(Boolean) as string[];
|
||||
await enrollUsersInCourseGroups(validGroupIds, [{ profileId: userId, email: emailLower }], invite.roleId);
|
||||
} catch (error) {
|
||||
console.error('ensureOrgMembership course enrollment error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
console.debug('User joined org via invite:', orgId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingByEmail) {
|
||||
const [policy] = await db
|
||||
.select()
|
||||
.from(schema.organizationAuthPolicy)
|
||||
.where(eq(schema.organizationAuthPolicy.organizationId, orgId))
|
||||
.limit(1);
|
||||
|
||||
const defaultRoleId = policy?.defaultRoleId ?? DEFAULT_STUDENT_ROLE_ID;
|
||||
const finalRoleId = roleId ?? defaultRoleId;
|
||||
|
||||
await db
|
||||
.update(schema.organizationmember)
|
||||
.set({
|
||||
profileId: userId,
|
||||
verified: true,
|
||||
roleId: finalRoleId
|
||||
})
|
||||
.where(eq(schema.organizationmember.id, existingByEmail.id));
|
||||
|
||||
console.debug('User linked to existing org membership row:', orgId);
|
||||
return;
|
||||
}
|
||||
|
||||
const [policy] = await db
|
||||
.select()
|
||||
.from(schema.organizationAuthPolicy)
|
||||
@@ -94,6 +157,7 @@ export async function ensureOrgMembership(
|
||||
* This runs after profile creation and creates organization membership
|
||||
*/
|
||||
export const ssoProvisioningHook = async (user: User) => {
|
||||
console.log('[auth] ssoProvisioningHook: running', { userId: user.id });
|
||||
if (!user.email) {
|
||||
console.debug('No email for user, skipping SSO provisioning');
|
||||
return;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { eq } from 'drizzle-orm';
|
||||
import { hasVerifiedEmailProvider } from './create-profile';
|
||||
|
||||
export const syncUserWithProfile = async (user: User) => {
|
||||
console.debug('syncUserWithProfile', user);
|
||||
console.log('[auth] syncUserWithProfile: running', { userId: user?.id });
|
||||
|
||||
if (!user?.id) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user