refactor: simplify forward authentication handling in UI and API

- Removed the selection of SSO providers from the UI, streamlining the process to enable/disable SSO for domains.
- Updated the API to eliminate the need for a provider ID when enabling forward authentication, relying on the configured settings instead.
- Enhanced user feedback by updating toast messages to reflect the current state of SSO authentication.
- Improved the UI layout for better clarity on SSO status and actions.

This refactor enhances the user experience by simplifying the SSO configuration process and ensuring clearer communication of actions taken.
This commit is contained in:
Mauricio Siu
2026-06-06 03:37:31 -06:00
parent 41c09cd86b
commit 4f6e57cc9c
3 changed files with 42 additions and 116 deletions
@@ -1,5 +1,5 @@
import { ShieldCheck } from "lucide-react";
import { useEffect, useState } from "react";
import { useState } from "react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
@@ -12,13 +12,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
interface Props {
@@ -28,35 +22,28 @@ interface Props {
export const HandleForwardAuth = ({ domainId, applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedProviderId, setSelectedProviderId] = useState<string>("");
const { data: haveValidLicense } =
api.licenseKey.haveValidLicenseKey.useQuery();
const utils = api.useUtils();
const { data: status, isLoading: isLoadingStatus } =
api.forwardAuth.status.useQuery({ domainId }, { enabled: isOpen });
const { data: providers, isLoading: isLoadingProviders } =
api.forwardAuth.listProviders.useQuery(undefined, { enabled: isOpen });
const { data: status } = api.forwardAuth.status.useQuery(
{ domainId },
{ enabled: isOpen },
);
const { mutateAsync: enable, isPending: isEnabling } =
api.forwardAuth.enable.useMutation();
const { mutateAsync: disable, isPending: isDisabling } =
api.forwardAuth.disable.useMutation();
useEffect(() => {
if (status?.providerId) {
setSelectedProviderId(status.providerId);
}
}, [status?.providerId]);
if (!haveValidLicense) {
return null;
}
const isEnabled = !!status?.enabled;
const hasProviders = (providers?.length ?? 0) > 0;
const isPending = isEnabling || isDisabling;
const refresh = async () => {
await utils.forwardAuth.status.invalidate({ domainId });
@@ -64,32 +51,21 @@ export const HandleForwardAuth = ({ domainId, applicationId }: Props) => {
await utils.application.readTraefikConfig.invalidate({ applicationId });
};
const handleEnable = async () => {
if (!selectedProviderId) {
toast.error("Select an SSO provider first");
return;
}
const handleToggle = async (next: boolean) => {
try {
await enable({ domainId, providerId: selectedProviderId });
if (next) {
await enable({ domainId });
toast.success("SSO authentication enabled for this domain");
} else {
await disable({ domainId });
toast.success("SSO authentication disabled for this domain");
}
await refresh();
toast.success("SSO authentication enabled for this domain");
setIsOpen(false);
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Error enabling SSO",
);
}
};
const handleDisable = async () => {
try {
await disable({ domainId });
await refresh();
toast.success("SSO authentication disabled for this domain");
setIsOpen(false);
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Error disabling SSO",
error instanceof Error
? error.message
: "Error updating SSO authentication",
);
}
};
@@ -121,72 +97,32 @@ export const HandleForwardAuth = ({ domainId, applicationId }: Props) => {
</DialogDescription>
</DialogHeader>
{!isLoadingProviders && !hasProviders && (
<AlertBlock type="warning">
No SSO providers configured. Add an OIDC provider in your
organization SSO settings first.
</AlertBlock>
)}
<AlertBlock type="info">
Requires the authentication domain + proxy to be configured in SSO
settings, and this app's domain to share its base domain.
The authentication proxy must be deployed for this app's server in SSO
settings. The domain must share its base domain.
</AlertBlock>
<div className="flex flex-col gap-4 py-2">
<div className="flex flex-col gap-2">
<span className="text-sm font-medium">Identity provider</span>
<Select
value={selectedProviderId}
onValueChange={setSelectedProviderId}
disabled={isLoadingStatus || isLoadingProviders || !hasProviders}
>
<SelectTrigger>
<SelectValue placeholder="Select an SSO provider">
{selectedProviderId || ""}
</SelectValue>
</SelectTrigger>
<SelectContent>
{providers?.map((provider) => (
<SelectItem
key={provider.providerId}
value={provider.providerId}
>
<div className="flex flex-col">
<span className="font-medium">{provider.providerId}</span>
<span className="text-xs text-muted-foreground">
{provider.issuer}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="flex flex-col">
<span className="text-sm font-medium">
Protect this domain with SSO
</span>
<span className="text-xs text-muted-foreground">
{isEnabled
? "Visitors must log in via your identity provider."
: "The domain is publicly accessible."}
</span>
</div>
{isEnabled && (
<AlertBlock type="info">
SSO is currently enabled for this domain.
</AlertBlock>
)}
<Switch
checked={isEnabled}
disabled={isPending}
onCheckedChange={handleToggle}
/>
</div>
<DialogFooter className="flex-row justify-end gap-2">
{isEnabled && (
<Button
variant="destructive"
isLoading={isDisabling}
onClick={handleDisable}
>
Disable
</Button>
)}
<Button
isLoading={isEnabling}
disabled={!hasProviders || !selectedProviderId}
onClick={handleEnable}
>
{isEnabled ? "Update" : "Enable"}
<DialogFooter>
<Button variant="outline" onClick={() => setIsOpen(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
@@ -124,12 +124,7 @@ export const forwardAuthRouter = createTRPCRouter({
.query(({ ctx, input }) => getDomainSsoStatus(ctx, input.domainId)),
enable: enterpriseProcedure
.input(
z.object({
domainId: z.string().min(1),
providerId: z.string().min(1),
}),
)
.input(z.object({ domainId: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
const domain = await assertApplicationDomainAccess(
ctx,
@@ -138,8 +133,6 @@ export const forwardAuthRouter = createTRPCRouter({
);
const result = await enableForwardAuthOnDomain({
domainId: input.domainId,
providerId: input.providerId,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
@@ -326,19 +326,16 @@ export const assertApplicationDomainAccess = async (
export const enableForwardAuthOnDomain = async (input: {
domainId: string;
providerId: string;
organizationId: string;
}) => {
const { application } = await resolveApplicationDomain(input.domainId);
await findProviderForOrg(input.providerId, input.organizationId);
const serverId = application.serverId ?? undefined;
const settings = await getForwardAuthSettings(serverId ?? null);
if (!settings) {
if (!settings?.providerId) {
throw new TRPCError({
code: "PRECONDITION_FAILED",
message:
"Set the authentication domain and deploy the proxy for this server first.",
"Deploy the authentication proxy for this server in SSO settings first.",
});
}
@@ -352,7 +349,7 @@ export const enableForwardAuthOnDomain = async (input: {
}
await updateDomainById(input.domainId, {
forwardAuthProviderId: input.providerId,
forwardAuthProviderId: settings.providerId,
});
const domain = await findDomainById(input.domainId);
await manageDomain(application, domain);