import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { admin as adminPlugin, genericOAuth, // apiKey, // createAuthMiddleware, //customSession, jwt, lastLoginMethod, username, } from "better-auth/plugins"; //import { eq } from "drizzle-orm"; import { db } from "../db/db.controller.js"; import * as rawSchema from "../db/schema/auth.schema.js"; import { ac, admin, systemAdmin, user } from "./auth.permissions.js"; import { allowedOrigins } from "./cors.utils.js"; import { sendEmail } from "./sendEmail.utils.js"; function decodeJwtPayload>(jwt: string): T { const parts = jwt.split("."); if (parts.length < 2) { throw new Error("Invalid JWT"); } const payload = parts[1]?.replace(/-/g, "+").replace(/_/g, "/"); const padded = payload?.padEnd( payload.length + ((4 - (payload.length % 4)) % 4), "=", ); const json = Buffer.from(padded ?? "", "base64").toString("utf8"); return JSON.parse(json) as T; } function normalizeGroups(groups?: unknown): string[] { if (!Array.isArray(groups)) return []; return groups .filter((g): g is string => typeof g === "string") .map((g) => g.trim().toLowerCase()) .filter((g) => g.length > 0); } type VoidAuthClaims = { sub: string; name?: string; preferred_username?: string; email?: string; email_verified?: boolean; groups?: string[]; picture?: string; iss?: string; aud?: string; exp?: number; iat?: number; }; export const schema = { user: rawSchema.user, session: rawSchema.session, account: rawSchema.account, verification: rawSchema.verification, jwks: rawSchema.jwks, apiKey: rawSchema.apikey, // 🔑 rename to apiKey }; const hasOAuth = Boolean(process.env.PROVIDER) && Boolean(process.env.CLIENT_ID) && Boolean(process.env.CLIENT_SECRET) && Boolean(process.env.DISCOVERY_URL); if (!hasOAuth) { console.warn("Missing oauth data."); } const oauthPlugins = hasOAuth ? [ genericOAuth({ config: [ { providerId: process.env.PROVIDER!, clientId: process.env.CLIENT_ID!, clientSecret: process.env.CLIENT_SECRET!, discoveryUrl: process.env.DISCOVERY_URL!, scopes: (process.env.CLIENT_SCOPES ?? "") .split(/[,\s]+/) .filter(Boolean), pkce: true, requireIssuerValidation: true, redirectURI: `${process.env.URL}/lst/api/auth/oauth2/callback/${process.env.PROVIDER!}`, getUserInfo: async (tokens) => { if (!tokens.idToken) { throw new Error("VoidAuth did not return an idToken"); } const claims = decodeJwtPayload(tokens.idToken); const groups = normalizeGroups(claims.groups); return { id: claims.sub, email: claims.email ?? "", name: claims.name ?? claims.preferred_username ?? claims.email ?? "Unknown User", image: claims.picture ?? null, emailVerified: Boolean(claims.email_verified), groups, username: claims.preferred_username ?? null, } as any; }, mapProfileToUser: async (profile) => { return { name: profile.name, role: profile.groups?.includes("lst_admins") ? "systemAdmin" : profile.groups?.includes("admins") ? "admin" : "user", }; }, }, ], }), ] : []; export const auth = betterAuth({ appName: "lst", baseURL: `${process.env.URL}/lst/api/auth`, database: drizzleAdapter(db, { provider: "pg", schema, }), trustedOrigins: allowedOrigins, user: { additionalFields: { role: { type: "string", //required: false, input: false, }, }, }, account: { encryptOAuthTokens: true, updateAccountOnSignIn: true, accountLinking: { enabled: true, trustedProviders: ["voidauth"], }, }, plugins: [ jwt({ jwt: { expirationTime: "1h" } }), //apiKey(), adminPlugin({ ac, roles: { admin, user, systemAdmin, }, }), lastLoginMethod(), username({ minUsernameLength: 5, usernameValidator: (username) => { if (username === "admin" || username === "root") { return false; } return true; }, }), ...oauthPlugins, // customSession(async ({ user, session }) => { // const roles = await db // .select({ roles: rawSchema.user.role }) // .from(rawSchema.user) // .where(eq(rawSchema.user.id, session.id)); // return { // roles, // user: { // ...user, // //newField: "newField", // }, // session, // }; // }), ], emailAndPassword: { enabled: true, minPasswordLength: 8, // optional config resetPasswordTokenExpirySeconds: process.env.RESET_EXPIRY_SECONDS, // time in seconds sendResetPassword: async ({ user, token }) => { const frontendUrl = `${process.env.URL}/lst/app/user/resetpassword?token=${token}`; const expiryMinutes = Math.floor( parseInt(process.env.RESET_EXPIRY_SECONDS ?? "3600", 10) / 60, ); const expiryText = expiryMinutes >= 60 ? `${expiryMinutes / 60} hour${expiryMinutes === 60 ? "" : "s"}` : `${expiryMinutes} minutes`; const emailData = { email: user.email, subject: "LST- Forgot password request", template: "forgotPassword", context: { username: user.name, email: user.email, url: frontendUrl, expiry: expiryText, }, }; await sendEmail(emailData); }, // onPasswordReset: async ({ user }, request) => { // // your logic here // console.log(`Password for user ${user.email} has been reset.`); // }, }, session: { expiresIn: 60 * 60, updateAge: 60 * 5, freshAge: 60 * 2, cookieCache: { enabled: true, maxAge: 5 * 60, }, }, cookie: { path: "/lst", sameSite: "lax", secure: false, httpOnly: true, }, // hooks: { // after: createAuthMiddleware(async (ctx) => { // if (ctx.path.startsWith("/login")) { // const newSession = ctx.context.newSession; // if (newSession) { // // something here later // } // } // }), // }, events: { // async onSignInSuccess({ user }: { user: User }) { // await db // .update(rawSchema.user) // .set({ lastLogin: new Date() }) // .where(eq(schema.user.id, user.id)); // }, }, });