feat: enhance API and middleware functionality: add public API routes, implement program and course member checks, and improve rate limiting for invite acceptance. Update OpenAPI spec generation to focus on public API endpoints and add new middleware for automation key scopes. Include new documentation for lesson AI features and public API tests.
This commit is contained in:
@@ -272,6 +272,59 @@ export async function getLatestOrgInvitesByEmails(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest active (non-revoked, non-accepted, non-expired) org invite for a given org+email.
|
||||
* Used to surface a pending invite to a logged-in student on the LMS dashboard.
|
||||
*/
|
||||
export async function getActivePendingOrgInviteForEmail(
|
||||
organizationId: string,
|
||||
email: string
|
||||
): Promise<TOrganizationInviteTokenData | null> {
|
||||
const normalized = email.toLowerCase().trim();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const [row] = await db
|
||||
.select({
|
||||
invite: schema.organizationInvite,
|
||||
organization: {
|
||||
id: schema.organization.id,
|
||||
name: schema.organization.name,
|
||||
siteName: schema.organization.siteName
|
||||
}
|
||||
})
|
||||
.from(schema.organizationInvite)
|
||||
.innerJoin(schema.organization, eq(schema.organizationInvite.organizationId, schema.organization.id))
|
||||
.where(
|
||||
and(
|
||||
eq(schema.organizationInvite.organizationId, organizationId),
|
||||
eq(schema.organizationInvite.email, normalized),
|
||||
eq(schema.organizationInvite.isRevoked, false),
|
||||
isNull(schema.organizationInvite.acceptedAt),
|
||||
gt(schema.organizationInvite.expiresAt, sql`NOW()`)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(schema.organizationInvite.createdAt))
|
||||
.limit(1);
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...row,
|
||||
organization: { ...row.organization, siteName: row.organization.siteName ?? '' }
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('getActivePendingOrgInviteForEmail error:', error);
|
||||
throw new Error(
|
||||
`Failed to get active pending org invite: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Latest organization_invite row for an org + email (by created_at), for metadata when resending.
|
||||
*/
|
||||
|
||||
@@ -268,6 +268,67 @@ export const deleteOrganizationAudienceMember = async (orgId: string, memberId:
|
||||
return deleted || null;
|
||||
};
|
||||
|
||||
export const getOrganizationAudienceMember = async (orgId: string, memberId: number) => {
|
||||
const [row] = await db
|
||||
.select({
|
||||
memberId: schema.organizationmember.id,
|
||||
profileId: schema.organizationmember.profileId,
|
||||
fullname: schema.profile.fullname,
|
||||
email: sql<string>`coalesce(${schema.profile.email}, ${schema.organizationmember.email})`.as('email'),
|
||||
avatarUrl: schema.profile.avatarUrl,
|
||||
profileCreatedAt: schema.profile.createdAt,
|
||||
memberCreatedAt: schema.organizationmember.createdAt
|
||||
})
|
||||
.from(schema.organizationmember)
|
||||
.leftJoin(schema.profile, eq(schema.organizationmember.profileId, schema.profile.id))
|
||||
.where(
|
||||
and(
|
||||
eq(schema.organizationmember.organizationId, orgId),
|
||||
eq(schema.organizationmember.id, memberId),
|
||||
eq(schema.organizationmember.roleId, ROLE.STUDENT)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const email = row.email?.trim() ?? '';
|
||||
const name = row.fullname?.trim() || (email.includes('@') ? email.split('@')[0] : email) || '';
|
||||
const createdAtRaw = row.profileId ? row.profileCreatedAt : row.memberCreatedAt;
|
||||
const createdAt = createdAtRaw ? new Date(createdAtRaw).toDateString() : '';
|
||||
|
||||
return {
|
||||
id: row.memberId,
|
||||
profileId: row.profileId ?? null,
|
||||
name,
|
||||
email,
|
||||
avatarUrl: row.avatarUrl || '',
|
||||
createdAt
|
||||
};
|
||||
};
|
||||
|
||||
export const updateOrganizationAudienceMember = async (
|
||||
orgId: string,
|
||||
memberId: number,
|
||||
data: Partial<Pick<TNewOrganizationmember, 'email' | 'verified'>>
|
||||
) => {
|
||||
const [updated] = await db
|
||||
.update(schema.organizationmember)
|
||||
.set(data)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.organizationmember.organizationId, orgId),
|
||||
eq(schema.organizationmember.id, memberId),
|
||||
eq(schema.organizationmember.roleId, ROLE.STUDENT)
|
||||
)
|
||||
)
|
||||
.returning();
|
||||
|
||||
return updated || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a user is an admin of an organization
|
||||
* @param orgId Organization ID
|
||||
|
||||
Reference in New Issue
Block a user