2026-03-18 19:47:41 +01: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";
|
2026-03-18 19:47:41 +01:00
|
|
|
import {db} from "@/db";
|
|
|
|
|
import {env} from "@/env.mjs";
|
|
|
|
|
import {nextCookies} from "better-auth/next-js";
|
|
|
|
|
import {admin as adminPlugin, openAPI, Organization, organization, twoFactor} from "better-auth/plugins";
|
|
|
|
|
import {ac, admin, orgAdmin, orgMember, orgOwner, pending, superadmin, user} from "@/lib/auth/permissions";
|
|
|
|
|
import {headers} from "next/headers";
|
|
|
|
|
import {count, eq} from "drizzle-orm";
|
|
|
|
|
import {MemberWithUser, OrganizationWithMembersAndUsers,} from "@/db/schema/03_organization";
|
|
|
|
|
import {sendEmail} from "@/lib/email";
|
|
|
|
|
import {render} from "@react-email/render";
|
|
|
|
|
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";
|
2026-03-18 19:47:41 +01:00
|
|
|
import {getDeviceDetails} from "@/utils/detection";
|
2025-12-23 18:25:31 +01:00
|
|
|
import EmailNewLogin from "@/components/emails/auth/email-new-login";
|
2026-03-18 19:47:41 +01:00
|
|
|
import {sso} from "@better-auth/sso";
|
|
|
|
|
import {SUPPORTED_PROVIDERS} from "@/lib/auth/config";
|
|
|
|
|
import {passkey} from "@better-auth/passkey";
|
|
|
|
|
import {getOidcProviders} from "./oidc";
|
|
|
|
|
import {APIError} from "better-auth/api";
|
|
|
|
|
import {getOAuthProviders} from "./oauth";
|
2026-05-15 22:05:13 +02:00
|
|
|
import { logger } from "@/lib/logger";
|
2026-03-18 19:47:41 +01:00
|
|
|
|
2026-05-15 22:05:13 +02:00
|
|
|
const log = logger.child({ module: "lib/auth" });
|
2026-02-23 12:04:48 +01:00
|
|
|
|
|
|
|
|
const oidcProviders = getOidcProviders();
|
2025-05-16 15:54:17 +02:00
|
|
|
|
|
|
|
|
export const auth = betterAuth({
|
2026-03-18 19:47:41 +01:00
|
|
|
onAPIError: {
|
|
|
|
|
errorURL: "/error",
|
|
|
|
|
onError: (error, ctx) => {
|
2026-04-10 18:37:35 +02:00
|
|
|
//TODO: capture errors in a monitoring service
|
2026-02-23 12:04:48 +01:00
|
|
|
},
|
2026-03-18 19:47:41 +01:00
|
|
|
},
|
|
|
|
|
database: drizzleAdapter(db, {
|
|
|
|
|
provider: "pg",
|
|
|
|
|
schema: drizzleDb.schemas,
|
|
|
|
|
}),
|
|
|
|
|
appName: env.PROJECT_NAME!,
|
|
|
|
|
baseURL: env.PROJECT_URL,
|
|
|
|
|
secret: env.PROJECT_SECRET,
|
|
|
|
|
emailAndPassword: {
|
|
|
|
|
enabled: env.AUTH_EMAIL_PASSWORD_ENABLED === "true",
|
|
|
|
|
requireEmailVerification: false,
|
|
|
|
|
sendResetPassword: async ({user, token}, request) => {
|
|
|
|
|
if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") {
|
|
|
|
|
throw new APIError("FORBIDDEN", {
|
|
|
|
|
message: "Password reset is disabled.",
|
|
|
|
|
});
|
2026-02-23 12:04:48 +01:00
|
|
|
}
|
2026-03-18 19:47:41 +01:00
|
|
|
await db
|
|
|
|
|
.update(drizzleDb.schemas.user)
|
|
|
|
|
.set(
|
|
|
|
|
withUpdatedAt({
|
|
|
|
|
emailVerified: true,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.where(eq(drizzleDb.schemas.user.id, user.id))
|
|
|
|
|
.returning();
|
2026-02-23 12:04:48 +01:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
await sendEmail({
|
|
|
|
|
to: user.email,
|
|
|
|
|
subject: "Reset your password",
|
|
|
|
|
html: await render(
|
|
|
|
|
EmailForgotPassword({
|
|
|
|
|
firstname: user.name!,
|
|
|
|
|
token,
|
|
|
|
|
}),
|
|
|
|
|
{},
|
|
|
|
|
),
|
|
|
|
|
});
|
2025-05-16 15:54:17 +02:00
|
|
|
},
|
|
|
|
|
},
|
2026-03-18 19:47:41 +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,
|
|
|
|
|
});
|
|
|
|
|
},
|
2026-02-23 12:04:48 +01:00
|
|
|
},
|
2026-03-18 19:47:41 +01:00
|
|
|
socialProviders: getOAuthProviders().reduce<
|
|
|
|
|
Record<string, { clientId: string; clientSecret: string }>
|
|
|
|
|
>((acc, provider) => {
|
|
|
|
|
const configEntry = SUPPORTED_PROVIDERS.find((p) => p.id === provider.id);
|
|
|
|
|
|
|
|
|
|
if (!configEntry?.isActive) {
|
|
|
|
|
return acc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
acc[provider.id] = {
|
|
|
|
|
clientId: provider.client,
|
|
|
|
|
clientSecret: provider.secret,
|
|
|
|
|
...(provider.id === "apple" && provider.appleBundleIdentifier
|
|
|
|
|
? {appBundleIdentifier: provider.appleBundleIdentifier}
|
|
|
|
|
: {}),
|
|
|
|
|
};
|
|
|
|
|
return acc;
|
|
|
|
|
}, {}),
|
2026-02-23 12:04:48 +01:00
|
|
|
account: {
|
2026-03-18 19:47:41 +01:00
|
|
|
accountLinking: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
trustedProviders: [
|
|
|
|
|
"credential",
|
|
|
|
|
...getOAuthProviders().map((p) => p.id),
|
|
|
|
|
...oidcProviders.map((p) => p.id),
|
|
|
|
|
],
|
|
|
|
|
allowDifferentEmails: true,
|
2025-05-16 15:54:17 +02:00
|
|
|
},
|
|
|
|
|
},
|
2026-02-23 12:04:48 +01:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
plugins: [
|
|
|
|
|
sso({
|
|
|
|
|
defaultSSO: oidcProviders.map((p) => ({
|
|
|
|
|
oidcConfig: {
|
|
|
|
|
issuer: p.issuerUrl,
|
|
|
|
|
discoveryEndpoint: p.discoveryEndpoint,
|
|
|
|
|
jwksEndpoint: p.jwksEndpoint,
|
|
|
|
|
clientId: p.client,
|
|
|
|
|
clientSecret: p.secret,
|
|
|
|
|
scopes: p.scopes?.split(" ") ?? ["openid", "profile", "email"],
|
|
|
|
|
pkce: p.pkce,
|
|
|
|
|
mapping: {
|
|
|
|
|
extraFields: {
|
|
|
|
|
groups: "groups",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
providerId: p.id,
|
|
|
|
|
domain: p.host,
|
|
|
|
|
//@ts-ignore
|
|
|
|
|
issuer: p.issuerUrl,
|
|
|
|
|
})),
|
|
|
|
|
provisionUser: async ({user: usr, userInfo, provider}) => {
|
|
|
|
|
const existingUser = await db.query.user.findFirst({
|
|
|
|
|
where: eq(drizzleDb.schemas.user.email, usr.email),
|
|
|
|
|
});
|
2026-02-23 12:04:48 +01:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
if (existingUser && existingUser.role === "superadmin") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-23 12:04:48 +01:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
if (existingUser && env.AUTH_SYNC_OIDC_ROLES_ON_LOGIN === "false") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-23 12:04:48 +01:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
const providerId = provider.providerId;
|
|
|
|
|
const oidcProvider = oidcProviders.find((p) => p.id === providerId);
|
|
|
|
|
const allowedGroup = oidcProvider?.allowedGroup || env.ALLOWED_GROUP;
|
|
|
|
|
const roleMapStr = oidcProvider?.roleMap;
|
2026-02-23 12:04:48 +01:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
const rawGroups = userInfo?.groups || userInfo?.roles || [];
|
2026-02-23 12:04:48 +01:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
const userGroups: string[] = Array.isArray(rawGroups)
|
|
|
|
|
? rawGroups.reduce((carry: string[], group) => {
|
|
|
|
|
carry.push(String(group).toLowerCase());
|
|
|
|
|
return carry;
|
|
|
|
|
}, [])
|
|
|
|
|
: [String(rawGroups).toLowerCase()];
|
2026-02-23 12:04:48 +01:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
let roleToAssign: string | undefined;
|
2026-02-23 12:04:48 +01:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
if (roleMapStr) {
|
|
|
|
|
const mappings = roleMapStr.split(",").map((m) => m.split(":"));
|
|
|
|
|
for (const [group, role] of mappings) {
|
|
|
|
|
if (userGroups.includes(group.trim())) {
|
|
|
|
|
roleToAssign = role.trim();
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-05-16 15:54:17 +02:00
|
|
|
}
|
2026-03-18 19:47:41 +01:00
|
|
|
}
|
2025-05-16 15:54:17 +02:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
if (!roleToAssign && allowedGroup) {
|
|
|
|
|
const hasAccess = userGroups.includes(allowedGroup);
|
2025-05-16 15:54:17 +02:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
if (hasAccess) {
|
|
|
|
|
const userCount = (
|
|
|
|
|
await db.select({count: count()}).from(drizzleDb.schemas.user)
|
|
|
|
|
)[0].count;
|
|
|
|
|
const isSuperadmin = userCount === 0;
|
2025-05-16 15:54:17 +02:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
roleToAssign =
|
|
|
|
|
allowedGroup.includes("admin") ||
|
|
|
|
|
allowedGroup.includes("superadmin")
|
|
|
|
|
? isSuperadmin
|
|
|
|
|
? "superadmin"
|
|
|
|
|
: "admin"
|
|
|
|
|
: "pending";
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-16 15:54:17 +02:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
if (!roleToAssign && oidcProvider?.defaultRole) {
|
|
|
|
|
roleToAssign = oidcProvider.defaultRole;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!roleToAssign) {
|
|
|
|
|
console.warn(
|
|
|
|
|
`Access Denied for user ${usr.email}: No matching group/role found in ${providerId} config.`,
|
|
|
|
|
);
|
|
|
|
|
throw new APIError("FORBIDDEN", {
|
|
|
|
|
message: `Access Denied: No matching roles found (${roleToAssign})`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const userCount = (
|
|
|
|
|
await db.select({count: count()}).from(drizzleDb.schemas.user)
|
|
|
|
|
)[0].count;
|
|
|
|
|
if (
|
|
|
|
|
userCount === 0 &&
|
|
|
|
|
(roleToAssign === "admin" || roleToAssign === "superadmin")
|
|
|
|
|
) {
|
|
|
|
|
roleToAssign = "superadmin";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (existingUser) {
|
|
|
|
|
await db
|
|
|
|
|
.update(drizzleDb.schemas.user)
|
|
|
|
|
.set({role: roleToAssign, emailVerified: true})
|
|
|
|
|
.where(eq(drizzleDb.schemas.user.id, existingUser.id));
|
|
|
|
|
} else {
|
2025-05-16 15:54:17 +02:00
|
|
|
return {
|
2026-03-18 19:47:41 +01:00
|
|
|
...usr,
|
|
|
|
|
role: roleToAssign,
|
|
|
|
|
emailVerified: true,
|
2025-05-16 15:54:17 +02:00
|
|
|
};
|
2026-03-18 19:47:41 +01:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
...(env.AUTH_PASSKEY_ENABLED === "true"
|
|
|
|
|
? [
|
|
|
|
|
passkey({
|
|
|
|
|
rpName: env.PROJECT_NAME || "Portabase",
|
|
|
|
|
rpID: env.PROJECT_URL
|
|
|
|
|
? new URL(env.PROJECT_URL).hostname
|
|
|
|
|
: "localhost",
|
|
|
|
|
}),
|
|
|
|
|
]
|
|
|
|
|
: []),
|
|
|
|
|
openAPI(),
|
|
|
|
|
nextCookies(),
|
|
|
|
|
twoFactor(),
|
|
|
|
|
organization({
|
|
|
|
|
ac,
|
|
|
|
|
roles: {
|
|
|
|
|
owner: orgOwner,
|
|
|
|
|
admin: orgAdmin,
|
|
|
|
|
member: orgMember,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
adminPlugin({
|
|
|
|
|
adminRoles: ["admin", "superadmin"],
|
|
|
|
|
defaultRole: "pending",
|
|
|
|
|
ac,
|
|
|
|
|
roles: {
|
|
|
|
|
admin,
|
|
|
|
|
user,
|
|
|
|
|
pending,
|
|
|
|
|
superadmin,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
advanced: {
|
|
|
|
|
database: {
|
|
|
|
|
generateId: false,
|
|
|
|
|
},
|
|
|
|
|
cookies: {
|
|
|
|
|
state: {
|
|
|
|
|
attributes: {
|
|
|
|
|
sameSite: "none",
|
|
|
|
|
secure: true,
|
2025-05-16 15:54:17 +02:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-03-18 19:47:41 +01:00
|
|
|
},
|
|
|
|
|
user: {
|
|
|
|
|
deleteUser: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
},
|
|
|
|
|
changeEmail: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
},
|
|
|
|
|
additionalFields: {
|
|
|
|
|
deletedAt: {
|
|
|
|
|
type: "number",
|
|
|
|
|
nullable: true,
|
|
|
|
|
required: false,
|
|
|
|
|
},
|
|
|
|
|
theme: {
|
|
|
|
|
type: "string",
|
|
|
|
|
},
|
|
|
|
|
lastConnectedAt: {
|
|
|
|
|
type: "date",
|
|
|
|
|
},
|
|
|
|
|
lastChangedPasswordAt: {
|
|
|
|
|
type: "date",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
databaseHooks: {
|
|
|
|
|
account: {
|
|
|
|
|
create: {
|
|
|
|
|
before: async (account) => {
|
|
|
|
|
const provider = SUPPORTED_PROVIDERS.find(
|
|
|
|
|
(p) => p.id === account.providerId,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (provider && provider.allowLinking === false) {
|
|
|
|
|
throw new APIError("FORBIDDEN", {
|
|
|
|
|
message: "Linking is disabled for this provider.",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
user: {
|
|
|
|
|
update: {
|
|
|
|
|
async before(user, context) {
|
|
|
|
|
if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") {
|
|
|
|
|
if (user.password || user.lastChangedPasswordAt) {
|
|
|
|
|
throw new APIError("FORBIDDEN", {
|
|
|
|
|
message: "Password updates are disabled",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
data: user,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
create: {
|
|
|
|
|
async before(user, context) {
|
|
|
|
|
const userCount = (
|
|
|
|
|
await db.select({count: count()}).from(drizzleDb.schemas.user)
|
|
|
|
|
)[0].count;
|
|
|
|
|
|
|
|
|
|
if (env.AUTH_SIGNUP_ENABLED !== "true" && userCount > 0) {
|
|
|
|
|
throw new APIError("FORBIDDEN", {
|
|
|
|
|
message: "Sign up is disabled",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const role = userCount === 0 ? "superadmin" : "pending";
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
data: {
|
|
|
|
|
...user,
|
|
|
|
|
role,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
async after(user, context) {
|
|
|
|
|
const userCount = (
|
|
|
|
|
await db.select({count: count()}).from(drizzleDb.schemas.user)
|
|
|
|
|
)[0].count;
|
|
|
|
|
const role = userCount === 0 ? "owner" : "admin";
|
|
|
|
|
|
|
|
|
|
const defaultOrgSlug = "default";
|
|
|
|
|
const defaultOrg = await db.query.organization.findFirst({
|
|
|
|
|
where: eq(drizzleDb.schemas.organization.slug, defaultOrgSlug),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (defaultOrg) {
|
|
|
|
|
await db.insert(drizzleDb.schemas.member).values({
|
|
|
|
|
userId: user.id,
|
|
|
|
|
organizationId: defaultOrg.id,
|
|
|
|
|
role: role,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
console.warn(
|
|
|
|
|
"Default organization not found. Cannot assign member.",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
session: {
|
|
|
|
|
create: {
|
|
|
|
|
before: async (session, context) => {
|
|
|
|
|
const userId = session.userId;
|
|
|
|
|
|
|
|
|
|
const memberships = await db.query.member.findMany({
|
|
|
|
|
where: eq(drizzleDb.schemas.member.userId, userId),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const url =
|
|
|
|
|
context?.request?.url || context?.headers?.get("referer") || "";
|
|
|
|
|
|
|
|
|
|
let providerId: string | undefined;
|
|
|
|
|
|
|
|
|
|
if (url) {
|
|
|
|
|
const urlPath = new URL(url).pathname;
|
|
|
|
|
const pathParts = urlPath.split("/");
|
|
|
|
|
const lastPathPart = pathParts[pathParts.length - 1];
|
|
|
|
|
|
|
|
|
|
if (urlPath.startsWith("/api/auth/sso/callback/")) {
|
|
|
|
|
providerId = lastPathPart;
|
|
|
|
|
} else if (urlPath.startsWith("/api/auth/callback/")) {
|
|
|
|
|
providerId = lastPathPart;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
data: {
|
|
|
|
|
activeOrganizationId: memberships[0]?.organizationId,
|
|
|
|
|
providerId: providerId,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
after: async (session) => {
|
|
|
|
|
const user = await db.query.user.findFirst({
|
|
|
|
|
where: eq(drizzleDb.schemas.user.id, session.userId),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user) return;
|
|
|
|
|
|
|
|
|
|
const createdAtDiff =
|
|
|
|
|
new Date(session.createdAt).getTime() -
|
|
|
|
|
new Date(user.createdAt).getTime();
|
|
|
|
|
|
|
|
|
|
if (createdAtDiff < 5000) {
|
2026-05-15 22:05:13 +02:00
|
|
|
log.debug(`Skipping new login email for freshly created user ${user.email}`);
|
2026-03-18 19:47:41 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lastDiff = user.lastConnectedAt
|
|
|
|
|
? new Date(session.createdAt).getTime() -
|
|
|
|
|
new Date(user.lastConnectedAt).getTime()
|
|
|
|
|
: Infinity;
|
|
|
|
|
|
|
|
|
|
if (lastDiff < 30000) return;
|
|
|
|
|
|
|
|
|
|
if (user.role === "pending") return;
|
|
|
|
|
|
|
|
|
|
const deviceInfo = getDeviceDetails(session.userAgent);
|
|
|
|
|
|
2026-04-02 11:07:48 +02:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
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!,
|
|
|
|
|
}),
|
|
|
|
|
{},
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
2026-05-15 22:05:13 +02:00
|
|
|
log.error({
|
2026-04-02 11:07:48 +02:00
|
|
|
userId: user.id,
|
|
|
|
|
details: "Please Check your env variables or system config",
|
|
|
|
|
email: user.email,
|
|
|
|
|
error,
|
2026-05-15 22:05:13 +02:00
|
|
|
}, "sendEmail failed");
|
2026-04-02 11:07:48 +02:00
|
|
|
}
|
2026-03-18 19:47:41 +01:00
|
|
|
|
|
|
|
|
(await auth.$context).internalAdapter.updateUser(user.id, {
|
|
|
|
|
lastConnectedAt: new Date(),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
session: {
|
|
|
|
|
additionalFields: {
|
|
|
|
|
activeOrganizationId: {
|
|
|
|
|
type: "string",
|
|
|
|
|
required: false,
|
|
|
|
|
},
|
|
|
|
|
providerId: {
|
|
|
|
|
type: "string",
|
|
|
|
|
required: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
/* databaseHooks: {
|
|
|
|
|
session: {
|
|
|
|
|
create: {
|
|
|
|
|
before: async (session) => {
|
|
|
|
|
const organizationId = await getLastOrganizationOrFirst(session.userId);
|
|
|
|
|
|
|
|
|
|
if (!organizationId) {
|
|
|
|
|
return {
|
|
|
|
|
...session,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const [aa] = await db
|
|
|
|
|
.update(drizzleUser.session)
|
|
|
|
|
.set({ activeOrganizationId: organizationId })
|
|
|
|
|
.where(eq(drizzleUser.session.id, session.id))
|
|
|
|
|
.returning();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...session,
|
|
|
|
|
activeOrganizationId: organizationId,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},*/
|
2026-03-29 14:10:58 +02:00
|
|
|
// trustedOrigins: [env.PROJECT_URL!, "http://app"],
|
|
|
|
|
trustedOrigins: async (request) => {
|
|
|
|
|
const trustedOrigins = await queryTrustedDomains();
|
|
|
|
|
return trustedOrigins;
|
|
|
|
|
}
|
2025-05-16 15:54:17 +02:00
|
|
|
});
|
|
|
|
|
|
2026-03-29 14:10:58 +02:00
|
|
|
|
|
|
|
|
const queryTrustedDomains = async (): Promise<string[]> => {
|
|
|
|
|
const envDomains = env.TRUSTED_DOMAINS || "";
|
|
|
|
|
const domains = envDomains
|
|
|
|
|
.split(",")
|
|
|
|
|
.map((d) => d.trim())
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
|
|
if (env.PROJECT_URL) domains.push(env.PROJECT_URL);
|
|
|
|
|
domains.push("http://app")
|
|
|
|
|
|
|
|
|
|
return domains;
|
|
|
|
|
};
|
|
|
|
|
|
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;
|
|
|
|
|
};*/
|
|
|
|
|
|
2026-02-23 12:04:48 +01:00
|
|
|
export const createUser = async (
|
2026-03-18 19:47:41 +01:00
|
|
|
name: string,
|
|
|
|
|
email: string,
|
|
|
|
|
password: string,
|
|
|
|
|
role: "user" | "pending" | "admin" | "superadmin" = "pending",
|
2026-02-23 12:04:48 +01:00
|
|
|
) => {
|
2026-03-18 19:47:41 +01:00
|
|
|
return await auth.api.createUser({
|
|
|
|
|
headers: await headers(),
|
|
|
|
|
body: {
|
|
|
|
|
name,
|
|
|
|
|
email,
|
|
|
|
|
password,
|
|
|
|
|
role,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-05-16 15:54:17 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getSessions = async () => {
|
2026-03-18 19:47:41 +01:00
|
|
|
return await auth.api.listSessions({
|
|
|
|
|
headers: await headers(),
|
|
|
|
|
});
|
2025-05-16 15:54:17 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getSession = async () => {
|
2026-03-18 19:47:41 +01:00
|
|
|
return await auth.api.getSession({
|
|
|
|
|
headers: await headers(),
|
|
|
|
|
});
|
2025-05-16 15:54:17 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const revokeSession = async (e: string) => {
|
2026-03-18 19:47:41 +01:00
|
|
|
try {
|
|
|
|
|
const {status} = await auth.api.revokeSession({
|
|
|
|
|
body: {
|
|
|
|
|
token: e,
|
|
|
|
|
},
|
|
|
|
|
headers: await headers(),
|
|
|
|
|
});
|
|
|
|
|
return status;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
}
|
2025-05-16 15:54:17 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getAccounts = async () => {
|
2026-03-18 19:47:41 +01:00
|
|
|
return await auth.api.listUserAccounts({
|
|
|
|
|
headers: await headers(),
|
|
|
|
|
});
|
2025-05-16 15:54:17 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const unlinkAccount = async (provider: string, account: string) => {
|
2026-03-18 19:47:41 +01:00
|
|
|
try {
|
|
|
|
|
const {status} = await auth.api.unlinkAccount({
|
|
|
|
|
body: {
|
|
|
|
|
providerId: provider,
|
|
|
|
|
accountId: account,
|
|
|
|
|
},
|
|
|
|
|
headers: await headers(),
|
|
|
|
|
});
|
2025-05-16 15:54:17 +02:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
return status;
|
|
|
|
|
} 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 ({
|
2026-03-18 19:47:41 +01:00
|
|
|
organizationId,
|
|
|
|
|
organizationSlug,
|
|
|
|
|
}: {
|
|
|
|
|
organizationId?: string;
|
|
|
|
|
organizationSlug?: string;
|
2025-07-26 14:43:59 +02:00
|
|
|
} = {}): Promise<OrganizationWithMembersAndUsers | null> => {
|
2026-03-18 19:47:41 +01:00
|
|
|
const query =
|
|
|
|
|
organizationId != null
|
|
|
|
|
? {organizationId}
|
|
|
|
|
: organizationSlug != null
|
|
|
|
|
? {organizationSlug}
|
|
|
|
|
: undefined;
|
2025-05-16 15:54:17 +02:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
try {
|
|
|
|
|
const response = await auth.api.getFullOrganization({
|
|
|
|
|
headers: await headers(),
|
|
|
|
|
...(query ? {query} : {}),
|
|
|
|
|
});
|
2025-07-26 14:43:59 +02:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
return response as OrganizationWithMembersAndUsers;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-05-16 15:54:17 +02:00
|
|
|
};
|
|
|
|
|
|
2026-02-13 16:48:13 +01:00
|
|
|
export const getPasskeys = async () => {
|
2026-03-18 19:47:41 +01:00
|
|
|
if (env.AUTH_PASSKEY_ENABLED !== "true") return [];
|
|
|
|
|
const passkeys = await auth.api.listPasskeys({
|
|
|
|
|
headers: await headers(),
|
|
|
|
|
});
|
2026-02-13 16:48:13 +01:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
return passkeys;
|
2026-02-13 16:48:13 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const revokePasskey = async (e: string) => {
|
2026-03-18 19:47:41 +01:00
|
|
|
if (env.AUTH_PASSKEY_ENABLED !== "true") return;
|
|
|
|
|
await auth.api.deletePasskey({
|
|
|
|
|
body: {
|
|
|
|
|
id: e,
|
|
|
|
|
},
|
|
|
|
|
headers: await headers(),
|
|
|
|
|
});
|
2026-02-13 16:48:13 +01:00
|
|
|
};
|
|
|
|
|
|
2025-07-29 08:51:50 +02:00
|
|
|
export const listOrganizations = async (): Promise<Organization[] | null> => {
|
2026-03-18 19:47:41 +01:00
|
|
|
try {
|
|
|
|
|
return (await auth.api.listOrganizations({
|
|
|
|
|
headers: await headers(),
|
|
|
|
|
})) as Organization[];
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-05-16 15:54:17 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getLastOrganizationOrFirst = async (userId: string) => {
|
2026-03-18 19:47:41 +01:00
|
|
|
try {
|
|
|
|
|
const organizations = await db.query.organization.findMany({
|
|
|
|
|
where: eq(drizzleDb.schemas.member.userId, userId),
|
|
|
|
|
});
|
2025-05-16 15:54:17 +02:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
if (organizations.length > 0) {
|
|
|
|
|
return organizations[0].id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return null;
|
2025-05-16 15:54:17 +02:00
|
|
|
}
|
|
|
|
|
};
|
2025-07-26 14:43:59 +02:00
|
|
|
|
2025-05-16 15:54:17 +02:00
|
|
|
export const createOrganization = async (name: string, slug: string) => {
|
2026-03-18 19:47:41 +01:00
|
|
|
try {
|
|
|
|
|
return await auth.api.createOrganization({
|
|
|
|
|
headers: await headers(),
|
|
|
|
|
body: {
|
|
|
|
|
name,
|
|
|
|
|
slug,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} 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
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
console.error("Auth API createOrganization error:", {
|
|
|
|
|
message: errorMessage,
|
|
|
|
|
status,
|
|
|
|
|
raw: e,
|
|
|
|
|
});
|
2025-07-26 14:43:59 +02:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
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) => {
|
2026-03-18 19:47:41 +01:00
|
|
|
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;
|
2025-07-26 14:43:59 +02:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
console.error("Auth API deleteOrganization error:", {
|
|
|
|
|
message: errorMessage,
|
|
|
|
|
status,
|
|
|
|
|
raw: e,
|
|
|
|
|
});
|
2025-07-26 14:43:59 +02:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
throw {
|
|
|
|
|
name: "AuthDeleteOrganizationError",
|
|
|
|
|
message: errorMessage,
|
|
|
|
|
status,
|
|
|
|
|
cause: e,
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-07-26 14:43:59 +02:00
|
|
|
};
|
|
|
|
|
|
2025-05-16 15:54:17 +02:00
|
|
|
export const checkSlugOrganization = async (slug: string) => {
|
2026-03-18 19:47:41 +01:00
|
|
|
try {
|
|
|
|
|
const {status} = await auth.api.checkOrganizationSlug({
|
|
|
|
|
headers: await headers(),
|
|
|
|
|
body: {
|
|
|
|
|
slug,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-05-16 15:54:17 +02:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
return status;
|
|
|
|
|
} catch {
|
|
|
|
|
}
|
2025-05-16 15:54:17 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getActiveMember = async () => {
|
2026-03-18 19:47:41 +01:00
|
|
|
try {
|
|
|
|
|
const member = await auth.api.getActiveMember({
|
|
|
|
|
headers: await headers(),
|
|
|
|
|
});
|
2025-05-16 15:54:17 +02:00
|
|
|
|
2026-03-18 19:47:41 +01:00
|
|
|
return member as MemberWithUser;
|
|
|
|
|
} catch (e) {
|
2026-05-15 22:05:13 +02:00
|
|
|
log.error({ error: e }, "Auth error");
|
2026-03-18 19:47:41 +01:00
|
|
|
}
|
2025-05-16 15:54:17 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const setActiveOrganization = async (slug: string) => {
|
2026-03-18 19:47:41 +01:00
|
|
|
try {
|
|
|
|
|
return await auth.api.setActiveOrganization({
|
|
|
|
|
headers: await headers(),
|
|
|
|
|
body: {
|
|
|
|
|
organizationSlug: slug,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} catch {
|
|
|
|
|
}
|
2025-05-16 15:54:17 +02:00
|
|
|
};
|