Merge pull request #5071 from Dokploy/fix/scim-sso-account-linking

fix(auth): link SCIM-provisioned users through SSO and surface sign-in errors
This commit is contained in:
Mauricio Siu
2026-08-13 02:12:45 -06:00
committed by GitHub
5 changed files with 48 additions and 3 deletions
@@ -13,6 +13,7 @@ export function SignInWithGithub() {
try {
const { error } = await authClient.signIn.social({
provider: "github",
errorCallbackURL: "/",
});
if (error) {
toast.error(error.message);
@@ -13,6 +13,7 @@ export function SignInWithGoogle() {
try {
const { error } = await authClient.signIn.social({
provider: "google",
errorCallbackURL: "/",
});
if (error) {
toast.error(error.message);
@@ -50,6 +50,7 @@ export function SignInWithSSO({
const { data, error } = await authClient.signIn.sso({
email: values.email,
callbackURL: "/dashboard/home",
errorCallbackURL: "/",
});
if (error) {
toast.error(error.message ?? "Failed to sign in with SSO");
+24 -1
View File
@@ -10,7 +10,7 @@ import { Fingerprint } from "lucide-react";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
import { type ReactElement, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -85,6 +85,29 @@ export default function Home({ IS_CLOUD, enforceSSO }: Props) {
},
});
useEffect(() => {
const queryError = router.query.error;
if (!queryError) return;
const raw = Array.isArray(queryError) ? queryError[0] : queryError;
if (!raw) return;
const normalized = raw.replace(/[+_]/g, " ").toLowerCase();
setError(
normalized.includes("account not linked")
? "This account already exists but isn't linked to that sign-in provider yet. Contact your administrator to link it."
: normalized.includes("access denied")
? "Access was denied by the identity provider."
: "We couldn't complete sign-in. Please try again or contact your administrator.",
);
const { error: _removed, ...rest } = router.query;
router.replace({ pathname: router.pathname, query: rest }, undefined, {
shallow: true,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [router.query.error]);
const onSubmit = async (values: LoginForm) => {
setIsLoginLoading(true);
try {
+21 -2
View File
@@ -77,6 +77,9 @@ const createBetterAuth = () =>
...(!IS_CLOUD ? ["/verify-email"] : []),
],
secret: betterAuthSecret,
onAPIError: {
errorURL: "/",
},
...(!IS_CLOUD
? {
advanced: {
@@ -165,6 +168,9 @@ const createBetterAuth = () =>
user: {
create: {
before: async (_user, context) => {
if (context?.path.includes("/scim")) {
return { data: { emailVerified: true } };
}
if (!IS_CLOUD) {
const xDokployToken =
context?.request?.headers?.get("x-dokploy-token");
@@ -197,8 +203,7 @@ const createBetterAuth = () =>
}
} else {
const isSSORequest = context?.path.includes("/sso");
const isSCIMRequest = context?.path.includes("/scim");
if (isSSORequest || isSCIMRequest) {
if (isSSORequest) {
return;
}
const isAdminPresent = await db.query.member.findFirst({
@@ -251,6 +256,20 @@ const createBetterAuth = () =>
}
if (isSCIMRequest) {
const membership = await db.query.member.findFirst({
where: eq(schema.member.userId, user.id),
});
if (membership) {
const defaultRole = await resolveOrganizationDefaultRole(
membership.organizationId,
);
if (defaultRole !== membership.role) {
await db
.update(schema.member)
.set({ role: defaultRole })
.where(eq(schema.member.id, membership.id));
}
}
return;
}