405 lines · 9.6 KB · TypeScript
RawDownload
import type { IncomingMessage } from "node:http";import * as bcrypt from "bcrypt";import { betterAuth } from "better-auth";import { drizzleAdapter } from "better-auth/adapters/drizzle";import { APIError } from "better-auth/api";import { admin, apiKey, organization, twoFactor } from "better-auth/plugins";import { and, desc, eq } from "drizzle-orm";import { IS_CLOUD } from "../constants";import { db } from "../db";import * as schema from "../db/schema";import { getUserByToken } from "../services/admin";import {	getWebServerSettings,	updateWebServerSettings,} from "../services/web-server-settings";import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot";import { sendEmail } from "../verification/send-verification-email";import { getPublicIpWithFallback } from "../wss/utils"; const { handler, api } = betterAuth({	database: drizzleAdapter(db, {		provider: "pg",		schema: schema,	}),	appName: "Dokploy",	socialProviders: {		github: {			clientId: process.env.GITHUB_CLIENT_ID as string,			clientSecret: process.env.GITHUB_CLIENT_SECRET as string,		},		google: {			clientId: process.env.GOOGLE_CLIENT_ID as string,			clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,		},	},	logger: {		disabled: process.env.NODE_ENV === "production",	},	...(!IS_CLOUD && {		async trustedOrigins() {			const settings = await getWebServerSettings();			if (!settings) {				return [];			}			return [				...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),				...(settings?.host ? [`https://${settings?.host}`] : []),				...(process.env.NODE_ENV === "development"					? [							"http://localhost:3000",							"https://absolutely-handy-falcon.ngrok-free.app",						]					: []),			];		},	}),	emailVerification: {		sendOnSignUp: true,		autoSignInAfterVerification: true,		sendVerificationEmail: async ({ user, url }) => {			if (IS_CLOUD) {				await sendEmail({					email: user.email,					subject: "Verify your email",					text: `				<p>Click the link to verify your email: <a href="${url}">Verify Email</a></p>				`,				});			}		},	},	emailAndPassword: {		enabled: true,		autoSignIn: !IS_CLOUD,		requireEmailVerification: IS_CLOUD,		password: {			async hash(password) {				return bcrypt.hashSync(password, 10);			},			async verify({ hash, password }) {				return bcrypt.compareSync(password, hash);			},		},		sendResetPassword: async ({ user, url }) => {			await sendEmail({				email: user.email,				subject: "Reset your password",				text: `				<p>Click the link to reset your password: <a href="${url}">Reset Password</a></p>				`,			});		},	},	databaseHooks: {		user: {			create: {				before: async (_user, context) => {					if (!IS_CLOUD) {						const xDokployToken =							context?.request?.headers?.get("x-dokploy-token");						if (xDokployToken) {							const user = await getUserByToken(xDokployToken);							if (!user) {								throw new APIError("BAD_REQUEST", {									message: "User not found",								});							}						} else {							const isAdminPresent = await db.query.member.findFirst({								where: eq(schema.member.role, "owner"),							});							if (isAdminPresent) {								throw new APIError("BAD_REQUEST", {									message: "Admin is already created",								});							}						}					}				},				after: async (user, context) => {					const isAdminPresent = await db.query.member.findFirst({						where: eq(schema.member.role, "owner"),					}); 					if (!IS_CLOUD) {						await updateWebServerSettings({							serverIp: await getPublicIpWithFallback(),						});					} 					if (IS_CLOUD) {						try {							const hutk = getHubSpotUTK(								context?.request?.headers?.get("cookie") || undefined,							);							// Cast to include additional fields							const userWithFields = user as typeof user & {								lastName?: string;							};							const hubspotSuccess = await submitToHubSpot(								{									email: user.email,									firstName: user.name || "", // name is mapped to firstName column									lastName: userWithFields.lastName || "",								},								hutk,							);							if (!hubspotSuccess) {								console.error("Failed to submit to HubSpot");							}						} catch (error) {							console.error("Error submitting to HubSpot", error);						}					} 					if (IS_CLOUD || !isAdminPresent) {						await db.transaction(async (tx) => {							const organization = await tx								.insert(schema.organization)								.values({									name: "My Organization",									ownerId: user.id,									createdAt: new Date(),								})								.returning()								.then((res) => res[0]); 							await tx.insert(schema.member).values({								userId: user.id,								organizationId: organization?.id || "",								role: "owner",								createdAt: new Date(),								isDefault: true, // Mark first organization as default							});						});					}				},			},		},		session: {			create: {				before: async (session) => {					// Find the default organization for this user					// Priority: 1) isDefault=true, 2) most recently created					const member = await db.query.member.findFirst({						where: eq(schema.member.userId, session.userId),						orderBy: [							desc(schema.member.isDefault),							desc(schema.member.createdAt),						],						with: {							organization: true,						},					}); 					return {						data: {							...session,							activeOrganizationId: member?.organization.id,						},					};				},			},		},	},	session: {		expiresIn: 60 * 60 * 24 * 3,		updateAge: 60 * 60 * 24,	},	user: {		modelName: "user",		fields: {			name: "firstName", // Map better-auth's default 'name' field to 'firstName' column		},		additionalFields: {			role: {				type: "string",				// required: true,				input: false,			},			ownerId: {				type: "string",				// required: true,				input: false,			},			allowImpersonation: {				fieldName: "allowImpersonation",				type: "boolean",				defaultValue: false,			},			lastName: {				type: "string",				required: false,				input: true,				defaultValue: "",			},		},	},	plugins: [		apiKey({			enableMetadata: true,		}),		twoFactor(),		organization({			async sendInvitationEmail(data, _request) {				if (IS_CLOUD) {					const host =						process.env.NODE_ENV === "development"							? "http://localhost:3000"							: "https://app.dokploy.com";					const inviteLink = `${host}/invitation?token=${data.id}`; 					await sendEmail({						email: data.email,						subject: "Invitation to join organization",						text: `					<p>You are invited to join ${data.organization.name} on Dokploy. Click the link to accept the invitation: <a href="${inviteLink}">Accept Invitation</a></p>					`,					});				}			},		}),		...(IS_CLOUD			? [					admin({						adminUserIds: [process.env.USER_ADMIN_ID as string],					}),				]			: []),	],}); export const auth = {	handler,	createApiKey: api.createApiKey,}; export const validateRequest = async (request: IncomingMessage) => {	const apiKey = request.headers["x-api-key"] as string;	if (apiKey) {		try {			const { valid, key, error } = await api.verifyApiKey({				body: {					key: apiKey,				},			}); 			if (error) {				throw new Error(error.message || "Error verifying API key");			}			if (!valid || !key) {				return {					session: null,					user: null,				};			} 			const apiKeyRecord = await db.query.apikey.findFirst({				where: eq(schema.apikey.id, key.id),				with: {					user: true,				},			}); 			if (!apiKeyRecord) {				return {					session: null,					user: null,				};			} 			const organizationId = JSON.parse(				apiKeyRecord.metadata || "{}",			).organizationId; 			if (!organizationId) {				return {					session: null,					user: null,				};			} 			const member = await db.query.member.findFirst({				where: and(					eq(schema.member.userId, apiKeyRecord.user.id),					eq(schema.member.organizationId, organizationId),				),				with: {					organization: true,				},			}); 			// When accessing from DB, use actual column names			const userFromDb = apiKeyRecord.user as typeof apiKeyRecord.user & {				firstName: string;				lastName: string;			}; 			const mockSession = {				session: {					userId: apiKeyRecord.user.id,					activeOrganizationId: organizationId || "",				},				user: {					id: userFromDb.id,					name: userFromDb.firstName, // Map firstName back to name for better-auth					email: userFromDb.email,					emailVerified: userFromDb.emailVerified,					image: userFromDb.image,					createdAt: userFromDb.createdAt,					updatedAt: userFromDb.updatedAt,					twoFactorEnabled: userFromDb.twoFactorEnabled,					role: member?.role || "member",					ownerId: member?.organization.ownerId || apiKeyRecord.user.id,				},			}; 			return mockSession;		} catch (error) {			console.error("Error verifying API key", error);			return {				session: null,				user: null,			};		}	} 	// If no API key, proceed with normal session validation	const session = await api.getSession({		headers: new Headers({			cookie: request.headers.cookie || "",		}),	}); 	if (!session?.session || !session.user) {		return {			session: null,			user: null,		};	} 	if (session?.user) {		const member = await db.query.member.findFirst({			where: and(				eq(schema.member.userId, session.user.id),				eq(					schema.member.organizationId,					session.session.activeOrganizationId || "",				),			),			with: {				organization: true,			},		}); 		session.user.role = member?.role || "member";		if (member) {			session.user.ownerId = member.organization.ownerId;		} else {			session.user.ownerId = session.user.id;		}	} 	return session;};