mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Merge branch 'dev' into feat/oidc
This commit is contained in:
+51
-58
@@ -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
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user