Merge branch 'dev' into feat/oidc

This commit is contained in:
Théo LAGACHE
2026-02-24 16:49:16 +01:00
35 changed files with 1191 additions and 3598 deletions
+51 -58
View File
@@ -35,10 +35,11 @@ import EmailForgotPassword from "@/components/emails/auth/email-forgot-password"
import { getDeviceDetails } from "@/utils/detection";
import EmailNewLogin from "@/components/emails/auth/email-new-login";
import { sso } from "@better-auth/sso";
import { AuthProviderConfig, SUPPORTED_PROVIDERS } from "@/lib/auth/config";
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";
const oidcProviders = getOidcProviders();
@@ -54,6 +55,11 @@ export const auth = betterAuth({
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.",
});
}
await db
.update(drizzleDb.schemas.user)
.set(
@@ -104,37 +110,33 @@ export const auth = betterAuth({
});
},
},
socialProviders: SUPPORTED_PROVIDERS.reduce(
(acc: any, provider: AuthProviderConfig) => {
if (!provider.isActive) return acc;
if (provider.id === "credential") return acc;
if (provider.type === "sso") return acc;
if (provider.id === "google") {
acc.google = {
clientId: env.AUTH_GOOGLE_ID! as string,
clientSecret: env.AUTH_GOOGLE_SECRET! as string,
};
}
if (provider.id === "github") {
acc.github = {
// clientId: provider.credentials?.clientId,
// clientSecret: provider.credentials?.clientSecret,
};
}
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;
}, {}),
account: {
accountLinking: {
enabled: true,
trustedProviders: [
"google",
"github",
"credential",
...getOAuthProviders().map((p) => p.id),
...oidcProviders.map((p) => p.id),
],
allowDifferentEmails: false,
allowDifferentEmails: true,
},
},
@@ -288,6 +290,14 @@ export const auth = betterAuth({
database: {
generateId: false,
},
cookies: {
state: {
attributes: {
sameSite: "none",
secure: true,
},
},
},
},
user: {
deleteUser: {
@@ -320,6 +330,7 @@ export const auth = betterAuth({
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.",
@@ -413,50 +424,28 @@ export const auth = betterAuth({
const url =
context?.request?.url || context?.headers?.get("referer") || "";
let providerId: string;
let providerId: string | undefined;
if (url.includes("/sso/callback")) {
const urlObj = new URL(url, "http://localhost");
providerId = urlObj.searchParams.get("providerId") || "sso";
console.log(`Found provider: ${providerId}`);
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,
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 && user.role != "pending") {
// const deviceInfo = getDeviceDetails(session.userAgent);
// 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!,
// }),
// {}
// ),
// });
//
// (await auth.$context).internalAdapter.updateUser(user.id, {
// lastConnectedAt: new Date(),
// });
// }
// },
after: async (session) => {
console.log("session", session);
const user = await db.query.user.findFirst({
where: eq(drizzleDb.schemas.user.id, session.userId),
});
@@ -512,6 +501,10 @@ export const auth = betterAuth({
type: "string",
required: false,
},
providerId: {
type: "string",
required: false,
},
},
},
/* databaseHooks: {
+26 -23
View File
@@ -1,5 +1,7 @@
import { env } from "@/env.mjs";
import { getOidcProviders } from "./oidc";
import { getOAuthProviders } from "./oauth";
import * as BetterAuthSocialProviders from "better-auth/social-providers";
export interface AuthProviderConfig {
id: string;
@@ -15,6 +17,9 @@ export interface AuthProviderConfig {
}
const oidcProviders = getOidcProviders();
const oauthProviders = getOAuthProviders();
const availableSocialProviders = Object.keys(BetterAuthSocialProviders);
export const SUPPORTED_PROVIDERS: AuthProviderConfig[] = [
{
@@ -29,28 +34,26 @@ export const SUPPORTED_PROVIDERS: AuthProviderConfig[] = [
allowLinking: true,
allowUnlinking: true,
},
{
id: "google",
isActive: !!env.AUTH_GOOGLE_ID,
name: "Google",
icon: "logos:google-icon",
title: "Google",
description: "Sign in with your Google account.",
type: "social",
allowLinking: true,
allowUnlinking: true,
},
{
id: "github",
isActive: !!env.AUTH_GITHUB_ID,
name: "GitHub",
icon: "logos:github-icon",
title: "GitHub",
description: "Sign in with your GitHub account.",
type: "social",
allowLinking: true,
allowUnlinking: true,
},
...oauthProviders.map((p) => {
const isSupported = availableSocialProviders.includes(p.id.toLowerCase());
if (!isSupported) {
console.warn(`Provider ${p.id} is not supported. Skipping...`);
}
return {
id: p.id,
isActive: isSupported,
name: p.title,
icon: p.icon,
title: p.title,
description: p.description,
isManual: false,
type: "social" as const,
allowLinking: p.allowLinking,
allowUnlinking: p.allowUnlinking,
};
}),
...oidcProviders.map((p) => ({
id: p.id,
isActive: true,
@@ -58,7 +61,7 @@ export const SUPPORTED_PROVIDERS: AuthProviderConfig[] = [
icon: p.icon,
title: p.title,
description: p.description,
isManual: true,
isManual: false,
type: "sso" as const,
allowLinking: p.allowLinking,
allowUnlinking: p.allowUnlinking,
+122
View File
@@ -0,0 +1,122 @@
import { env } from "@/env.mjs";
export interface OAuthProvider {
id: string;
title: string;
description: string;
icon: string;
client: string;
secret: string;
allowedGroup?: string;
roleMap?: string;
defaultRole?: string;
allowLinking: boolean;
allowUnlinking: boolean;
appleBundleIdentifier?: string;
}
const PROVIDER_ICONS: Record<string, string> = {
google: "logos:google-icon",
github: "logos:github-icon",
gitlab: "logos:gitlab-icon",
discord: "logos:discord-icon",
facebook: "logos:facebook",
twitter: "logos:twitter",
x: "logos:x",
linkedin: "logos:linkedin-icon",
apple: "logos:apple",
microsoft: "logos:microsoft-icon",
twitch: "logos:twitch",
spotify: "logos:spotify-icon",
slack: "logos:slack-icon",
tiktok: "logos:tiktok-icon",
figma: "logos:figma",
dropbox: "logos:dropbox",
notion: "logos:notion-icon",
paypal: "logos:paypal",
reddit: "logos:reddit-icon",
salesforce: "logos:salesforce",
vercel: "logos:vercel-icon",
zoom: "logos:zoom-icon",
altassian: "logos:jira",
};
function getProviderIcon(providerId: string, envIcon?: string): string {
if (envIcon) return envIcon;
const normalizedId = providerId.toLowerCase();
if (PROVIDER_ICONS[normalizedId]) {
return PROVIDER_ICONS[normalizedId];
}
return `lucide:building`;
}
export function getOAuthProviders(): OAuthProvider[] {
const providers: OAuthProvider[] = [];
if (env.AUTH_SOCIAL_CLIENT && env.AUTH_SOCIAL_ID) {
providers.push({
id: env.AUTH_SOCIAL_ID,
title: env.AUTH_SOCIAL_TITLE || "OAuth",
description: env.AUTH_SOCIAL_DESC || "Sign in with your OAuth account.",
icon: env.AUTH_SOCIAL_ICON || "lucide:building",
client: env.AUTH_SOCIAL_CLIENT,
secret: env.AUTH_SOCIAL_SECRET || "",
allowedGroup: env.ALLOWED_GROUP,
roleMap: env.AUTH_ROLE_MAP,
defaultRole: env.AUTH_DEFAULT_ROLE,
allowLinking: env.AUTH_ALLOW_LINKING !== "false",
allowUnlinking: env.AUTH_ALLOW_UNLINKING !== "false",
appleBundleIdentifier: env.AUTH_SOCIAL_APPLE_APP_BUNDLE_IDENTIFIER,
});
}
const prefixes = new Set<string>();
Object.keys(process.env).forEach((key) => {
const match = key.match(/^AUTH_SOCIAL_(.+)_CLIENT$/);
if (match) {
prefixes.add(match[1]);
}
});
prefixes.forEach((prefix) => {
const client = process.env[`AUTH_SOCIAL_${prefix}_CLIENT`];
if (!client) return;
const providerId = prefix.toLowerCase();
const envTitle = process.env[`AUTH_SOCIAL_${prefix}_TITLE`];
const envDesc = process.env[`AUTH_SOCIAL_${prefix}_DESC`];
const envIcon = process.env[`AUTH_SOCIAL_${prefix}_ICON`];
const envSecret = process.env[`AUTH_SOCIAL_${prefix}_SECRET`];
const envAllowedGroup = process.env[`AUTH_SOCIAL_${prefix}_ALLOWED_GROUP`];
const envRoleMap = process.env[`AUTH_SOCIAL_${prefix}_ROLE_MAP`];
const envDefaultRole = process.env[`AUTH_SOCIAL_${prefix}_DEFAULT_ROLE`];
const envAppleBundleId =
process.env[`AUTH_SOCIAL_APPLE_APP_BUNDLE_IDENTIFIER`];
providers.push({
id: providerId,
title:
envTitle ||
prefix.charAt(0).toUpperCase() + prefix.slice(1).toLowerCase(),
description:
envDesc ||
`Sign in with ${prefix.charAt(0).toUpperCase() + prefix.slice(1).toLowerCase()}`,
icon: getProviderIcon(providerId, envIcon),
client: client,
secret: envSecret || "",
allowedGroup: envAllowedGroup || env.ALLOWED_GROUP,
roleMap: envRoleMap,
defaultRole: envDefaultRole,
allowLinking: env.AUTH_ALLOW_LINKING !== "false",
allowUnlinking: env.AUTH_ALLOW_UNLINKING !== "false",
appleBundleIdentifier: envAppleBundleId,
});
});
return providers;
}
+4 -4
View File
@@ -41,10 +41,10 @@ export function getOidcProviders(): OIDCProvider[] {
jwksEndpoint: env.AUTH_OIDC_JWKS_ENDPOINT,
pkce: env.AUTH_OIDC_PKCE === "true",
allowedGroup: env.ALLOWED_GROUP,
roleMap: process.env.AUTH_OIDC_ROLE_MAP,
defaultRole: process.env.AUTH_OIDC_DEFAULT_ROLE,
allowLinking: process.env.AUTH_OIDC_ALLOW_LINKING !== "false",
allowUnlinking: process.env.AUTH_OIDC_ALLOW_UNLINKING !== "false",
roleMap: env.AUTH_ROLE_MAP,
defaultRole: env.AUTH_DEFAULT_ROLE,
allowLinking: env.AUTH_ALLOW_LINKING !== "false",
allowUnlinking: env.AUTH_ALLOW_UNLINKING !== "false",
});
}