Files
portabase/src/lib/auth/auth.ts
T

503 lines
14 KiB
TypeScript
Raw Normal View History

2025-05-18 20:46:59 +02:00
import {betterAuth} from "better-auth";
import {drizzleAdapter} from "better-auth/adapters/drizzle";
2025-07-26 14:43:59 +02:00
import * as drizzleDb from "@/db";
2025-05-18 20:46:59 +02:00
import {db} from "@/db";
import {env} from "@/env.mjs";
import {nextCookies} from "better-auth/next-js";
2025-12-21 16:55:12 +01:00
import {admin as adminPlugin, openAPI, Organization, organization, twoFactor} from "better-auth/plugins";
2025-05-18 20:46:59 +02:00
import {ac, admin, orgAdmin, orgMember, orgOwner, pending, superadmin, user} from "@/lib/auth/permissions";
import {headers} from "next/headers";
import {count, eq} from "drizzle-orm";
2025-11-01 14:50:08 +01:00
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
2025-11-22 17:20:52 +01:00
import {sendEmail} from "@/lib/email/email-helper";
import {render} from "@react-email/render";
2025-12-21 16:55:12 +01:00
import {SUPPORTED_PROVIDERS} from "../../../portabase.config";
import {withUpdatedAt} from "@/db/utils";
2025-12-23 18:25:31 +01:00
import EmailVerification from "@/components/emails/auth/email-verification";
import EmailForgotPassword from "@/components/emails/auth/email-forgot-password";
import {getDeviceDetails} from "@/utils/detection";
import EmailNewLogin from "@/components/emails/auth/email-new-login";
2025-05-16 15:54:17 +02:00
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
2025-07-16 12:36:11 +02:00
schema: drizzleDb.schemas,
2025-05-16 15:54:17 +02:00
}),
2025-12-22 20:32:18 +01:00
appName: env.PROJECT_NAME!,
2025-12-21 19:27:00 +01:00
baseURL: env.PROJECT_URL,
2025-07-17 12:43:00 +02:00
secret: env.PROJECT_SECRET,
2025-05-16 15:54:17 +02:00
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
2025-12-23 18:25:31 +01:00
sendResetPassword: async ({user, token}, request) => {
2025-12-21 16:55:12 +01:00
const [updatedUser] = await db.update(drizzleDb.schemas.user).set(withUpdatedAt({
emailVerified: true,
})).where(eq(drizzleDb.schemas.user.id, user.id)).returning();
2025-11-22 17:20:52 +01:00
await sendEmail({
to: user.email,
subject: "Reset your password",
2025-12-23 18:25:31 +01:00
html: await render(
EmailForgotPassword({
firstname: user.name!,
token,
}),
{}
),
2025-11-22 17:20:52 +01:00
});
2025-12-21 16:55:12 +01:00
2025-05-16 15:54:17 +02:00
},
2025-11-22 17:20:52 +01:00
onPasswordReset: async ({user}, request) => {
console.log(`Password for user ${user.email} has been reset.`);
},
2025-05-16 15:54:17 +02:00
},
2025-12-21 16:55:12 +01:00
emailVerification: {
async sendVerificationEmail({user, token, url}) {
await sendEmail({
to: user.email,
subject: "Portabase Email Verification",
html: await render(EmailVerification({
firstname: user.name,
url: url
})),
});
await (
await auth.$context
).internalAdapter.updateUser(user.id, {
emailVerified: false,
});
},
async afterEmailVerification(user) {
await (
await auth.$context
).internalAdapter.updateUser(user.id, {
emailVerified: true,
});
2025-05-16 15:54:17 +02:00
},
},
2025-12-21 16:55:12 +01:00
socialProviders: SUPPORTED_PROVIDERS.reduce((acc: any, provider: any) => {
if (provider.id === "credential") return acc;
if (provider.id === "google") {
acc.google = {
clientId: env.AUTH_GOOGLE_ID! as string,
clientSecret: env.AUTH_GOOGLE_SECRET! as string,
};
}
return acc;
}, {}),
account: {
accountLinking: {
enabled: true,
},
},
2025-05-16 15:54:17 +02:00
plugins: [
openAPI(),
nextCookies(),
2025-12-21 16:55:12 +01:00
twoFactor(),
2025-05-16 15:54:17 +02:00
organization({
ac,
roles: {
2025-07-26 14:43:59 +02:00
owner: orgOwner,
admin: orgAdmin,
member: orgMember,
2025-05-16 15:54:17 +02:00
},
}),
adminPlugin({
adminRoles: ["admin", "superadmin"],
2025-07-16 12:36:11 +02:00
defaultRole: "pending",
2025-05-16 15:54:17 +02:00
ac,
roles: {
admin,
user,
pending,
superadmin,
},
}),
],
advanced: {
database: {
generateId: false,
},
},
user: {
2025-07-28 16:53:21 +02:00
deleteUser: {
enabled: true,
2025-12-21 16:55:12 +01:00
},
changeEmail: {
enabled: true,
2025-07-28 16:53:21 +02:00
},
2025-05-16 15:54:17 +02:00
additionalFields: {
deletedAt: {
2025-12-09 21:23:15 +01:00
type: "number",
2025-05-16 15:54:17 +02:00
nullable: true,
required: false,
},
2025-12-22 20:32:18 +01:00
theme: {
type: "string",
},
lastConnectedAt: {
type: "date",
},
lastChangedPasswordAt: {
type: "date",
},
2025-05-16 15:54:17 +02:00
},
},
2025-07-16 12:36:11 +02:00
databaseHooks: {
user: {
create: {
async before(user, context) {
const userCount = (await db.select({count: count()}).from(drizzleDb.schemas.user))[0].count;
const role = userCount === 0 ? "superadmin" : "pending";
return {
data: {
...user,
role,
},
};
},
async after(user, context) {
2025-07-28 16:53:21 +02:00
const userCount = (await db.select({count: count()}).from(drizzleDb.schemas.user))[0].count;
const role = userCount === 0 ? "owner" : "admin";
2025-07-16 12:36:11 +02:00
2025-07-27 21:26:31 +02:00
const defaultOrgSlug = "default"; // change this if your default org has a different slug
const defaultOrg = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, defaultOrgSlug),
});
if (defaultOrg) {
2025-07-28 16:53:21 +02:00
console.log(user)
2025-07-27 21:26:31 +02:00
await db.insert(drizzleDb.schemas.member).values({
userId: user.id,
organizationId: defaultOrg.id,
2025-07-28 16:53:21 +02:00
role: role,
2025-07-16 12:36:11 +02:00
});
2025-07-27 21:26:31 +02:00
} else {
console.warn("Default organization not found. Cannot assign member.");
2025-07-16 12:36:11 +02:00
}
},
},
},
session: {
create: {
before: async (session, context) => {
const userId = session.userId;
let memberships = await db.query.member.findMany({
2025-07-16 12:36:11 +02:00
where: eq(drizzleDb.schemas.member.userId, userId),
});
return {
data: {
activeOrganizationId: memberships[0].organizationId,
2025-07-16 12:36:11 +02:00
},
};
},
2025-12-23 18:25:31 +01:00
after: async (session) => {
const user = await db.query.user.findFirst({
where: eq(drizzleDb.schemas.user.id, session.userId),
});
if (user && user.role != "pending") {
const deviceInfo = getDeviceDetails(session.userAgent);
await sendEmail({
to: user.email,
subject: "New login to your account",
html: await render(
EmailNewLogin({
firstname: user.name!,
os: deviceInfo.os,
browser: deviceInfo.browser,
ipAddress: session.ipAddress!,
}),
{}
),
});
(await auth.$context).internalAdapter.updateUser(user.id, {
lastConnectedAt: new Date(),
});
}
},
2025-07-16 12:36:11 +02:00
},
},
2025-07-16 12:36:11 +02:00
},
2025-05-16 15:54:17 +02:00
session: {
additionalFields: {
activeOrganizationId: {
type: "string",
required: false,
},
},
},
/* databaseHooks: {
session: {
create: {
before: async (session) => {
const organizationId = await getLastOrganizationOrFirst(session.userId);
if (!organizationId) {
return {
...session,
};
}
console.log("sessionId", session.id);
const [aa] = await db
.update(drizzleUser.session)
.set({ activeOrganizationId: organizationId })
.where(eq(drizzleUser.session.id, session.id))
.returning();
console.log("aaaaa", aa);
return {
...session,
activeOrganizationId: organizationId,
};
},
},
},
},*/
2025-11-04 08:48:46 +01:00
trustedOrigins: [env.PROJECT_URL!, "http://app"],
2025-05-16 15:54:17 +02:00
});
/*export const signUpUser = async (email: string, password: string, name: string) => {
const user = await auth.api.signUpEmail({
body: {
email,
password,
name,
},
});
return user;
};
export const signInUser = async (email: string, password: string) => {
const user = await auth.api.signInEmail({
body: {
email,
password,
},
});
return user;
};*/
export const createUser = async (name: string, email: string, password: string, role: "user" | "pending" | "admin" | "superadmin" = "pending") => {
2025-05-18 20:46:59 +02:00
return await auth.api.createUser({
2025-05-16 15:54:17 +02:00
headers: await headers(),
body: {
name,
email,
password,
role,
},
});
};
export const getSessions = async () => {
2025-05-18 20:46:59 +02:00
return await auth.api.listSessions({
2025-05-16 15:54:17 +02:00
headers: await headers(),
});
};
export const getSession = async () => {
2025-05-18 20:46:59 +02:00
return await auth.api.getSession({
2025-05-16 15:54:17 +02:00
headers: await headers(),
});
};
export const revokeSession = async (e: string) => {
try {
2025-07-16 12:36:11 +02:00
const {status} = await auth.api.revokeSession({
2025-05-16 15:54:17 +02:00
body: {
token: e,
},
headers: await headers(),
});
return status;
2025-07-16 12:36:11 +02:00
} catch (e) {
}
2025-05-16 15:54:17 +02:00
};
export const getAccounts = async () => {
2025-05-18 20:46:59 +02:00
return await auth.api.listUserAccounts({
2025-05-16 15:54:17 +02:00
headers: await headers(),
});
};
export const unlinkAccount = async (provider: string, account: string) => {
try {
2025-07-16 12:36:11 +02:00
const {status} = await auth.api.unlinkAccount({
2025-05-16 15:54:17 +02:00
body: {
providerId: provider,
accountId: account,
},
headers: await headers(),
});
return status;
2025-07-16 12:36:11 +02:00
} catch (e) {
}
2025-05-16 15:54:17 +02:00
};
2025-11-03 11:37:14 +01:00
2025-05-18 20:46:59 +02:00
export const getOrganization = async ({
organizationId,
organizationSlug,
}: {
organizationId?: string;
organizationSlug?: string;
2025-07-26 14:43:59 +02:00
} = {}): Promise<OrganizationWithMembersAndUsers | null> => {
2025-07-16 12:36:11 +02:00
const query =
organizationId != null
2025-09-23 20:35:48 +02:00
? {organizationId}
2025-07-16 12:36:11 +02:00
: organizationSlug != null
2025-09-23 20:35:48 +02:00
? {organizationSlug}
2025-07-16 12:36:11 +02:00
: undefined;
2025-05-16 15:54:17 +02:00
2025-05-18 20:46:59 +02:00
try {
2025-07-26 14:43:59 +02:00
const response = await auth.api.getFullOrganization({
2025-05-18 20:46:59 +02:00
headers: await headers(),
2025-09-23 20:35:48 +02:00
...(query ? {query} : {}),
2025-05-18 20:46:59 +02:00
});
2025-07-26 14:43:59 +02:00
return response as OrganizationWithMembersAndUsers;
2025-05-18 20:46:59 +02:00
} catch (e) {
console.error(e);
return null;
}
2025-05-16 15:54:17 +02:00
};
2025-07-29 08:51:50 +02:00
export const listOrganizations = async (): Promise<Organization[] | null> => {
2025-05-16 15:54:17 +02:00
try {
2025-05-18 20:46:59 +02:00
return await auth.api.listOrganizations({
2025-05-16 15:54:17 +02:00
headers: await headers(),
2025-07-29 08:51:50 +02:00
}) as Organization[];
2025-07-16 12:36:11 +02:00
} catch (e) {
2025-07-29 08:51:50 +02:00
return null;
2025-07-16 12:36:11 +02:00
}
2025-05-16 15:54:17 +02:00
};
export const getLastOrganizationOrFirst = async (userId: string) => {
try {
2025-07-16 12:36:11 +02:00
const organizations = await db.query.organization.findMany({
where: eq(drizzleDb.schemas.member.userId, userId),
2025-05-16 15:54:17 +02:00
});
if (organizations.length > 0) {
return organizations[0].id;
}
return null;
} catch (e) {
return null;
}
};
2025-07-26 14:43:59 +02:00
2025-05-16 15:54:17 +02:00
export const createOrganization = async (name: string, slug: string) => {
try {
2025-07-26 14:43:59 +02:00
return await auth.api.createOrganization({
2025-09-23 20:35:48 +02:00
headers: await headers(),
2025-05-16 15:54:17 +02:00
body: {
name,
slug,
},
});
2025-07-26 14:43:59 +02:00
} catch (e: any) {
const errorMessage = e?.response?.data?.message || e?.message || "Unknown auth error";
const status = e?.response?.status || 500;
2025-05-16 15:54:17 +02:00
2025-07-26 14:43:59 +02:00
console.error("Auth API createOrganization error:", {
message: errorMessage,
status,
raw: e,
});
throw {
name: "AuthCreateOrganizationError",
message: errorMessage,
status,
cause: e,
};
2025-05-16 15:54:17 +02:00
}
};
2025-07-26 14:43:59 +02:00
export const deleteOrganization = async (organizationId: string) => {
try {
return await auth.api.deleteOrganization({
body: {
organizationId,
},
headers: await headers(),
});
} catch (e: any) {
const errorMessage = e?.response?.data?.message || e?.message || "Unknown auth error";
const status = e?.response?.status || 500;
console.error("Auth API deleteOrganization error:", {
message: errorMessage,
status,
raw: e,
});
throw {
name: "AuthDeleteOrganizationError",
message: errorMessage,
status,
cause: e,
};
}
};
2025-05-16 15:54:17 +02:00
export const checkSlugOrganization = async (slug: string) => {
try {
2025-07-16 12:36:11 +02:00
const {status} = await auth.api.checkOrganizationSlug({
2025-05-16 15:54:17 +02:00
headers: await headers(),
body: {
slug,
},
});
return status;
} catch (e) {
console.log("err", e);
}
};
export const getActiveMember = async () => {
try {
const member = await auth.api.getActiveMember({
headers: await headers(),
});
2025-07-16 12:36:11 +02:00
console.log(member);
2025-05-16 15:54:17 +02:00
2025-11-01 14:50:08 +01:00
return member as MemberWithUser;
2025-05-16 15:54:17 +02:00
} catch (e) {
console.log("err", e);
}
};
export const setActiveOrganization = async (slug: string) => {
try {
2025-09-23 20:35:48 +02:00
return await auth.api.setActiveOrganization({
2025-05-16 15:54:17 +02:00
headers: await headers(),
body: {
organizationSlug: slug,
},
});
} catch (e) {
console.log("error", e);
}
};