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,28 @@
import * as z from 'zod';
import { defineEmail } from '../send';
import { getDefaultTemplate } from '../templates';
export const forgotPasswordEmail = defineEmail({
id: 'forgotPassword',
subject: 'Password reset notification - ClassroomIO',
schema: z.object({
email: z.email(),
name: z.string().min(1),
link: z.url()
}),
render: (fields) => {
const content = `Hello ${fields.name},
<p>You are receiving this email because you have requested a password reset for your ClassroomIO account.</p>
<p>Please click the button below to reset your password:</p>
<div>
<a class="button" href="${fields.link}">Reset my password</a>
</div>
<p>PS: If you did not initiate this request, reply to this email or write to [email protected] so we can look into a possible attempt to breach your account.</p>
`;
return getDefaultTemplate(content);
}
});
+31
View File
@@ -0,0 +1,31 @@
/**
* Auto-import all email definitions to register them in the registry
*
* When this module is imported, all emails are automatically registered
* and available for use with the send() function.
*/
// Import all email definitions (side effect: registers them)
export * from './forgot-password';
export * from './welcome';
export * from './verify-email';
export * from './on-password-reset';
// Import types for schema mapping (must be after exports to avoid circular dependency)
import type { forgotPasswordEmail } from './forgot-password';
import type { onPasswordResetEmail } from './on-password-reset';
import type { verifyEmailEmail } from './verify-email';
import type { welcomeEmail } from './welcome';
/**
* Type mapping that maps each EmailId to its corresponding Zod schema type
* This is used for compile-time type inference in sendEmail()
*
* **Important**: When adding a new email, add its schema mapping here!
*/
export type EmailSchemas = {
forgotPassword: typeof forgotPasswordEmail.template.schema;
welcome: typeof welcomeEmail.template.schema;
verifyEmail: typeof verifyEmailEmail.template.schema;
onPasswordReset: typeof onPasswordResetEmail.template.schema;
};
@@ -0,0 +1,17 @@
import * as z from 'zod';
import { defineEmail } from '../send';
import { getDefaultTemplate } from '../templates';
export const onPasswordResetEmail = defineEmail({
id: 'onPasswordReset',
subject: 'Password Reset Successful - ClassroomIO',
schema: z.object({
name: z.string().min(1)
}),
render: (fields) => {
return getDefaultTemplate(`Heya ${fields.name},
<p>Your password has been reset successfully.</p>
`);
}
});
+40
View File
@@ -0,0 +1,40 @@
import * as z from 'zod';
import { defineEmail } from '../send';
import { getDefaultTemplate } from '../templates';
export const verifyEmailEmail = defineEmail({
id: 'verifyEmail',
subject: 'Action Required: Confirm your email',
schema: z.object({
link: z.url(),
newEmail: z.email().optional(),
userName: z.string().optional()
}),
render: (fields) => {
const isEmailChange = !!fields.newEmail;
const userName = fields.userName || 'there';
const content = isEmailChange
? `
<p><strong>Hey ${userName} 👋</strong></p>
<p>You have requested to change your email address to <strong>${fields.newEmail}</strong>.</p>
<p>To approve this change, please click the button below:</p>
<div>
<a class="button" href="${fields.link}">Approve Email Change</a>
</div>
<p>If you did not request this change, please ignore this email.</p>
`
: `
<p><strong>Hey ${userName} 👋</strong></p>
<p>Welcome to ClassroomIO! In order to get your account ready for usage, we need to verify your email. </p>
<p>We do this to make sure we don't get fake user emails in our signup. To get the best out of our product, we'll need you to verify your email by clicking the <strong>Verify</strong> button below
</p>
<div>
<a class="button" href="${fields.link}">Verify</a>
</div>
`;
return getDefaultTemplate(content);
}
});
+32
View File
@@ -0,0 +1,32 @@
import * as z from 'zod';
import { defineEmail } from '../send';
import { getDefaultTemplate } from '../templates';
export const welcomeEmail = defineEmail({
id: 'welcome',
subject: 'Welcome to ClassroomIO!',
schema: z.object({
name: z.string().min(1)
}),
render: (fields) => {
const content = `
<p>Dear ${fields.name},</p>
<p>My name is Best, the founder of ClassroomIO. I saw you signed up, any questions so far?</p>
<p>
Would you like to get a walkthrough of the platform in a 30 minutes product demo? It's free and we won't try to sell you anything, just want to learn from your use case and show you all the cool features and flows we thought about.
</p>
<p>
Interested? just book a 30-minute demo slot in our calendar!
</p>
<div>
<a class="button" href="https://classroomio.com/demo">Book demo</a>
</div>
<p>
PS: I reply personally to every email. We don't have an outsourced support team at the other end of the globe.. ;-)
</p>
`;
return getDefaultTemplate(content);
}
});