262 lines
6.1 KiB
TypeScript
262 lines
6.1 KiB
TypeScript
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<T = Record<string, unknown>>(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<VoidAuthClaims>(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));
|
|
// },
|
|
},
|
|
});
|