mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
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:
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
24
packages/server/src/services/proprietary/license-key.ts
Normal file
24
packages/server/src/services/proprietary/license-key.ts
Normal 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
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user