feat: migrate auth from supabase to better auth (#596)

* feat: add README for @cio/db package and remove existingAccounts.json

* load session, load profile and organization, add create profile hook

* chore: cache session request

The trick was passing all the cookies to the backend. Previously I was only passing the better-auth.session_token and not better-auth.session_data. Read about the difference here https://www.better-auth.com/docs/concepts/cookies#custom-cookies

* feat: login, logout, onboarding, verify email, reset password.

* feat: complete change email and email verification

* feat: standardise how we make rpc calls and create org endpoints

* fix ui errors

* fix: handle potential undefined customerId and standardize avatar URL property

* refactor: standardize custom domain handling and update telegram chat ID references

* chore: add tailwind prefix to @cio/ui

this fixed the media query issues because  @cio/ui classes were overriding this dashboard tailwind build

* chore: clean up org sidebar

* chore: persist mode state

* feat: implement order breadcrumbs

* chore: upgrade to better auth 1.4

* chore: address common auth flow bugs and setup app for others to setup locally.
This commit is contained in:
Rotimi Best
2025-11-24 01:00:50 +01:00
committed by GitHub
parent 24741644d6
commit a3924100dd
567 changed files with 22364 additions and 13437 deletions
@@ -0,0 +1,2 @@
export * from './org.svelte';
export * from './org-plan.svelte';
@@ -0,0 +1,46 @@
import { BaseApi, classroomio } from '$lib/utils/services/api';
import type { TNewOrganizationPlan } from '@cio/db/types';
/**
* API class for organization plan operations
*/
class OrgPlanApi extends BaseApi {
/**
* Creates a new organization plan
* @param params Organization plan creation parameters
* @returns Created organization plan
*/
async createOrgPlan(params: TNewOrganizationPlan) {
return this.execute({
requestFn: () => classroomio.organization.plan.$post({ json: params }),
logContext: 'creating organization plan'
});
}
/**
* Updates an organization plan
* @param params Update parameters (subscriptionId, payload)
* @returns Updated organization plan
*/
async updateOrgPlan(params: { subscriptionId: string; payload: Record<string, unknown> }) {
return this.execute({
requestFn: () => classroomio.organization.plan.$put({ json: params }),
logContext: 'updating organization plan'
});
}
/**
* Cancels an organization plan
* @param params Cancel parameters (subscriptionId, payload)
* @returns Updated organization plan
*/
async cancelOrgPlan(params: { subscriptionId: string; payload: Record<string, unknown> }) {
return this.execute({
requestFn: () => classroomio.organization.plan.cancel.$post({ json: params }),
logContext: 'canceling organization plan'
});
}
}
export const orgPlanApi = new OrgPlanApi();
@@ -0,0 +1,108 @@
import { BaseApi, classroomio } from '$lib/utils/services/api';
import type {
CoursesByOrganizationSiteName,
GetAudienceResponse,
GetCoursesBySiteNameResponse,
OrganizationAudience,
OrganizationTeamMembers
} from '../utils/types';
import type { GetTeamResponse } from '../utils/types';
import type { TGetOrganizations } from '@cio/utils/validation/organization';
import { orgs } from '$lib/utils/store/org';
/**
* API class for organization operations
*/
class OrgApi extends BaseApi {
teamMembers = $state<OrganizationTeamMembers>([]);
audience = $state<OrganizationAudience>([]);
orgSiteCourses = $state<CoursesByOrganizationSiteName>([]);
/**
* Gets organization team members (non-students)
* @param orgId Organization ID
* @returns Team members array
*/
async getOrgTeam(orgId: string) {
return this.execute<GetTeamResponse>({
requestFn: () =>
classroomio.organization[':orgId'].team.$get({
param: { orgId }
}),
logContext: 'fetching organization team',
onSuccess: (response) => {
this.teamMembers = response.data;
}
});
}
/**
* Gets organization audience (students)
* @param orgId Organization ID
* @returns Audience array
*/
async getOrgAudience(orgId: string) {
return this.execute<GetAudienceResponse>({
requestFn: () =>
classroomio.organization[':orgId'].audience.$get({
param: { orgId }
}),
logContext: 'fetching organization audience',
onSuccess: (response) => {
this.audience = response.data;
}
});
}
/**
* Gets courses by organization siteName
* @param siteName Organization site name
* @returns Courses array
*/
async getCourseBySiteName(siteName: string) {
return this.execute<GetCoursesBySiteNameResponse>({
requestFn: () =>
classroomio.organization.courses.$get({
query: { siteName }
}),
logContext: 'fetching courses',
onSuccess: (response) => {
this.orgSiteCourses = response.data;
}
});
}
/**
* Gets current organization by siteName or custom domain
* @param siteName Organization site name or custom domain
* @param isCustomDomain Whether the siteName is a custom domain
* @returns Organization data or null if not found
*/
async getOrgBySiteName(siteName: string, isCustomDomain = false) {
const query: TGetOrganizations = { siteName };
if (isCustomDomain) {
query.customDomain = siteName;
query.isCustomDomainVerified = true;
}
const result = await this.execute<typeof classroomio.organization.$get>({
requestFn: () =>
classroomio.organization.$get({
query
}),
logContext: 'fetching organization',
onSuccess: (response) => {
if (!response.data || !Array.isArray(response.data) || response.data.length === 0) {
return;
}
orgs.set(response.data);
}
});
return result;
}
}
export const orgApi = new OrgApi();
@@ -0,0 +1,25 @@
import { classroomio, type InferResponseType } from '$lib/utils/services/api';
// Team member types
export type GetTeamResponse = (typeof classroomio.organization)[':orgId']['team']['$get'];
export type OrganizationTeamResponse = InferResponseType<GetTeamResponse> | null;
export type OrganizationTeamSuccess = Extract<InferResponseType<GetTeamResponse>, { success: true }>;
export type OrganizationTeamMembers = OrganizationTeamSuccess['data'];
// Audience types
export type GetAudienceResponse = (typeof classroomio.organization)[':orgId']['audience']['$get'];
export type OrganizationAudienceResponse = InferResponseType<GetAudienceResponse> | null;
export type OrganizationAudienceSuccess = Extract<InferResponseType<GetAudienceResponse>, { success: true }>;
export type OrganizationAudience = OrganizationAudienceSuccess['data'];
// Org Public Courses types
export type GetCoursesBySiteNameResponse = typeof classroomio.organization.courses.$get;
export type CoursesByOrganizationSiteNameResponse = InferResponseType<GetCoursesBySiteNameResponse> | null;
export type CoursesByOrganizationSiteNameSuccess = Extract<
InferResponseType<GetCoursesBySiteNameResponse>,
{ success: true }
>;
export type CoursesByOrganizationSiteName = CoursesByOrganizationSiteNameSuccess['data'];