prmbr-image-mksaas/src/lib/auth.ts

220 lines
7.1 KiB
TypeScript

import { websiteConfig } from '@/config/website';
import {
addMonthlyFreeCredits,
addRegisterGiftCredits,
} from '@/credits/credits';
import { getDb } from '@/db/index';
import { defaultMessages } from '@/i18n/messages';
import { LOCALE_COOKIE_NAME, routing } from '@/i18n/routing';
import { sendEmail } from '@/mail';
import { subscribe } from '@/newsletter';
import { type User, betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { admin } from 'better-auth/plugins';
import { parse as parseCookies } from 'cookie';
import type { Locale } from 'next-intl';
import { getAllPricePlans } from './price-plan';
import { getBaseUrl, getUrlWithLocaleInCallbackUrl } from './urls/urls';
/**
* Better Auth configuration
*
* docs:
* https://mksaas.com/docs/auth
* https://www.better-auth.com/docs/reference/options
*/
export const auth = betterAuth({
baseURL: getBaseUrl(),
appName: defaultMessages.Metadata.name,
database: drizzleAdapter(await getDb(), {
provider: 'pg', // or "mysql", "sqlite"
}),
session: {
// https://www.better-auth.com/docs/concepts/session-management#cookie-cache
cookieCache: {
enabled: true,
maxAge: 60 * 60, // Cache duration in seconds
},
// https://www.better-auth.com/docs/concepts/session-management#session-expiration
expiresIn: 60 * 60 * 24 * 7,
updateAge: 60 * 60 * 24,
// https://www.better-auth.com/docs/concepts/session-management#session-freshness
// https://www.better-auth.com/docs/concepts/users-accounts#authentication-requirements
// disable freshness check for user deletion
freshAge: 0 /* 60 * 60 * 24 */,
},
emailAndPassword: {
enabled: true,
// https://www.better-auth.com/docs/concepts/email#2-require-email-verification
requireEmailVerification: true,
// https://www.better-auth.com/docs/authentication/email-password#forget-password
async sendResetPassword({ user, url }, request) {
const locale = getLocaleFromRequest(request);
const localizedUrl = getUrlWithLocaleInCallbackUrl(url, locale);
await sendEmail({
to: user.email,
template: 'forgotPassword',
context: {
url: localizedUrl,
name: user.name,
},
locale,
});
},
},
emailVerification: {
// https://www.better-auth.com/docs/concepts/email#auto-signin-after-verification
autoSignInAfterVerification: true,
// https://www.better-auth.com/docs/authentication/email-password#require-email-verification
sendVerificationEmail: async ({ user, url, token }, request) => {
const locale = getLocaleFromRequest(request);
const localizedUrl = getUrlWithLocaleInCallbackUrl(url, locale);
await sendEmail({
to: user.email,
template: 'verifyEmail',
context: {
url: localizedUrl,
name: user.name,
},
locale,
});
},
},
socialProviders: {
// https://www.better-auth.com/docs/authentication/github
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
// https://www.better-auth.com/docs/authentication/google
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
account: {
// https://www.better-auth.com/docs/concepts/users-accounts#account-linking
accountLinking: {
enabled: true,
trustedProviders: ['google', 'github'],
},
},
user: {
// https://www.better-auth.com/docs/concepts/database#extending-core-schema
additionalFields: {
customerId: {
type: 'string',
required: false,
},
},
// https://www.better-auth.com/docs/concepts/users-accounts#delete-user
deleteUser: {
enabled: true,
},
},
databaseHooks: {
// https://www.better-auth.com/docs/concepts/database#database-hooks
user: {
create: {
after: async (user) => {
await onCreateUser(user);
},
},
},
},
plugins: [
// https://www.better-auth.com/docs/plugins/admin
// support user management, ban/unban user, manage user roles, etc.
admin({
// https://www.better-auth.com/docs/plugins/admin#default-ban-reason
// defaultBanReason: 'Spamming',
defaultBanExpiresIn: undefined,
bannedUserMessage:
'You have been banned from this application. Please contact support if you believe this is an error.',
}),
],
onAPIError: {
// https://www.better-auth.com/docs/reference/options#onapierror
errorURL: '/auth/error',
onError: (error, ctx) => {
console.error('auth error:', error);
},
},
});
/**
* Gets the locale from a request by parsing the cookies
* If no locale is found in the cookies, returns the default locale
*
* @param request - The request to get the locale from
* @returns The locale from the request or the default locale
*/
export function getLocaleFromRequest(request?: Request): Locale {
const cookies = parseCookies(request?.headers.get('cookie') ?? '');
return (cookies[LOCALE_COOKIE_NAME] as Locale) ?? routing.defaultLocale;
}
/**
* On create user hook
*
* @param user - The user to create
*/
async function onCreateUser(user: User) {
// Auto subscribe user to newsletter after sign up if enabled in website config
// Add a delay to avoid hitting Resend's 1 email per second limit
if (
user.email &&
websiteConfig.newsletter.enable &&
websiteConfig.newsletter.autoSubscribeAfterSignUp
) {
// Delay newsletter subscription by 2 seconds to avoid rate limiting
// This ensures the email verification email is sent first
// Using 2 seconds instead of 1 to provide extra buffer for network delays
setTimeout(async () => {
try {
const subscribed = await subscribe(user.email);
if (!subscribed) {
console.error(`Failed to subscribe user ${user.email} to newsletter`);
} else {
console.log(`User ${user.email} subscribed to newsletter`);
}
} catch (error) {
console.error('Newsletter subscription error:', error);
}
}, 2000);
}
// Add register gift credits to the user if enabled in website config
if (
websiteConfig.credits.enableCredits &&
websiteConfig.credits.registerGiftCredits.enable &&
websiteConfig.credits.registerGiftCredits.amount > 0
) {
try {
await addRegisterGiftCredits(user.id);
console.log(`added register gift credits for user ${user.id}`);
} catch (error) {
console.error('Register gift credits error:', error);
}
}
// Add free monthly credits to the user if enabled in website config
if (websiteConfig.credits.enableCredits) {
const pricePlans = getAllPricePlans();
// NOTICE: make sure the free plan is not disabled and has credits enabled
const freePlan = pricePlans.find(
(plan) => plan.isFree && !plan.disabled && plan.credits?.enable
);
if (freePlan) {
try {
await addMonthlyFreeCredits(user.id, freePlan.id);
console.log(`added Free monthly credits for user ${user.id}`);
} catch (error) {
console.error('Free monthly credits error:', error);
}
}
}
}