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:
@@ -0,0 +1,49 @@
|
||||
import { getAccountData, updateUser } from '@api/services/account';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { ZUpdateProfile } from '@cio/utils/validation/account';
|
||||
import { authMiddleware } from '@api/middlewares/auth';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const accountRouter = new Hono()
|
||||
.get('/', authMiddleware, async (c) => {
|
||||
const user = c.get('user');
|
||||
|
||||
try {
|
||||
console.time('accountRouter');
|
||||
const accountData = await getAccountData(user.id);
|
||||
console.timeEnd('accountRouter');
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
user,
|
||||
profile: accountData.profile,
|
||||
organizations: accountData.organizations
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to fetch account data');
|
||||
}
|
||||
})
|
||||
.put('/user', authMiddleware, zValidator('json', ZUpdateProfile), async (c) => {
|
||||
const user = c.get('user');
|
||||
|
||||
try {
|
||||
const validatedData = c.req.valid('json');
|
||||
|
||||
const updatedProfile = await updateUser(user.id, validatedData);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
profile: updatedProfile
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to update profile');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './account';
|
||||
@@ -1,42 +1,32 @@
|
||||
import { Hono } from 'hono';
|
||||
import { ZCourseClone, TCourseClone } from '@api/types/course';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { cloneCourse } from '@api/services/course/clone';
|
||||
import type { CloneCourseErrorResponse, CloneCourseResponse } from '@api/types/database';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { ZCourseClone } from '@cio/utils/validation/course';
|
||||
import { authMiddleware } from '@api/middlewares/auth';
|
||||
import type { CloneCourseResponse, CloneCourseErrorResponse } from '@api/types/database';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
import { cloneCourse } from '@api/services/course/clone';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
type Variables = {
|
||||
user: User;
|
||||
};
|
||||
export const cloneRouter = new Hono().post('/', authMiddleware, zValidator('json', ZCourseClone), async (c) => {
|
||||
try {
|
||||
const validatedData = c.req.valid('json');
|
||||
const { id, title, description, slug, organizationId } = validatedData;
|
||||
|
||||
export const cloneRouter = new Hono<{ Variables: Variables }>().post(
|
||||
'/',
|
||||
authMiddleware,
|
||||
zValidator('json', ZCourseClone),
|
||||
async (c) => {
|
||||
try {
|
||||
const validatedData = (c.req.valid as (key: 'json') => TCourseClone)('json');
|
||||
const { id, title, description, slug, organizationId } = validatedData;
|
||||
const user = c.get('user');
|
||||
|
||||
// Get user from context (set by authMiddleware)
|
||||
const user = c.get('user');
|
||||
// Clone the course
|
||||
const newCourse = await cloneCourse(id, title, user.id, description, slug, organizationId);
|
||||
|
||||
// Clone the course
|
||||
const newCourse = await cloneCourse(id, title, user.id, description, slug, organizationId);
|
||||
|
||||
const successResponse: CloneCourseResponse = {
|
||||
success: true,
|
||||
course: newCourse
|
||||
};
|
||||
return c.json(successResponse, 200);
|
||||
} catch (error) {
|
||||
console.error('Error cloning course:', error);
|
||||
const errorResponse: CloneCourseErrorResponse = {
|
||||
error: 'Failed to clone course',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
return c.json(errorResponse, 500);
|
||||
}
|
||||
const successResponse: CloneCourseResponse = {
|
||||
success: true,
|
||||
course: newCourse
|
||||
};
|
||||
return c.json(successResponse, 200);
|
||||
} catch (error) {
|
||||
console.error('Error cloning course:', error);
|
||||
const errorResponse: CloneCourseErrorResponse = {
|
||||
error: 'Failed to clone course',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
return c.json(errorResponse, 500);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import { Hono } from 'hono';
|
||||
import { TCourseClone,TCourseDownloadContent, TCertificateDownload, ZCertificateDownload, ZCourseDownloadContent } from '@api/types/course';
|
||||
import { ZCertificateDownload, ZCourseDownloadContent } from '@cio/utils/validation/course';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { cloneRouter } from '@api/routes/course/clone';
|
||||
import { generateCertificate } from '@api/utils/certificate';
|
||||
import { generateCoursePdf } from '@api/utils/course';
|
||||
import { katexRouter } from '@api/routes/course/katex';
|
||||
import { lessonRouter } from '@api/routes/course/lesson';
|
||||
import { presignRouter } from '@api/routes/course/presign';
|
||||
import { cloneRouter } from '@api/routes/course/clone';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const courseRouter = new Hono()
|
||||
.post('/download/certificate', zValidator('json', ZCertificateDownload), async (c) => {
|
||||
const validatedData = (c.req.valid as (key: 'json') => TCertificateDownload)('json');
|
||||
const validatedData = c.req.valid('json');
|
||||
|
||||
const result = await generateCertificate(validatedData);
|
||||
|
||||
c.header('Content-Type', 'application/pdf');
|
||||
c.header(
|
||||
'Content-Disposition',
|
||||
`attachment; filename="certificate-${validatedData.courseName}.pdf"`
|
||||
);
|
||||
c.header('Content-Disposition', `attachment; filename="certificate-${validatedData.courseName}.pdf"`);
|
||||
|
||||
return c.body(
|
||||
new ReadableStream({
|
||||
@@ -31,7 +28,7 @@ export const courseRouter = new Hono()
|
||||
);
|
||||
})
|
||||
.post('/download/content', zValidator('json', ZCourseDownloadContent), async (c) => {
|
||||
const validatedData = (c.req.valid as (key: 'json') => TCourseDownloadContent)('json');
|
||||
const validatedData = c.req.valid('json');
|
||||
|
||||
const pdfBuffer = await generateCoursePdf(validatedData);
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './course';
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Context, Hono } from 'hono';
|
||||
import { Hono } from '@api/utils/hono';
|
||||
|
||||
import katex from 'katex';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const katexRouter = new Hono().get('/', async (c: Context) => {
|
||||
export const katexRouter = new Hono().get('/', async (c) => {
|
||||
try {
|
||||
const original = c.req.raw.url;
|
||||
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import { Hono } from 'hono';
|
||||
import { TLessonDownloadContent, ZLessonDownloadContent } from '@api/types/course/lesson';
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { ZLessonDownloadContent } from '@cio/utils/validation/course';
|
||||
import { generateLessonPdf } from '@api/utils/lesson';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const lessonRouter = new Hono().post(
|
||||
'/download/pdf',
|
||||
zValidator('json', ZLessonDownloadContent),
|
||||
async (c) => {
|
||||
const validatedData = (c.req.valid as (key: 'json') => TLessonDownloadContent)('json');
|
||||
export const lessonRouter = new Hono().post('/download/pdf', zValidator('json', ZLessonDownloadContent), async (c) => {
|
||||
const validatedData = c.req.valid('json');
|
||||
|
||||
const pdfBuffer = await generateLessonPdf(validatedData);
|
||||
const pdfBuffer = await generateLessonPdf(validatedData);
|
||||
|
||||
c.header('Content-Type', 'application/pdf');
|
||||
return c.body(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(pdfBuffer);
|
||||
controller.close();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
c.header('Content-Type', 'application/pdf');
|
||||
return c.body(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(pdfBuffer);
|
||||
controller.close();
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,13 +3,13 @@ import {
|
||||
ZCourseDocumentPresignUrlUpload,
|
||||
ZCourseDownloadPresignedUrl,
|
||||
ZCoursePresignUrlUpload
|
||||
} from '@api/types/course';
|
||||
} from '@cio/utils/validation/course';
|
||||
import { describeRoute, validator } from 'hono-openapi';
|
||||
|
||||
import { BUCKET_NAME } from '@api/constants/upload';
|
||||
import { CLOUDFLARE } from '@api/constants';
|
||||
import type { GetSignedUrlParameters } from '@api/utils/s3';
|
||||
import { Hono } from 'hono';
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { authMiddleware } from '@api/middlewares/auth';
|
||||
import { generateFileKey } from '@api/utils/upload';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { sendWithNodemailer, sendWithZoho } from '@api/services/mail';
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import { ZSendEmailValidation } from '@api/types/mail';
|
||||
import { env } from '@api/config/env';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const mailRouter = new Hono().post(
|
||||
'/send',
|
||||
zValidator('json', ZSendEmailValidation),
|
||||
async (c) => {
|
||||
const validatedData = c.req.valid('json');
|
||||
|
||||
const results = await Promise.all(
|
||||
validatedData.map(async (emailData) => {
|
||||
try {
|
||||
const res = env.ZOHO_TOKEN
|
||||
? await sendWithZoho(emailData)
|
||||
: await sendWithNodemailer(emailData);
|
||||
|
||||
console.log('Email status:', res);
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('Error sending email:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
details: error
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const hasErrors = results.some((result) => !result.success);
|
||||
if (hasErrors) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Some emails failed to send',
|
||||
details: results
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
return c.json({ success: true, details: results });
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export * from './mail';
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { ZDeliverEmail } from '@cio/utils/validation/mail';
|
||||
import { deliverEmail } from '@cio/email';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const mailRouter = new Hono().post('/send', zValidator('json', ZDeliverEmail), async (c) => {
|
||||
const validatedData = c.req.valid('json');
|
||||
|
||||
try {
|
||||
// Use service to send emails
|
||||
const results = await deliverEmail(validatedData);
|
||||
|
||||
const hasErrors = results.some((result) => !result.success);
|
||||
if (hasErrors) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Some emails failed to send',
|
||||
code: 'EMAIL_SEND_PARTIAL_FAILURE',
|
||||
details: results
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
return c.json({ success: true, data: results }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to process email send request', 'EMAIL_SEND_FAILED');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './onboarding';
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ZOnboardingStep1, ZOnboardingStep2 } from '@cio/utils/validation/onboarding';
|
||||
import { completeOnboarding, createOrganizationWithOwner, updateUserOnboarding } from '@api/services/onboarding';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { authMiddleware } from '@api/middlewares/auth';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const onboardingRouter = new Hono()
|
||||
.post('/step1', authMiddleware, zValidator('json', ZOnboardingStep1), async (c) => {
|
||||
try {
|
||||
const user = c.get('user');
|
||||
const { fullname, orgName, siteName } = c.req.valid('json');
|
||||
|
||||
const result = await createOrganizationWithOwner(user.id, { fullname, orgName, siteName });
|
||||
|
||||
return c.json({ success: true, data: result }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to create organization');
|
||||
}
|
||||
})
|
||||
.post('/step2', authMiddleware, zValidator('json', ZOnboardingStep2), async (c) => {
|
||||
try {
|
||||
const user = c.get('user');
|
||||
const { goal, source, fullname } = c.req.valid('json');
|
||||
|
||||
const result = await updateUserOnboarding(user.id, { goal, source, fullname });
|
||||
|
||||
return c.json({ success: true, data: result }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to update onboarding data');
|
||||
}
|
||||
})
|
||||
.post('/complete', authMiddleware, async (c) => {
|
||||
try {
|
||||
const user = c.get('user');
|
||||
|
||||
await completeOnboarding(user);
|
||||
|
||||
return c.json({ success: true }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to complete onboarding');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './organization';
|
||||
@@ -0,0 +1,172 @@
|
||||
import {
|
||||
ZCancelOrgPlan,
|
||||
ZCreateOrgPlan,
|
||||
ZGetCoursesBySiteName,
|
||||
ZGetOrganizationAudience,
|
||||
ZGetOrganizationTeam,
|
||||
ZGetOrganizations,
|
||||
ZUpdateOrgPlan
|
||||
} from '@cio/utils/validation/organization';
|
||||
import {
|
||||
cancelOrgPlan,
|
||||
createOrgPlan,
|
||||
getCoursesByOrgSiteName,
|
||||
getOrgAudience,
|
||||
getOrgTeam,
|
||||
getOrganizationsWithFilters,
|
||||
updateOrgPlan
|
||||
} from '@api/services/organization';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { authMiddleware } from '@api/middlewares/auth';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const organizationRouter = new Hono()
|
||||
/**
|
||||
* GET /organization
|
||||
* Gets organizations with optional filters
|
||||
* Query params: siteName (string, optional), customDomain (string, optional), isCustomDomainVerified (boolean, optional)
|
||||
*/
|
||||
.get('/', zValidator('query', ZGetOrganizations), async (c) => {
|
||||
try {
|
||||
const filters = c.req.valid('query');
|
||||
const organizations = await getOrganizationsWithFilters(filters);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: organizations
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to fetch organizations');
|
||||
}
|
||||
})
|
||||
/**
|
||||
* GET /organization/:orgId/team
|
||||
* Gets organization team members (non-students)
|
||||
* Requires authentication
|
||||
*/
|
||||
.get('/:orgId/team', authMiddleware, zValidator('param', ZGetOrganizationTeam), async (c) => {
|
||||
try {
|
||||
const { orgId } = c.req.valid('param');
|
||||
const team = await getOrgTeam(orgId);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: team
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to fetch organization team');
|
||||
}
|
||||
})
|
||||
/**
|
||||
* GET /organization/:orgId/audience
|
||||
* Gets organization audience (students)
|
||||
* Requires authentication
|
||||
*/
|
||||
.get('/:orgId/audience', authMiddleware, zValidator('param', ZGetOrganizationAudience), async (c) => {
|
||||
try {
|
||||
const { orgId } = c.req.valid('param');
|
||||
const audience = await getOrgAudience(orgId);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: audience
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to fetch organization audience');
|
||||
}
|
||||
})
|
||||
/**
|
||||
* GET /organization/courses
|
||||
* Gets courses by organization siteName
|
||||
* Query params: siteName (string)
|
||||
*/
|
||||
.get('/courses', zValidator('query', ZGetCoursesBySiteName), async (c) => {
|
||||
try {
|
||||
const { siteName } = c.req.valid('query');
|
||||
const courses = await getCoursesByOrgSiteName(siteName);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: courses
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to fetch courses');
|
||||
}
|
||||
})
|
||||
/**
|
||||
* POST /organization/plan
|
||||
* Creates a new organization plan
|
||||
* Requires authentication
|
||||
*/
|
||||
.post('/plan', authMiddleware, zValidator('json', ZCreateOrgPlan), async (c) => {
|
||||
try {
|
||||
const data = c.req.valid('json');
|
||||
const plan = await createOrgPlan(data);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: plan
|
||||
},
|
||||
201
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to create organization plan');
|
||||
}
|
||||
})
|
||||
/**
|
||||
* PUT /organization/plan
|
||||
* Updates an organization plan by subscription ID
|
||||
* Requires authentication
|
||||
*/
|
||||
.put('/plan', authMiddleware, zValidator('json', ZUpdateOrgPlan), async (c) => {
|
||||
try {
|
||||
const { subscriptionId, payload } = c.req.valid('json');
|
||||
const plan = await updateOrgPlan(subscriptionId, payload);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: plan
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to update organization plan');
|
||||
}
|
||||
})
|
||||
/**
|
||||
* POST /organization/plan/cancel
|
||||
* Cancels an organization plan by subscription ID
|
||||
* Requires authentication
|
||||
*/
|
||||
.post('/plan/cancel', authMiddleware, zValidator('json', ZCancelOrgPlan), async (c) => {
|
||||
try {
|
||||
const { subscriptionId, payload } = c.req.valid('json');
|
||||
const plan = await cancelOrgPlan(subscriptionId, payload);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: plan
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to cancel organization plan');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user