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:
@@ -1,16 +1,6 @@
|
||||
import type { TCertificateDownload } from '@api/types/course/lesson';
|
||||
import type { TCertificateDownload } from '@cio/utils/validation/course';
|
||||
import { getCloudflarePdfBuffer } from '@api/utils/cloudflare';
|
||||
|
||||
export interface CertificateData {
|
||||
theme: string;
|
||||
studentName: string;
|
||||
courseName: string;
|
||||
courseDescription: string;
|
||||
orgLogoUrl: string;
|
||||
orgName: string;
|
||||
facilitator?: string;
|
||||
}
|
||||
|
||||
export interface CertificateGenerationResult {
|
||||
success: boolean;
|
||||
buffer?: Buffer;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TCourseDownloadContent } from '@api/types/course';
|
||||
import type { TCourseDownloadContent } from '@cio/utils/validation/course';
|
||||
import { getCloudflarePdfBuffer } from '@api/utils/cloudflare';
|
||||
import { marked } from 'marked';
|
||||
|
||||
@@ -138,12 +138,7 @@ function getLessonBody(
|
||||
`;
|
||||
}
|
||||
|
||||
function getCourseBody({
|
||||
courseTitle,
|
||||
orgName,
|
||||
orgTheme,
|
||||
lessons
|
||||
}: TCourseDownloadContent): string {
|
||||
function getCourseBody({ courseTitle, orgName, orgTheme, lessons }: TCourseDownloadContent): string {
|
||||
return `
|
||||
<header class="flex justify-center items-center flex-col border-[${orgTheme ? orgTheme : '#0030FF'}] border-[40px] mx-3 h-[100vh] m-0">
|
||||
<h1 class="font-bold text-8xl text-black m-0 mx-96">${courseTitle}</h1>
|
||||
@@ -155,11 +150,7 @@ function getCourseBody({
|
||||
`;
|
||||
}
|
||||
|
||||
async function generateSinglePdfFromHtml(
|
||||
htmlContent: string,
|
||||
courseTitle: string,
|
||||
orgTheme: string
|
||||
): Promise<Buffer> {
|
||||
async function generateSinglePdfFromHtml(htmlContent: string, courseTitle: string, orgTheme: string): Promise<Buffer> {
|
||||
return await getCloudflarePdfBuffer(htmlContent);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { SendMailClient } from 'zeptomail';
|
||||
import type { Transporter } from 'nodemailer';
|
||||
import { env } from '@api/config/env';
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
export const zohoClient = new SendMailClient({
|
||||
url: 'api.zeptomail.eu/',
|
||||
token: env.ZOHO_TOKEN!
|
||||
});
|
||||
|
||||
export const nodemailerTransporter = async (): Promise<Transporter | null> => {
|
||||
if (!env.SMTP_HOST || !env.SMTP_PORT || !env.SMTP_USER || !env.SMTP_PASSWORD) {
|
||||
console.error('SMTP configuration missing');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: env.SMTP_HOST,
|
||||
port: parseInt(env.SMTP_PORT || '465', 10),
|
||||
secure: true,
|
||||
auth: {
|
||||
user: env.SMTP_USER,
|
||||
pass: env.SMTP_PASSWORD
|
||||
}
|
||||
});
|
||||
|
||||
await transporter.verify();
|
||||
|
||||
return transporter;
|
||||
} catch (error) {
|
||||
console.error('Transporter error:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
||||
import type { Context } from 'hono';
|
||||
|
||||
/**
|
||||
* Standard application error with code for easy identification
|
||||
*/
|
||||
export class AppError extends Error {
|
||||
constructor(
|
||||
error: string | Error,
|
||||
public code: string,
|
||||
public statusCode: ContentfulStatusCode = 500,
|
||||
public field?: string
|
||||
) {
|
||||
super(error instanceof Error ? error.message : error);
|
||||
this.name = 'AppError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard error response format
|
||||
*/
|
||||
export interface ErrorResponse {
|
||||
success: false;
|
||||
error: string;
|
||||
code: string;
|
||||
field?: string;
|
||||
}
|
||||
|
||||
// Error codes constants for consistency
|
||||
export const ErrorCodes = {
|
||||
// Organization errors
|
||||
SITENAME_EXISTS: 'SITENAME_EXISTS',
|
||||
ORG_CREATE_FAILED: 'ORG_CREATE_FAILED',
|
||||
ORG_NOT_FOUND: 'ORG_NOT_FOUND',
|
||||
ORGANIZATION_NOT_FOUND: 'ORGANIZATION_NOT_FOUND',
|
||||
ORG_TEAM_FETCH_FAILED: 'ORG_TEAM_FETCH_FAILED',
|
||||
ORG_AUDIENCE_FETCH_FAILED: 'ORG_AUDIENCE_FETCH_FAILED',
|
||||
COURSES_FETCH_FAILED: 'COURSES_FETCH_FAILED',
|
||||
ORG_PLAN_CREATE_FAILED: 'ORG_PLAN_CREATE_FAILED',
|
||||
ORG_PLAN_UPDATE_FAILED: 'ORG_PLAN_UPDATE_FAILED',
|
||||
ORG_PLAN_CANCEL_FAILED: 'ORG_PLAN_CANCEL_FAILED',
|
||||
ORG_PLAN_NOT_FOUND: 'ORG_PLAN_NOT_FOUND',
|
||||
|
||||
// Profile errors
|
||||
PROFILE_UPDATE_FAILED: 'PROFILE_UPDATE_FAILED',
|
||||
PROFILE_NOT_FOUND: 'PROFILE_NOT_FOUND',
|
||||
|
||||
// Account errors
|
||||
ACCOUNT_NOT_FOUND: 'ACCOUNT_NOT_FOUND',
|
||||
|
||||
// Auth errors
|
||||
UNAUTHORIZED: 'UNAUTHORIZED',
|
||||
FORBIDDEN: 'FORBIDDEN',
|
||||
|
||||
// Generic
|
||||
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
||||
INTERNAL_ERROR: 'INTERNAL_ERROR'
|
||||
} as const;
|
||||
|
||||
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
|
||||
|
||||
/**
|
||||
* Handles errors consistently across all routes
|
||||
* @param c - Hono context
|
||||
* @param error - The error to handle
|
||||
* @param fallbackMessage - Default error message if error is not an AppError
|
||||
* @param fallbackCode - Default error code if error is not an AppError
|
||||
* @returns JSON error response with appropriate status code
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* try {
|
||||
* const data = await someService();
|
||||
* return c.json({ success: true, data });
|
||||
* } catch (error) {
|
||||
* return handleError(c, error, 'Failed to process request');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const handleError = (
|
||||
c: Context,
|
||||
error: unknown,
|
||||
fallbackMessage: string = 'An unexpected error occurred',
|
||||
fallbackCode: string = ErrorCodes.INTERNAL_ERROR
|
||||
) => {
|
||||
console.error('Error in route:', error);
|
||||
|
||||
if (error instanceof AppError) {
|
||||
return c.json<ErrorResponse>(
|
||||
{
|
||||
success: false,
|
||||
error: error.message,
|
||||
code: error.code,
|
||||
field: error.field
|
||||
},
|
||||
error.statusCode
|
||||
);
|
||||
}
|
||||
|
||||
return c.json<ErrorResponse>(
|
||||
{
|
||||
success: false,
|
||||
error: fallbackMessage,
|
||||
code: fallbackCode
|
||||
},
|
||||
500
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import { Hono as HonoBase } from 'hono';
|
||||
import { AuthSession } from '@api/types/auth';
|
||||
|
||||
export const Hono = HonoBase<AuthSession>;
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TLessonDownloadContent } from '@api/types/course/lesson';
|
||||
import type { TLessonDownloadContent } from '@cio/utils/validation/course';
|
||||
import { marked } from 'marked';
|
||||
|
||||
function getHtmlTemplate(body: string): string {
|
||||
@@ -74,15 +74,7 @@ function getHtmlTemplate(body: string): string {
|
||||
`;
|
||||
}
|
||||
|
||||
function getLessonBody({
|
||||
title,
|
||||
number,
|
||||
orgName,
|
||||
note,
|
||||
slideUrl,
|
||||
video,
|
||||
courseTitle
|
||||
}: TLessonDownloadContent): string {
|
||||
function getLessonBody({ title, number, orgName, note, slideUrl, video, courseTitle }: TLessonDownloadContent): string {
|
||||
const noteHtml = marked.parse(note);
|
||||
const showExtraResources = slideUrl || (video && video.length > 0);
|
||||
return `
|
||||
@@ -146,10 +138,7 @@ function getLessonBody({
|
||||
`;
|
||||
}
|
||||
|
||||
async function generateSinglePdfFromHtml(
|
||||
htmlContent: string,
|
||||
courseTitle: string
|
||||
): Promise<Buffer> {
|
||||
async function generateSinglePdfFromHtml(htmlContent: string, courseTitle: string): Promise<Buffer> {
|
||||
throw 'Download disabled';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,39 @@
|
||||
import { SendMailClient } from 'zeptomail';
|
||||
import type { Transporter } from 'nodemailer';
|
||||
import { env } from '@api/config/env';
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
export const zohoClient = new SendMailClient({
|
||||
url: 'api.zeptomail.eu/',
|
||||
token: env.ZOHO_TOKEN!
|
||||
});
|
||||
|
||||
export const nodemailerTransporter = async (): Promise<Transporter | null> => {
|
||||
if (!env.SMTP_HOST || !env.SMTP_PORT || !env.SMTP_USER || !env.SMTP_PASSWORD) {
|
||||
console.error('SMTP configuration missing');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: env.SMTP_HOST,
|
||||
port: parseInt(env.SMTP_PORT || '465', 10),
|
||||
secure: true,
|
||||
auth: {
|
||||
user: env.SMTP_USER,
|
||||
pass: env.SMTP_PASSWORD
|
||||
}
|
||||
});
|
||||
|
||||
await transporter.verify();
|
||||
|
||||
return transporter;
|
||||
} catch (error) {
|
||||
console.error('Transporter error:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
interface FromData {
|
||||
name: string;
|
||||
email: string;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Hono } from 'hono';
|
||||
import { Scalar } from '@scalar/hono-api-reference';
|
||||
import { env } from '@api/config/env';
|
||||
import { AuthSession } from '@api/types/auth';
|
||||
|
||||
export function configureOpenAPI(app: Hono) {
|
||||
export function configureOpenAPI(app: Hono<AuthSession>) {
|
||||
if (env.OPENAPI_URL) {
|
||||
app.get('/docs', Scalar({ url: env.OPENAPI_URL, theme: 'none' }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user