feat: redo settings and all org pages. (#601)
* feat: new components, deploy storybook * refactor: simplify component field definitions and enhance button loading state - Removed unnecessary type imports and simplified the FIELDS array definitions across multiple components in Storybook. - Updated button component to use a loading state instead of isLoading, improving clarity and consistency in props. - Adjusted button stories to reflect the new loading prop and enhanced the button's loading visual feedback. - Minor CSS adjustments for better layout and styling consistency. * chore: add field, card, password components * feat: improve page component & update AuthUI path * chore: update bits-ui to version 2.14.4 and all org related UI's - Updated bits-ui dependency in pnpm-lock.yaml and package.json to version 2.14.4. - Refactored imports for RoleBasedSecurity and UpgradeBanner components to use the new ui feature directory. - Removed unused Course components and related files to clean up the codebase. - Enhanced various components to improve consistency and maintainability. * chore: move all setting buttons to the top and update auth ui styles * chore: remove svelte-loading-spinners and replace with @cio/ui/base/spinner - Removed all instances of svelte-loading-spinners from the project. - Updated components to use the new Spinner from @cio/ui/base/spinner. - Cleaned up pnpm-lock.yaml and package.json to reflect the removal of svelte-loading-spinners. - Added new CSS animations for rocket launch effects in app.css. - Enhanced email change flow with updated verification messages in translations. * chore: remove supabase file upload from dashboard and move to r2 * chore: update organization API and UI components - Added new API methods for organization plan operations and team management. - Introduced middleware for API key authentication and organization admin checks. - Refactored organization-related components to utilize the new API structure. - Updated various UI components to improve consistency and maintainability. - Enhanced error handling and validation for organization-related actions. - Removed deprecated services and streamlined the codebase.
This commit is contained in:
@@ -2,8 +2,8 @@ import * as schema from '@db/schema';
|
||||
|
||||
import { and, eq, sql } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@db/drizzle';
|
||||
import { TNewCourse } from '@db/types';
|
||||
import { db } from '@db/drizzle';
|
||||
|
||||
/**
|
||||
* Gets courses by organization siteName
|
||||
@@ -50,15 +50,89 @@ export const getCoursesBySiteName = async (siteName: string) => {
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get courses by site name "${siteName}": ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
throw new Error(
|
||||
`Failed to get courses by site name "${siteName}": ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets lessons by organization siteName
|
||||
* @param siteName Organization site name
|
||||
* @returns Array of lessons
|
||||
*/
|
||||
export const getLessonsBySiteName = async (siteName: string) => {
|
||||
const result = await db
|
||||
.select({
|
||||
lesson: schema.lesson
|
||||
})
|
||||
.from(schema.lesson)
|
||||
.innerJoin(schema.course, eq(schema.lesson.courseId, schema.course.id))
|
||||
.innerJoin(schema.group, eq(schema.course.groupId, schema.group.id))
|
||||
.innerJoin(schema.organization, eq(schema.group.organizationId, schema.organization.id))
|
||||
.where(eq(schema.organization.siteName, siteName))
|
||||
.limit(1);
|
||||
|
||||
return result.map((row) => row.lesson);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets exercises by organization siteName
|
||||
* @param siteName Organization site name
|
||||
* @returns Array of exercises
|
||||
*/
|
||||
export const getExercisesBySiteName = async (siteName: string) => {
|
||||
const result = await db
|
||||
.select({
|
||||
exercise: schema.exercise
|
||||
})
|
||||
.from(schema.exercise)
|
||||
.innerJoin(schema.lesson, eq(schema.exercise.lessonId, schema.lesson.id))
|
||||
.innerJoin(schema.course, eq(schema.lesson.courseId, schema.course.id))
|
||||
.innerJoin(schema.group, eq(schema.course.groupId, schema.group.id))
|
||||
.innerJoin(schema.organization, eq(schema.group.organizationId, schema.organization.id))
|
||||
.where(eq(schema.organization.siteName, siteName))
|
||||
.limit(1);
|
||||
|
||||
return result.map((row) => row.exercise);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets courses by organization siteName (for setup - includes unpublished courses)
|
||||
* @param siteName Organization site name
|
||||
* @returns Array of courses with organization info
|
||||
*/
|
||||
export const getCoursesBySiteNameForSetup = async (siteName: string) => {
|
||||
const result = await db
|
||||
.select({
|
||||
course: schema.course,
|
||||
organization: {
|
||||
id: schema.organization.id,
|
||||
name: schema.organization.name,
|
||||
siteName: schema.organization.siteName,
|
||||
avatarUrl: schema.organization.avatarUrl
|
||||
}
|
||||
})
|
||||
.from(schema.course)
|
||||
.innerJoin(schema.group, eq(schema.course.groupId, schema.group.id))
|
||||
.innerJoin(schema.organization, eq(schema.group.organizationId, schema.organization.id))
|
||||
.where(eq(schema.organization.siteName, siteName))
|
||||
.limit(1);
|
||||
|
||||
return result.map((row) => ({
|
||||
...row.course,
|
||||
group: {
|
||||
organization: row.organization
|
||||
}
|
||||
}));
|
||||
};
|
||||
export async function getCourseById(courseId: string) {
|
||||
try {
|
||||
return await db.select().from(schema.course).where(eq(schema.course.id, courseId)).limit(1);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get course by ID "${courseId}": ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
throw new Error(
|
||||
`Failed to get course by ID "${courseId}": ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
TOrganization,
|
||||
TOrganizationPlan
|
||||
} from '@db/types';
|
||||
import { and, eq, sql } from 'drizzle-orm';
|
||||
import { and, eq, inArray, or, sql } from 'drizzle-orm';
|
||||
|
||||
import { ROLE } from '@cio/utils/constants';
|
||||
import { db } from '@db/drizzle';
|
||||
@@ -90,6 +90,17 @@ export const createOrganizationMember = async (data: TNewOrganizationmember) =>
|
||||
return member;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates multiple organization members in a single query
|
||||
* @param data Array of organization member creation data
|
||||
* @returns Array of created members
|
||||
*/
|
||||
export const createOrganizationMembers = async (data: TNewOrganizationmember[]) => {
|
||||
const members = await db.insert(schema.organizationmember).values(data).returning();
|
||||
|
||||
return members;
|
||||
};
|
||||
|
||||
export const checkSiteNameExists = async (siteName: string): Promise<boolean> => {
|
||||
const result = await db
|
||||
.select({ id: schema.organization.id })
|
||||
@@ -102,10 +113,132 @@ export const checkSiteNameExists = async (siteName: string): Promise<boolean> =>
|
||||
return result.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an organization by siteName
|
||||
* @param siteName Organization site name
|
||||
* @returns Organization or null if not found
|
||||
*/
|
||||
export const getOrganizationBySiteName = async (siteName: string): Promise<TOrganization | null> => {
|
||||
const [organization] = await db
|
||||
.select()
|
||||
.from(schema.organization)
|
||||
.where(eq(schema.organization.siteName, siteName))
|
||||
.limit(1);
|
||||
|
||||
return organization || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an organization by ID
|
||||
* @param id Organization ID
|
||||
* @returns Organization or null if not found
|
||||
*/
|
||||
export const getOrganizationById = async (id: string): Promise<TOrganization | null> => {
|
||||
const [organization] = await db.select().from(schema.organization).where(eq(schema.organization.id, id)).limit(1);
|
||||
|
||||
return organization || null;
|
||||
};
|
||||
|
||||
export const deleteOrganizationById = async (id: string) => {
|
||||
await db.delete(schema.organization).where(eq(schema.organization.id, id));
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if an email already exists as a team member in an organization
|
||||
* @param orgId Organization ID
|
||||
* @param email Email address to check
|
||||
* @returns True if email exists, false otherwise
|
||||
*/
|
||||
export const checkEmailExistsInOrg = async (orgId: string, email: string): Promise<boolean> => {
|
||||
const result = await db
|
||||
.select({ id: schema.organizationmember.id })
|
||||
.from(schema.organizationmember)
|
||||
.where(
|
||||
and(eq(schema.organizationmember.organizationId, orgId), eq(schema.organizationmember.email, email.toLowerCase()))
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return result.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks which emails already exist as team members in an organization (bulk check)
|
||||
* Checks both organizationmember.email and profile.email (via organizationmember.profileId)
|
||||
* @param orgId Organization ID
|
||||
* @param emails Array of email addresses to check (should already be normalized)
|
||||
* @returns Array of emails that already exist in the organization
|
||||
*/
|
||||
export const checkEmailsExistInOrg = async (orgId: string, emails: string[]): Promise<string[]> => {
|
||||
if (emails.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [orgMemberEmails, profileEmails] = await Promise.all([
|
||||
db
|
||||
.select({ email: schema.organizationmember.email })
|
||||
.from(schema.organizationmember)
|
||||
.where(
|
||||
and(eq(schema.organizationmember.organizationId, orgId), inArray(schema.organizationmember.email, emails))
|
||||
),
|
||||
db
|
||||
.select({ email: schema.profile.email })
|
||||
.from(schema.organizationmember)
|
||||
.innerJoin(schema.profile, eq(schema.organizationmember.profileId, schema.profile.id))
|
||||
.where(and(eq(schema.organizationmember.organizationId, orgId), inArray(schema.profile.email, emails)))
|
||||
]);
|
||||
|
||||
const existingEmails = new Set<string>();
|
||||
orgMemberEmails.forEach((r) => {
|
||||
if (r.email) {
|
||||
existingEmails.add(r.email.toLowerCase());
|
||||
}
|
||||
});
|
||||
profileEmails.forEach((r) => {
|
||||
if (r.email) {
|
||||
existingEmails.add(r.email.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(existingEmails);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an organization member by ID
|
||||
* @param orgId Organization ID
|
||||
* @param memberId Member ID to delete
|
||||
* @returns Deleted member or null if not found
|
||||
*/
|
||||
export const deleteOrganizationMember = async (orgId: string, memberId: number) => {
|
||||
const [deleted] = await db
|
||||
.delete(schema.organizationmember)
|
||||
.where(and(eq(schema.organizationmember.organizationId, orgId), eq(schema.organizationmember.id, memberId)))
|
||||
.returning();
|
||||
|
||||
return deleted || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a user is an admin of an organization
|
||||
* @param orgId Organization ID
|
||||
* @param profileId Profile ID to check
|
||||
* @returns True if user is admin, false otherwise
|
||||
*/
|
||||
export const isUserOrgAdmin = async (orgId: string, profileId: string): Promise<boolean> => {
|
||||
const result = await db
|
||||
.select({ roleId: schema.organizationmember.roleId })
|
||||
.from(schema.organizationmember)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.organizationmember.organizationId, orgId),
|
||||
eq(schema.organizationmember.profileId, profileId),
|
||||
eq(schema.organizationmember.roleId, ROLE.ADMIN)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return result.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets organization team members (non-students)
|
||||
* @param orgId Organization ID
|
||||
@@ -167,8 +300,8 @@ export const getOrganizationAudience = async (orgId: string) => {
|
||||
id: profile.id,
|
||||
name: profile.fullname,
|
||||
email: profile.email || '',
|
||||
avatar_url: profile.avatarUrl || '',
|
||||
date_joined: profile.createdAt ? new Date(profile.createdAt).toDateString() : ''
|
||||
avatarUrl: profile.avatarUrl || '',
|
||||
createdAt: profile.createdAt ? new Date(profile.createdAt).toDateString() : ''
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -265,7 +398,7 @@ export const getOrganizations = async (filters?: {
|
||||
* @param data Organization plan creation data
|
||||
* @returns Created organization plan record
|
||||
*/
|
||||
export const createOrganizationPlan = async (data: TNewOrganizationPlan) => {
|
||||
export const createOrganizationPlan = async (data: TNewOrganizationPlan): Promise<TOrganizationPlan> => {
|
||||
const [plan] = await db.insert(schema.organizationPlan).values(data).returning();
|
||||
return plan;
|
||||
};
|
||||
@@ -276,7 +409,10 @@ export const createOrganizationPlan = async (data: TNewOrganizationPlan) => {
|
||||
* @param payload Payload data to update
|
||||
* @returns Updated organization plan record
|
||||
*/
|
||||
export const updateOrganizationPlan = async (subscriptionId: string, payload: TOrganizationPlan['payload']) => {
|
||||
export const updateOrganizationPlan = async (
|
||||
subscriptionId: string,
|
||||
payload: TOrganizationPlan['payload']
|
||||
): Promise<TOrganizationPlan> => {
|
||||
const [plan] = await db
|
||||
.update(schema.organizationPlan)
|
||||
.set({ payload, updatedAt: sql`timezone('utc'::text, now())` })
|
||||
@@ -291,7 +427,10 @@ export const updateOrganizationPlan = async (subscriptionId: string, payload: TO
|
||||
* @param payload Payload data to update
|
||||
* @returns Updated organization plan record
|
||||
*/
|
||||
export const cancelOrganizationPlan = async (subscriptionId: string, payload: TOrganizationPlan['payload']) => {
|
||||
export const cancelOrganizationPlan = async (
|
||||
subscriptionId: string,
|
||||
payload: TOrganizationPlan['payload']
|
||||
): Promise<TOrganizationPlan> => {
|
||||
const [plan] = await db
|
||||
.update(schema.organizationPlan)
|
||||
.set({
|
||||
@@ -305,3 +444,19 @@ export const cancelOrganizationPlan = async (subscriptionId: string, payload: TO
|
||||
|
||||
return plan;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates an organization
|
||||
* @param id Organization ID
|
||||
* @param data Partial organization data to update
|
||||
* @returns Updated organization record
|
||||
*/
|
||||
export const updateOrganization = async (id: string, data: Partial<TOrganization>): Promise<TOrganization> => {
|
||||
const [organization] = await db
|
||||
.update(schema.organization)
|
||||
.set(data)
|
||||
.where(eq(schema.organization.id, id))
|
||||
.returning();
|
||||
|
||||
return organization;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TOrganization, TOrganizationPlan, TOrganizationmember } from '@db/types';
|
||||
import { TOrganization, TOrganizationPlan } from '@db/types';
|
||||
|
||||
export type OrganizationPlan = Pick<TOrganizationPlan, 'planName' | 'isActive' | 'provider' | 'subscriptionId'> & {
|
||||
customerId: string | null;
|
||||
|
||||
Reference in New Issue
Block a user