Merge pull request #3736 from Dokploy/feat/add-modify-sso-by-admin

refactor(sso): update trusted origins handling and introduce license …
This commit is contained in:
Mauricio Siu
2026-02-18 01:49:37 -06:00
committed by GitHub
8 changed files with 118 additions and 43 deletions

View File

@@ -84,9 +84,10 @@ export const SSOSettings = () => {
const [newOriginInput, setNewOriginInput] = useState("");
const { data: providers, isLoading } = api.sso.listProviders.useQuery();
const { data: userData } = api.user.get.useQuery(undefined, {
enabled: manageOriginsOpen,
});
const { data: trustedOrigins = [] } = api.sso.getTrustedOrigins.useQuery(
undefined,
{ enabled: manageOriginsOpen },
);
const { mutateAsync: deleteProvider, isLoading: isDeleting } =
api.sso.deleteProvider.useMutation();
const { mutateAsync: addTrustedOrigin, isLoading: isAddingOrigin } =
@@ -96,8 +97,6 @@ export const SSOSettings = () => {
const { mutateAsync: updateTrustedOrigin, isLoading: isUpdatingOrigin } =
api.sso.updateTrustedOrigin.useMutation();
const trustedOrigins = userData?.user?.trustedOrigins ?? [];
const handleAddOrigin = async () => {
const value = newOriginInput.trim();
if (!value) return;
@@ -105,7 +104,7 @@ export const SSOSettings = () => {
await addTrustedOrigin({ origin: value });
toast.success("Trusted origin added");
setNewOriginInput("");
await utils.user.get.invalidate();
await utils.sso.getTrustedOrigins.invalidate();
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Failed to add trusted origin",
@@ -118,7 +117,7 @@ export const SSOSettings = () => {
await removeTrustedOrigin({ origin });
toast.success("Trusted origin removed");
if (editingOrigin === origin) setEditingOrigin(null);
await utils.user.get.invalidate();
await utils.sso.getTrustedOrigins.invalidate();
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Failed to remove trusted origin",
@@ -144,7 +143,7 @@ export const SSOSettings = () => {
toast.success("Trusted origin updated");
setEditingOrigin(null);
setEditingValue("");
await utils.user.get.invalidate();
await utils.sso.getTrustedOrigins.invalidate();
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Failed to update trusted origin",

View File

@@ -105,7 +105,6 @@ export default function Home({ IS_CLOUD }: Props) {
setIsLoginLoading(false);
}
};
const onTwoFactorSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (twoFactorCode.length !== 6) {

View File

@@ -1,5 +1,5 @@
import { user } from "@dokploy/server/db/schema";
import { validateLicenseKey } from "@dokploy/server/index";
import { hasValidLicense, validateLicenseKey } from "@dokploy/server/index";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { z } from "zod";
@@ -184,18 +184,7 @@ export const licenseKeyRouter = createTRPCRouter({
};
}),
haveValidLicenseKey: adminProcedure.query(async ({ ctx }) => {
const currentUserId = ctx.user.id;
const currentUser = await db.query.user.findFirst({
where: eq(user.id, currentUserId),
columns: {
enableEnterpriseFeatures: true,
isValidEnterpriseLicense: true,
},
});
return !!(
currentUser?.enableEnterpriseFeatures &&
currentUser?.isValidEnterpriseLicense
);
return await hasValidLicense(ctx.session.activeOrganizationId);
}),
updateEnterpriseSettings: adminProcedure
.input(

View File

@@ -2,7 +2,10 @@ import { normalizeTrustedOrigin } from "@dokploy/server";
import { IS_CLOUD } from "@dokploy/server/constants";
import { member, ssoProvider, user } from "@dokploy/server/db/schema";
import { ssoProviderBodySchema } from "@dokploy/server/db/schema/sso";
import { requestToHeaders } from "@dokploy/server/index";
import {
getOrganizationOwnerId,
requestToHeaders,
} from "@dokploy/server/index";
import { auth } from "@dokploy/server/lib/auth";
import { TRPCError } from "@trpc/server";
import { and, asc, eq } from "drizzle-orm";
@@ -59,6 +62,17 @@ export const ssoRouter = createTRPCRouter({
});
return providers;
}),
getTrustedOrigins: enterpriseProcedure.query(async ({ ctx }) => {
const ownerId = await getOrganizationOwnerId(
ctx.session.activeOrganizationId,
);
if (!ownerId) return [];
const ownerUser = await db.query.user.findFirst({
where: eq(user.id, ownerId),
columns: { trustedOrigins: true },
});
return ownerUser?.trustedOrigins ?? [];
}),
one: enterpriseProcedure
.input(z.object({ providerId: z.string().min(1) }))
.query(async ({ ctx, input }) => {
@@ -135,11 +149,20 @@ export const ssoRouter = createTRPCRouter({
normalizeTrustedOrigin(existing.issuer) !==
normalizeTrustedOrigin(input.issuer);
if (issuerChanged) {
const currentUser = await db.query.user.findFirst({
where: eq(user.id, ctx.session.userId),
const ownerId = await getOrganizationOwnerId(
ctx.session.activeOrganizationId,
);
if (!ownerId) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Organization owner not found",
});
}
const ownerUser = await db.query.user.findFirst({
where: eq(user.id, ownerId),
columns: { trustedOrigins: true },
});
const trustedOrigins = currentUser?.trustedOrigins ?? [];
const trustedOrigins = ownerUser?.trustedOrigins ?? [];
const newOrigin = normalizeTrustedOrigin(input.issuer);
const isInTrustedOrigins = trustedOrigins.some(
(o) => o.toLowerCase() === newOrigin.toLowerCase(),
@@ -148,7 +171,7 @@ export const ssoRouter = createTRPCRouter({
throw new TRPCError({
code: "BAD_REQUEST",
message:
"The new Issuer URL is not in your trusted origins list. Please add it in Manage origins before saving.",
"The new Issuer URL is not in the organization's trusted origins list. Please add it in Manage origins before saving.",
});
}
}
@@ -262,12 +285,21 @@ export const ssoRouter = createTRPCRouter({
addTrustedOrigin: enterpriseProcedure
.input(z.object({ origin: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
const ownerId = await getOrganizationOwnerId(
ctx.session.activeOrganizationId,
);
if (!ownerId) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Organization owner not found",
});
}
const normalized = normalizeTrustedOrigin(input.origin);
const currentUser = await db.query.user.findFirst({
where: eq(user.id, ctx.session.userId),
const ownerUser = await db.query.user.findFirst({
where: eq(user.id, ownerId),
columns: { trustedOrigins: true },
});
const existing = currentUser?.trustedOrigins || [];
const existing = ownerUser?.trustedOrigins || [];
if (existing.some((o) => o.toLowerCase() === normalized.toLowerCase())) {
return { success: true };
}
@@ -275,25 +307,34 @@ export const ssoRouter = createTRPCRouter({
await db
.update(user)
.set({ trustedOrigins: next })
.where(eq(user.id, ctx.session.userId));
.where(eq(user.id, ownerId));
return { success: true };
}),
removeTrustedOrigin: enterpriseProcedure
.input(z.object({ origin: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
const ownerId = await getOrganizationOwnerId(
ctx.session.activeOrganizationId,
);
if (!ownerId) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Organization owner not found",
});
}
const normalized = normalizeTrustedOrigin(input.origin);
const currentUser = await db.query.user.findFirst({
where: eq(user.id, ctx.session.userId),
const ownerUser = await db.query.user.findFirst({
where: eq(user.id, ownerId),
columns: { trustedOrigins: true },
});
const existing = currentUser?.trustedOrigins || [];
const existing = ownerUser?.trustedOrigins || [];
const next = existing.filter(
(o) => o.toLowerCase() !== normalized.toLowerCase(),
);
await db
.update(user)
.set({ trustedOrigins: next })
.where(eq(user.id, ctx.session.userId));
.where(eq(user.id, ownerId));
return { success: true };
}),
updateTrustedOrigin: enterpriseProcedure
@@ -304,20 +345,29 @@ export const ssoRouter = createTRPCRouter({
}),
)
.mutation(async ({ ctx, input }) => {
const ownerId = await getOrganizationOwnerId(
ctx.session.activeOrganizationId,
);
if (!ownerId) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Organization owner not found",
});
}
const oldNorm = normalizeTrustedOrigin(input.oldOrigin);
const newNorm = normalizeTrustedOrigin(input.newOrigin);
const currentUser = await db.query.user.findFirst({
where: eq(user.id, ctx.session.userId),
const ownerUser = await db.query.user.findFirst({
where: eq(user.id, ownerId),
columns: { trustedOrigins: true },
});
const existing = currentUser?.trustedOrigins || [];
const existing = ownerUser?.trustedOrigins || [];
const next = existing.map((o) =>
o.toLowerCase() === oldNorm.toLowerCase() ? newNorm : o,
);
await db
.update(user)
.set({ trustedOrigins: next })
.where(eq(user.id, ctx.session.userId));
.where(eq(user.id, ownerId));
return { success: true };
}),
});

View File

@@ -7,6 +7,7 @@
* need to use are documented accordingly near the end.
*/
import { hasValidLicense } from "@dokploy/server/index";
import { validateRequest } from "@dokploy/server/lib/auth";
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
import { initTRPC, TRPCError } from "@trpc/server";
@@ -239,10 +240,11 @@ export const enterpriseProcedure = t.procedure.use(async ({ ctx, next }) => {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
if (
!ctx.user?.enableEnterpriseFeatures ||
!ctx.user.isValidEnterpriseLicense
) {
const hasValidLicenseResult = await hasValidLicense(
ctx.session.activeOrganizationId,
);
if (!hasValidLicenseResult) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Valid enterprise license required",

View File

@@ -31,6 +31,7 @@ export * from "./services/port";
export * from "./services/postgres";
export * from "./services/preview-deployment";
export * from "./services/project";
export * from "./services/proprietary/license-key";
export * from "./services/proprietary/sso";
export * from "./services/redirect";
export * from "./services/redis";

View File

@@ -0,0 +1,24 @@
import { db } from "@dokploy/server/db";
import { user } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
import { getOrganizationOwnerId } from "./sso";
export const hasValidLicense = async (organizationId: string) => {
const ownerId = await getOrganizationOwnerId(organizationId);
if (!ownerId) {
return false;
}
const currentUser = await db.query.user.findFirst({
where: eq(user.id, ownerId),
columns: {
enableEnterpriseFeatures: true,
isValidEnterpriseLicense: true,
},
});
return !!(
currentUser?.enableEnterpriseFeatures &&
currentUser?.isValidEnterpriseLicense
);
};

View File

@@ -1,4 +1,6 @@
import { db } from "@dokploy/server/db";
import { organization } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
export const getSSOProviders = async () => {
const providers = await db.query.ssoProvider.findMany({
@@ -33,3 +35,12 @@ export const normalizeTrustedOrigin = (value: string): string => {
// e.g. "https://example.com/" -> "https://example.com"
return value.trim().replace(/\/+$/, "");
};
export const getOrganizationOwnerId = async (organizationId: string) => {
const org = await db.query.organization.findFirst({
where: eq(organization.id, organizationId),
columns: { ownerId: true },
});
if (!org) return null;
return org.ownerId;
};