mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-08 04:55:17 +00:00
feat(sso): enhance SSO provider management and trusted origins handling
- Added logic to retrieve and delete SSO providers, ensuring proper permission checks and error handling. - Updated user trusted origins when adding or removing SSO providers, maintaining accurate origin lists. - Refactored trusted origins retrieval to improve clarity and efficiency in the authentication process. - Introduced utility functions for normalizing trusted origins and converting request headers.
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
|
import { normalizeTrustedOrigin } from "@dokploy/server";
|
||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
import { member, ssoProvider } from "@dokploy/server/db/schema";
|
import { member, ssoProvider, user } from "@dokploy/server/db/schema";
|
||||||
import { ssoProviderBodySchema } from "@dokploy/server/db/schema/sso";
|
import { ssoProviderBodySchema } from "@dokploy/server/db/schema/sso";
|
||||||
|
import { requestToHeaders } from "@dokploy/server/index";
|
||||||
import { auth } from "@dokploy/server/lib/auth";
|
import { auth } from "@dokploy/server/lib/auth";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { and, asc, eq } from "drizzle-orm";
|
import { and, asc, eq } from "drizzle-orm";
|
||||||
@@ -12,20 +14,6 @@ import {
|
|||||||
} from "@/server/api/trpc";
|
} from "@/server/api/trpc";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
|
|
||||||
function requestToHeaders(req: {
|
|
||||||
headers?: Record<string, string | string[] | undefined>;
|
|
||||||
}): Headers {
|
|
||||||
const headers = new Headers();
|
|
||||||
if (req?.headers) {
|
|
||||||
for (const [key, value] of Object.entries(req.headers)) {
|
|
||||||
if (value !== undefined && key.toLowerCase() !== "host") {
|
|
||||||
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ssoRouter = createTRPCRouter({
|
export const ssoRouter = createTRPCRouter({
|
||||||
showSignInWithSSO: publicProcedure.query(async () => {
|
showSignInWithSSO: publicProcedure.query(async () => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
@@ -73,6 +61,28 @@ export const ssoRouter = createTRPCRouter({
|
|||||||
deleteProvider: enterpriseProcedure
|
deleteProvider: enterpriseProcedure
|
||||||
.input(z.object({ providerId: z.string().min(1) }))
|
.input(z.object({ providerId: z.string().min(1) }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
// Obtener el provider antes de eliminarlo para obtener sus dominios
|
||||||
|
const providerToDelete = await db.query.ssoProvider.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(ssoProvider.providerId, input.providerId),
|
||||||
|
eq(ssoProvider.organizationId, ctx.session.activeOrganizationId),
|
||||||
|
eq(ssoProvider.userId, ctx.session.userId),
|
||||||
|
),
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
domain: true,
|
||||||
|
issuer: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!providerToDelete) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message:
|
||||||
|
"SSO provider not found or you do not have permission to delete it",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const [deleted] = await db
|
const [deleted] = await db
|
||||||
.delete(ssoProvider)
|
.delete(ssoProvider)
|
||||||
.where(
|
.where(
|
||||||
@@ -92,6 +102,24 @@ export const ssoRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentUser = await db.query.user.findFirst({
|
||||||
|
where: eq(user.id, ctx.session.userId),
|
||||||
|
columns: {
|
||||||
|
trustedOrigins: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentUser?.trustedOrigins) {
|
||||||
|
const issuerOrigin = normalizeTrustedOrigin(providerToDelete.issuer);
|
||||||
|
const updatedOrigins = currentUser.trustedOrigins.filter(
|
||||||
|
(origin) => origin.toLowerCase() !== issuerOrigin.toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(user)
|
||||||
|
.set({ trustedOrigins: updatedOrigins })
|
||||||
|
.where(eq(user.id, ctx.session.userId));
|
||||||
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
register: enterpriseProcedure
|
register: enterpriseProcedure
|
||||||
@@ -119,6 +147,26 @@ export const ssoRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const domain = input.domains.join(",");
|
const domain = input.domains.join(",");
|
||||||
|
const currentUser = await db.query.user.findFirst({
|
||||||
|
where: eq(user.id, ctx.session.userId),
|
||||||
|
columns: {
|
||||||
|
trustedOrigins: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingOrigins = currentUser?.trustedOrigins || [];
|
||||||
|
|
||||||
|
const issuerOrigin = normalizeTrustedOrigin(input.issuer);
|
||||||
|
|
||||||
|
const newOrigins = Array.from(
|
||||||
|
new Set([...existingOrigins, issuerOrigin]),
|
||||||
|
);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(user)
|
||||||
|
.set({ trustedOrigins: newOrigins })
|
||||||
|
.where(eq(user.id, ctx.session.userId));
|
||||||
|
|
||||||
await auth.registerSSOProvider({
|
await auth.registerSSOProvider({
|
||||||
body: {
|
body: {
|
||||||
...input,
|
...input,
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export const user = pgTable("user", {
|
|||||||
stripeCustomerId: text("stripeCustomerId"),
|
stripeCustomerId: text("stripeCustomerId"),
|
||||||
stripeSubscriptionId: text("stripeSubscriptionId"),
|
stripeSubscriptionId: text("stripeSubscriptionId"),
|
||||||
serversQuantity: integer("serversQuantity").notNull().default(0),
|
serversQuantity: integer("serversQuantity").notNull().default(0),
|
||||||
|
trustedOrigins: text("trustedOrigins").array(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const usersRelations = relations(user, ({ one, many }) => ({
|
export const usersRelations = relations(user, ({ one, many }) => ({
|
||||||
@@ -85,6 +86,8 @@ const createSchema = createInsertSchema(user, {
|
|||||||
isRegistered: z.boolean().optional(),
|
isRegistered: z.boolean().optional(),
|
||||||
}).omit({
|
}).omit({
|
||||||
role: true,
|
role: true,
|
||||||
|
trustedOrigins: true,
|
||||||
|
isValidEnterpriseLicense: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateUserInvitation = createSchema.pick({}).extend({
|
export const apiCreateUserInvitation = createSchema.pick({}).extend({
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { and, desc, eq } from "drizzle-orm";
|
|||||||
import { IS_CLOUD } from "../constants";
|
import { IS_CLOUD } from "../constants";
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import * as schema from "../db/schema";
|
import * as schema from "../db/schema";
|
||||||
import { getUserByToken } from "../services/admin";
|
import { getTrustedOrigins, getUserByToken } from "../services/admin";
|
||||||
import {
|
import {
|
||||||
getWebServerSettings,
|
getWebServerSettings,
|
||||||
updateWebServerSettings,
|
updateWebServerSettings,
|
||||||
@@ -43,28 +43,24 @@ const { handler, api } = betterAuth({
|
|||||||
logger: {
|
logger: {
|
||||||
disabled: process.env.NODE_ENV === "production",
|
disabled: process.env.NODE_ENV === "production",
|
||||||
},
|
},
|
||||||
...(!IS_CLOUD && {
|
async trustedOrigins() {
|
||||||
async trustedOrigins() {
|
const trustedOrigins = await getTrustedOrigins();
|
||||||
const settings = await getWebServerSettings();
|
if (IS_CLOUD) {
|
||||||
if (!settings) {
|
return trustedOrigins;
|
||||||
return [];
|
}
|
||||||
}
|
const settings = await getWebServerSettings();
|
||||||
return [
|
if (!settings) {
|
||||||
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
|
return [];
|
||||||
...(settings?.host ? [`https://${settings?.host}`] : []),
|
}
|
||||||
...(process.env.NODE_ENV === "development"
|
return [
|
||||||
? [
|
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
|
||||||
"http://localhost:3000",
|
...(settings?.host ? [`https://${settings?.host}`] : []),
|
||||||
"https://absolutely-handy-falcon.ngrok-free.app",
|
...(process.env.NODE_ENV === "development"
|
||||||
"https://dev-pee8hhc3qbjlqedb.us.auth0.com",
|
? ["http://localhost:3000"]
|
||||||
"https://trial-2804699.okta.com",
|
: []),
|
||||||
"https://login.microsoftonline.com",
|
...trustedOrigins,
|
||||||
"https://graph.microsoft.com",
|
];
|
||||||
]
|
},
|
||||||
: []),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
emailVerification: {
|
emailVerification: {
|
||||||
sendOnSignUp: true,
|
sendOnSignUp: true,
|
||||||
autoSignInAfterVerification: true,
|
autoSignInAfterVerification: true,
|
||||||
|
|||||||
@@ -116,3 +116,18 @@ export const getDokployUrl = async () => {
|
|||||||
}
|
}
|
||||||
return `http://${settings?.serverIp}:${process.env.PORT}`;
|
return `http://${settings?.serverIp}:${process.env.PORT}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTrustedOrigins = async () => {
|
||||||
|
const members = await db.query.member.findMany({
|
||||||
|
where: eq(member.role, "owner"),
|
||||||
|
with: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const trustedOrigins = members.flatMap(
|
||||||
|
(member) => member.user.trustedOrigins || [],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Array.from(new Set(trustedOrigins));
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,3 +13,23 @@ export const getSSOProviders = async () => {
|
|||||||
});
|
});
|
||||||
return providers;
|
return providers;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const requestToHeaders = (req: {
|
||||||
|
headers?: Record<string, string | string[] | undefined>;
|
||||||
|
}): Headers => {
|
||||||
|
const headers = new Headers();
|
||||||
|
if (req?.headers) {
|
||||||
|
for (const [key, value] of Object.entries(req.headers)) {
|
||||||
|
if (value !== undefined && key.toLowerCase() !== "host") {
|
||||||
|
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeTrustedOrigin = (value: string): string => {
|
||||||
|
// Keep it simple: trim and remove trailing slashes.
|
||||||
|
// e.g. "https://example.com/" -> "https://example.com"
|
||||||
|
return value.trim().replace(/\/+$/, "");
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user