mirror of
https://github.com/nicotsx/zerobyte.git
synced 2026-02-19 23:34:53 -05:00
* refactor: add nitro bootstrap plugin to ensure app is started before first call * refactor(bootstrap): avoid duplicate event firing * refactor: extract common setup in initModule function * refactor: remove proxy pattern for db and auth Since we migrated away from rr this is not needed anymore as the bundler correctly split chunks
178 lines
3.8 KiB
TypeScript
178 lines
3.8 KiB
TypeScript
import {
|
|
betterAuth,
|
|
type AuthContext,
|
|
type BetterAuthOptions,
|
|
type MiddlewareContext,
|
|
type MiddlewareOptions,
|
|
} from "better-auth";
|
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
import {
|
|
admin,
|
|
createAuthMiddleware,
|
|
twoFactor,
|
|
username,
|
|
organization,
|
|
} from "better-auth/plugins";
|
|
import { UnauthorizedError } from "http-errors-enhanced";
|
|
import { convertLegacyUserOnFirstLogin } from "./auth-middlewares/convert-legacy-user";
|
|
import { eq } from "drizzle-orm";
|
|
import { config } from "../core/config";
|
|
import { db } from "../db/db";
|
|
import { cryptoUtils } from "../utils/crypto";
|
|
import {
|
|
organization as organizationTable,
|
|
member,
|
|
usersTable,
|
|
} from "../db/schema";
|
|
import { ensureOnlyOneUser } from "./auth-middlewares/only-one-user";
|
|
import { authService } from "../modules/auth/auth.service";
|
|
import { tanstackStartCookies } from "better-auth/tanstack-start";
|
|
|
|
export type AuthMiddlewareContext = MiddlewareContext<
|
|
MiddlewareOptions,
|
|
AuthContext<BetterAuthOptions>
|
|
>;
|
|
|
|
export const auth = betterAuth({
|
|
secret: await cryptoUtils.deriveSecret("better-auth"),
|
|
baseURL: config.baseUrl,
|
|
trustedOrigins: config.trustedOrigins,
|
|
advanced: {
|
|
cookiePrefix: "zerobyte",
|
|
useSecureCookies: config.isSecure,
|
|
},
|
|
onAPIError: {
|
|
throw: true,
|
|
},
|
|
hooks: {
|
|
before: createAuthMiddleware(async (ctx) => {
|
|
await ensureOnlyOneUser(ctx);
|
|
await convertLegacyUserOnFirstLogin(ctx);
|
|
}),
|
|
},
|
|
database: drizzleAdapter(db, {
|
|
provider: "sqlite",
|
|
}),
|
|
databaseHooks: {
|
|
user: {
|
|
delete: {
|
|
before: async (user) => {
|
|
await authService.cleanupUserOrganizations(user.id);
|
|
},
|
|
},
|
|
create: {
|
|
before: async (user) => {
|
|
const anyUser = await db.query.usersTable.findFirst();
|
|
const isFirstUser = !anyUser;
|
|
|
|
if (isFirstUser) {
|
|
user.role = "admin";
|
|
}
|
|
|
|
return { data: user };
|
|
},
|
|
after: async (user) => {
|
|
const slug =
|
|
user.email.split("@")[0] +
|
|
"-" +
|
|
Math.random().toString(36).slice(-4);
|
|
|
|
const resticPassword = cryptoUtils.generateResticPassword();
|
|
const metadata = {
|
|
resticPassword:
|
|
await cryptoUtils.sealSecret(resticPassword),
|
|
};
|
|
|
|
try {
|
|
await db.transaction(async (tx) => {
|
|
const orgId = Bun.randomUUIDv7();
|
|
|
|
await tx.insert(organizationTable).values({
|
|
name: `${user.name}'s Workspace`,
|
|
slug: slug,
|
|
id: orgId,
|
|
createdAt: new Date(),
|
|
metadata,
|
|
});
|
|
|
|
await tx.insert(member).values({
|
|
id: Bun.randomUUIDv7(),
|
|
userId: user.id,
|
|
role: "owner",
|
|
organizationId: orgId,
|
|
createdAt: new Date(),
|
|
});
|
|
});
|
|
} catch {
|
|
await db
|
|
.delete(usersTable)
|
|
.where(eq(usersTable.id, user.id));
|
|
|
|
throw new Error(
|
|
`Failed to create organization for user ${user.id}`,
|
|
);
|
|
}
|
|
},
|
|
},
|
|
},
|
|
session: {
|
|
create: {
|
|
before: async (session) => {
|
|
const orgMembership = await db.query.member.findFirst({
|
|
where: { userId: session.userId },
|
|
});
|
|
|
|
if (!orgMembership) {
|
|
throw new UnauthorizedError(
|
|
"User does not belong to any organization",
|
|
);
|
|
}
|
|
|
|
return {
|
|
data: {
|
|
...session,
|
|
activeOrganizationId: orgMembership?.organizationId,
|
|
},
|
|
};
|
|
},
|
|
},
|
|
},
|
|
},
|
|
emailAndPassword: {
|
|
enabled: true,
|
|
},
|
|
user: {
|
|
modelName: "usersTable",
|
|
additionalFields: {
|
|
username: {
|
|
type: "string",
|
|
returned: true,
|
|
required: true,
|
|
},
|
|
hasDownloadedResticPassword: {
|
|
type: "boolean",
|
|
returned: true,
|
|
},
|
|
},
|
|
},
|
|
session: {
|
|
modelName: "sessionsTable",
|
|
},
|
|
plugins: [
|
|
username(),
|
|
admin({
|
|
defaultRole: "user",
|
|
}),
|
|
organization({
|
|
allowUserToCreateOrganization: false,
|
|
}),
|
|
twoFactor({
|
|
backupCodeOptions: {
|
|
storeBackupCodes: "encrypted",
|
|
amount: 5,
|
|
},
|
|
}),
|
|
tanstackStartCookies(),
|
|
],
|
|
});
|