refactor: own page for sso registration
This commit is contained in:
parent
4619d9d976
commit
0bf0094dbd
4 changed files with 313 additions and 256 deletions
|
|
@ -1,9 +1,7 @@
|
||||||
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
|
||||||
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { type } from "arktype";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { AlertTriangle, Ban, Trash2 } from "lucide-react";
|
import { Ban, Trash2 } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
deleteSsoInvitationMutation,
|
deleteSsoInvitationMutation,
|
||||||
|
|
@ -22,69 +20,28 @@ import {
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "~/client/components/ui/alert-dialog";
|
} from "~/client/components/ui/alert-dialog";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "~/client/components/ui/alert";
|
import { Alert, AlertDescription } from "~/client/components/ui/alert";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "~/client/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "~/client/components/ui/form";
|
|
||||||
import { Input } from "~/client/components/ui/input";
|
import { Input } from "~/client/components/ui/input";
|
||||||
import { Label } from "~/client/components/ui/label";
|
import { Label } from "~/client/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||||
import { Switch } from "~/client/components/ui/switch";
|
import { Switch } from "~/client/components/ui/switch";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
|
||||||
import { authClient } from "~/client/lib/auth-client";
|
|
||||||
import { parseError } from "~/client/lib/errors";
|
import { parseError } from "~/client/lib/errors";
|
||||||
import { useOrganizationContext } from "~/client/hooks/use-org-context";
|
import { useOrganizationContext } from "~/client/hooks/use-org-context";
|
||||||
import { formatDateWithMonth } from "~/client/lib/datetime";
|
import { formatDateWithMonth } from "~/client/lib/datetime";
|
||||||
import { getOrigin } from "~/client/functions/get-origin";
|
import { getOrigin } from "~/client/functions/get-origin";
|
||||||
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
|
|
||||||
const ssoProviderSchema = type({
|
|
||||||
providerId: "string>=1",
|
|
||||||
issuer: "string>=1",
|
|
||||||
domain: "string>=1",
|
|
||||||
clientId: "string>=1",
|
|
||||||
clientSecret: "string>=1",
|
|
||||||
discoveryEndpoint: "string>=1",
|
|
||||||
linkMatchingEmails: "boolean",
|
|
||||||
});
|
|
||||||
|
|
||||||
type ProviderForm = typeof ssoProviderSchema.infer;
|
|
||||||
type InvitationRole = "member" | "admin" | "owner";
|
type InvitationRole = "member" | "admin" | "owner";
|
||||||
|
|
||||||
export function SsoSettingsSection() {
|
export function SsoSettingsSection() {
|
||||||
const origin = getOrigin();
|
const origin = getOrigin();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { activeOrganization } = useOrganizationContext();
|
const { activeOrganization } = useOrganizationContext();
|
||||||
const [isRegisterProviderDialogOpen, setIsRegisterProviderDialogOpen] = useState(false);
|
|
||||||
const [inviteEmail, setInviteEmail] = useState("");
|
const [inviteEmail, setInviteEmail] = useState("");
|
||||||
const [inviteRole, setInviteRole] = useState<InvitationRole>("member");
|
const [inviteRole, setInviteRole] = useState<InvitationRole>("member");
|
||||||
|
|
||||||
const form = useForm<ProviderForm>({
|
|
||||||
resolver: arktypeResolver(ssoProviderSchema),
|
|
||||||
defaultValues: {
|
|
||||||
providerId: "",
|
|
||||||
issuer: "",
|
|
||||||
domain: "",
|
|
||||||
clientId: "",
|
|
||||||
clientSecret: "",
|
|
||||||
discoveryEndpoint: "",
|
|
||||||
linkMatchingEmails: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data } = useSuspenseQuery({
|
const { data } = useSuspenseQuery({
|
||||||
...getSsoSettingsOptions(),
|
...getSsoSettingsOptions(),
|
||||||
});
|
});
|
||||||
|
|
@ -96,48 +53,6 @@ export function SsoSettingsSection() {
|
||||||
...updateSsoProviderAutoLinkingMutation(),
|
...updateSsoProviderAutoLinkingMutation(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const registerProviderMutation = useMutation({
|
|
||||||
mutationFn: async (formData: ProviderForm) => {
|
|
||||||
if (!activeOrganization) {
|
|
||||||
throw new Error("No active organization found in session");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { error } = await authClient.sso.register({
|
|
||||||
providerId: formData.providerId,
|
|
||||||
issuer: formData.issuer,
|
|
||||||
domain: formData.domain,
|
|
||||||
organizationId: activeOrganization.id,
|
|
||||||
oidcConfig: {
|
|
||||||
clientId: formData.clientId,
|
|
||||||
clientSecret: formData.clientSecret,
|
|
||||||
discoveryEndpoint: formData.discoveryEndpoint,
|
|
||||||
scopes: ["openid", "email", "profile"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
await updateProviderAutoLinkingMutation
|
|
||||||
.mutateAsync({
|
|
||||||
path: { providerId: formData.providerId },
|
|
||||||
body: { enabled: formData.linkMatchingEmails },
|
|
||||||
})
|
|
||||||
.catch((updateError) => {
|
|
||||||
throw new Error(
|
|
||||||
`The provider was created, but we could not save the auto-link setting. ${parseError(updateError)?.message ?? ""}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("SSO provider registered successfully");
|
|
||||||
form.reset();
|
|
||||||
setIsRegisterProviderDialogOpen(false);
|
|
||||||
},
|
|
||||||
onError: (error: unknown) => {
|
|
||||||
toast.error("Failed to register provider", { description: parseError(error)?.message });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteProviderMutation = useMutation({
|
const deleteProviderMutation = useMutation({
|
||||||
...deleteSsoProviderMutation(),
|
...deleteSsoProviderMutation(),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -213,172 +128,13 @@ export function SsoSettingsSection() {
|
||||||
<p className="text-xs text-muted-foreground">Manage identity providers used for organization sign-in.</p>
|
<p className="text-xs text-muted-foreground">Manage identity providers used for organization sign-in.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={isRegisterProviderDialogOpen} onOpenChange={setIsRegisterProviderDialogOpen}>
|
<Button
|
||||||
<DialogTrigger asChild>
|
type="button"
|
||||||
<Button type="button" disabled={!activeOrganization}>
|
disabled={!activeOrganization}
|
||||||
Register new
|
onClick={() => void navigate({ to: "/settings/sso/new" })}
|
||||||
</Button>
|
>
|
||||||
</DialogTrigger>
|
Register new
|
||||||
|
</Button>
|
||||||
<DialogContent className="sm:max-w-2xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Register SSO provider</DialogTitle>
|
|
||||||
<DialogDescription>Connect an OIDC provider for the active organization.</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit((values) => registerProviderMutation.mutate(values))}
|
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
<div className="grid gap-4 @xl:grid-cols-2">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="providerId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Provider ID</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="acme-oidc" disabled={registerProviderMutation.isPending} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Unique identifier used in callback URLs.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="domain"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Organization Domain</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="example.com" disabled={registerProviderMutation.isPending} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Used to discover providers during login.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="issuer"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Issuer URL</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder="https://idp.example.com"
|
|
||||||
disabled={registerProviderMutation.isPending}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="discoveryEndpoint"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Discovery Endpoint</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder="https://idp.example.com/.well-known/openid-configuration"
|
|
||||||
disabled={registerProviderMutation.isPending}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="clientId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Client ID</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder="oidc-client-id"
|
|
||||||
disabled={registerProviderMutation.isPending}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="clientSecret"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Client Secret</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
type="password"
|
|
||||||
placeholder="oidc-client-secret"
|
|
||||||
disabled={registerProviderMutation.isPending}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="linkMatchingEmails"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="rounded-md border p-4">
|
|
||||||
<div className="flex items-start justify-between gap-4">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<FormLabel>Link matching emails to existing accounts</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
If enabled, users who sign in with this provider will automatically access their existing
|
|
||||||
account when the email address matches.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
disabled={registerProviderMutation.isPending}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!activeOrganization ? (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertDescription>
|
|
||||||
No active organization found. Select an organization before registering an SSO provider.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button type="submit" loading={registerProviderMutation.isPending} disabled={!activeOrganization}>
|
|
||||||
Register SSO Provider
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Alert variant="warning">
|
<Alert variant="warning">
|
||||||
|
|
|
||||||
258
app/client/modules/settings/routes/create-sso-provider.tsx
Normal file
258
app/client/modules/settings/routes/create-sso-provider.tsx
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { type } from "arktype";
|
||||||
|
import { ShieldCheck, Plus } from "lucide-react";
|
||||||
|
import { useId } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { updateSsoProviderAutoLinkingMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
import { Button } from "~/client/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "~/client/components/ui/form";
|
||||||
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import { Switch } from "~/client/components/ui/switch";
|
||||||
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
|
import { parseError } from "~/client/lib/errors";
|
||||||
|
import { useOrganizationContext } from "~/client/hooks/use-org-context";
|
||||||
|
|
||||||
|
const ssoProviderSchema = type({
|
||||||
|
providerId: "string>=1",
|
||||||
|
issuer: "string>=1",
|
||||||
|
domain: "string>=1",
|
||||||
|
clientId: "string>=1",
|
||||||
|
clientSecret: "string>=1",
|
||||||
|
discoveryEndpoint: "string>=1",
|
||||||
|
linkMatchingEmails: "boolean",
|
||||||
|
});
|
||||||
|
|
||||||
|
type ProviderForm = typeof ssoProviderSchema.infer;
|
||||||
|
|
||||||
|
export function CreateSsoProviderPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const formId = useId();
|
||||||
|
const { activeOrganization } = useOrganizationContext();
|
||||||
|
|
||||||
|
const form = useForm<ProviderForm>({
|
||||||
|
resolver: arktypeResolver(ssoProviderSchema),
|
||||||
|
defaultValues: {
|
||||||
|
providerId: "",
|
||||||
|
issuer: "",
|
||||||
|
domain: "",
|
||||||
|
clientId: "",
|
||||||
|
clientSecret: "",
|
||||||
|
discoveryEndpoint: "",
|
||||||
|
linkMatchingEmails: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateProviderAutoLinking = useMutation({
|
||||||
|
...updateSsoProviderAutoLinkingMutation(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const registerProvider = useMutation({
|
||||||
|
mutationFn: async (formData: ProviderForm) => {
|
||||||
|
const { error } = await authClient.sso.register({
|
||||||
|
providerId: formData.providerId,
|
||||||
|
issuer: formData.issuer,
|
||||||
|
domain: formData.domain,
|
||||||
|
organizationId: activeOrganization.id,
|
||||||
|
oidcConfig: {
|
||||||
|
clientId: formData.clientId,
|
||||||
|
clientSecret: formData.clientSecret,
|
||||||
|
discoveryEndpoint: formData.discoveryEndpoint,
|
||||||
|
scopes: ["openid", "email", "profile"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
await updateProviderAutoLinking
|
||||||
|
.mutateAsync({
|
||||||
|
path: { providerId: formData.providerId },
|
||||||
|
body: { enabled: formData.linkMatchingEmails },
|
||||||
|
})
|
||||||
|
.catch((updateError) => {
|
||||||
|
throw new Error(
|
||||||
|
`The provider was created, but we could not save the auto-link setting. ${parseError(updateError)?.message ?? ""}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("SSO provider registered successfully");
|
||||||
|
void navigate({ to: "/settings", search: { tab: "users" } });
|
||||||
|
},
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
toast.error("Failed to register provider", { description: parseError(error)?.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
|
||||||
|
<ShieldCheck className="w-5 h-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<CardTitle>Register SSO Provider</CardTitle>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
onSubmit={form.handleSubmit((values) => registerProvider.mutate(values))}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<div className="grid gap-4 @xl:grid-cols-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="providerId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Provider ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="acme-oidc" disabled={registerProvider.isPending} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Unique identifier used in callback URLs.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="domain"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Organization Domain</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="example.com" disabled={registerProvider.isPending} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Used to discover providers during login.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="issuer"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Issuer URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="https://idp.example.com" disabled={registerProvider.isPending} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="discoveryEndpoint"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Discovery Endpoint</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="https://idp.example.com/.well-known/openid-configuration"
|
||||||
|
disabled={registerProvider.isPending}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Client ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="oidc-client-id" disabled={registerProvider.isPending} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientSecret"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Client Secret</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="password"
|
||||||
|
placeholder="oidc-client-secret"
|
||||||
|
disabled={registerProvider.isPending}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="linkMatchingEmails"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="rounded-md border p-4">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<FormLabel>Link matching emails to existing accounts</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
If enabled, users who sign in with this provider will automatically access their existing
|
||||||
|
account when the email address matches.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
disabled={registerProvider.isPending}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => void navigate({ to: "/settings", search: { tab: "users" } })}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" form={formId} loading={registerProvider.isPending}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Register Provider
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,7 @@ import { Route as dashboardBackupsCreateRouteImport } from './routes/(dashboard)
|
||||||
import { Route as authLoginErrorRouteImport } from './routes/(auth)/login.error'
|
import { Route as authLoginErrorRouteImport } from './routes/(auth)/login.error'
|
||||||
import { Route as dashboardRepositoriesRepositoryIdIndexRouteImport } from './routes/(dashboard)/repositories/$repositoryId/index'
|
import { Route as dashboardRepositoriesRepositoryIdIndexRouteImport } from './routes/(dashboard)/repositories/$repositoryId/index'
|
||||||
import { Route as dashboardBackupsBackupIdIndexRouteImport } from './routes/(dashboard)/backups/$backupId/index'
|
import { Route as dashboardBackupsBackupIdIndexRouteImport } from './routes/(dashboard)/backups/$backupId/index'
|
||||||
|
import { Route as dashboardSettingsSsoNewRouteImport } from './routes/(dashboard)/settings/sso/new'
|
||||||
import { Route as dashboardRepositoriesRepositoryIdEditRouteImport } from './routes/(dashboard)/repositories/$repositoryId/edit'
|
import { Route as dashboardRepositoriesRepositoryIdEditRouteImport } from './routes/(dashboard)/repositories/$repositoryId/edit'
|
||||||
import { Route as dashboardRepositoriesRepositoryIdSnapshotIdIndexRouteImport } from './routes/(dashboard)/repositories/$repositoryId/$snapshotId/index'
|
import { Route as dashboardRepositoriesRepositoryIdSnapshotIdIndexRouteImport } from './routes/(dashboard)/repositories/$repositoryId/$snapshotId/index'
|
||||||
import { Route as dashboardRepositoriesRepositoryIdSnapshotIdRestoreRouteImport } from './routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore'
|
import { Route as dashboardRepositoriesRepositoryIdSnapshotIdRestoreRouteImport } from './routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore'
|
||||||
|
|
@ -146,6 +147,11 @@ const dashboardBackupsBackupIdIndexRoute =
|
||||||
path: '/backups/$backupId/',
|
path: '/backups/$backupId/',
|
||||||
getParentRoute: () => dashboardRouteRoute,
|
getParentRoute: () => dashboardRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const dashboardSettingsSsoNewRoute = dashboardSettingsSsoNewRouteImport.update({
|
||||||
|
id: '/settings/sso/new',
|
||||||
|
path: '/settings/sso/new',
|
||||||
|
getParentRoute: () => dashboardRouteRoute,
|
||||||
|
} as any)
|
||||||
const dashboardRepositoriesRepositoryIdEditRoute =
|
const dashboardRepositoriesRepositoryIdEditRoute =
|
||||||
dashboardRepositoriesRepositoryIdEditRouteImport.update({
|
dashboardRepositoriesRepositoryIdEditRouteImport.update({
|
||||||
id: '/repositories/$repositoryId/edit',
|
id: '/repositories/$repositoryId/edit',
|
||||||
|
|
@ -190,6 +196,7 @@ export interface FileRoutesByFullPath {
|
||||||
'/settings/': typeof dashboardSettingsIndexRoute
|
'/settings/': typeof dashboardSettingsIndexRoute
|
||||||
'/volumes/': typeof dashboardVolumesIndexRoute
|
'/volumes/': typeof dashboardVolumesIndexRoute
|
||||||
'/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
|
'/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
|
||||||
|
'/settings/sso/new': typeof dashboardSettingsSsoNewRoute
|
||||||
'/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute
|
'/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute
|
||||||
'/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute
|
'/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute
|
||||||
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
||||||
|
|
@ -215,6 +222,7 @@ export interface FileRoutesByTo {
|
||||||
'/settings': typeof dashboardSettingsIndexRoute
|
'/settings': typeof dashboardSettingsIndexRoute
|
||||||
'/volumes': typeof dashboardVolumesIndexRoute
|
'/volumes': typeof dashboardVolumesIndexRoute
|
||||||
'/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
|
'/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
|
||||||
|
'/settings/sso/new': typeof dashboardSettingsSsoNewRoute
|
||||||
'/backups/$backupId': typeof dashboardBackupsBackupIdIndexRoute
|
'/backups/$backupId': typeof dashboardBackupsBackupIdIndexRoute
|
||||||
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdIndexRoute
|
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdIndexRoute
|
||||||
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
||||||
|
|
@ -243,6 +251,7 @@ export interface FileRoutesById {
|
||||||
'/(dashboard)/settings/': typeof dashboardSettingsIndexRoute
|
'/(dashboard)/settings/': typeof dashboardSettingsIndexRoute
|
||||||
'/(dashboard)/volumes/': typeof dashboardVolumesIndexRoute
|
'/(dashboard)/volumes/': typeof dashboardVolumesIndexRoute
|
||||||
'/(dashboard)/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
|
'/(dashboard)/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
|
||||||
|
'/(dashboard)/settings/sso/new': typeof dashboardSettingsSsoNewRoute
|
||||||
'/(dashboard)/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute
|
'/(dashboard)/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute
|
||||||
'/(dashboard)/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute
|
'/(dashboard)/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute
|
||||||
'/(dashboard)/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
'/(dashboard)/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
||||||
|
|
@ -270,6 +279,7 @@ export interface FileRouteTypes {
|
||||||
| '/settings/'
|
| '/settings/'
|
||||||
| '/volumes/'
|
| '/volumes/'
|
||||||
| '/repositories/$repositoryId/edit'
|
| '/repositories/$repositoryId/edit'
|
||||||
|
| '/settings/sso/new'
|
||||||
| '/backups/$backupId/'
|
| '/backups/$backupId/'
|
||||||
| '/repositories/$repositoryId/'
|
| '/repositories/$repositoryId/'
|
||||||
| '/backups/$backupId/$snapshotId/restore'
|
| '/backups/$backupId/$snapshotId/restore'
|
||||||
|
|
@ -295,6 +305,7 @@ export interface FileRouteTypes {
|
||||||
| '/settings'
|
| '/settings'
|
||||||
| '/volumes'
|
| '/volumes'
|
||||||
| '/repositories/$repositoryId/edit'
|
| '/repositories/$repositoryId/edit'
|
||||||
|
| '/settings/sso/new'
|
||||||
| '/backups/$backupId'
|
| '/backups/$backupId'
|
||||||
| '/repositories/$repositoryId'
|
| '/repositories/$repositoryId'
|
||||||
| '/backups/$backupId/$snapshotId/restore'
|
| '/backups/$backupId/$snapshotId/restore'
|
||||||
|
|
@ -322,6 +333,7 @@ export interface FileRouteTypes {
|
||||||
| '/(dashboard)/settings/'
|
| '/(dashboard)/settings/'
|
||||||
| '/(dashboard)/volumes/'
|
| '/(dashboard)/volumes/'
|
||||||
| '/(dashboard)/repositories/$repositoryId/edit'
|
| '/(dashboard)/repositories/$repositoryId/edit'
|
||||||
|
| '/(dashboard)/settings/sso/new'
|
||||||
| '/(dashboard)/backups/$backupId/'
|
| '/(dashboard)/backups/$backupId/'
|
||||||
| '/(dashboard)/repositories/$repositoryId/'
|
| '/(dashboard)/repositories/$repositoryId/'
|
||||||
| '/(dashboard)/backups/$backupId/$snapshotId/restore'
|
| '/(dashboard)/backups/$backupId/$snapshotId/restore'
|
||||||
|
|
@ -485,6 +497,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof dashboardBackupsBackupIdIndexRouteImport
|
preLoaderRoute: typeof dashboardBackupsBackupIdIndexRouteImport
|
||||||
parentRoute: typeof dashboardRouteRoute
|
parentRoute: typeof dashboardRouteRoute
|
||||||
}
|
}
|
||||||
|
'/(dashboard)/settings/sso/new': {
|
||||||
|
id: '/(dashboard)/settings/sso/new'
|
||||||
|
path: '/settings/sso/new'
|
||||||
|
fullPath: '/settings/sso/new'
|
||||||
|
preLoaderRoute: typeof dashboardSettingsSsoNewRouteImport
|
||||||
|
parentRoute: typeof dashboardRouteRoute
|
||||||
|
}
|
||||||
'/(dashboard)/repositories/$repositoryId/edit': {
|
'/(dashboard)/repositories/$repositoryId/edit': {
|
||||||
id: '/(dashboard)/repositories/$repositoryId/edit'
|
id: '/(dashboard)/repositories/$repositoryId/edit'
|
||||||
path: '/repositories/$repositoryId/edit'
|
path: '/repositories/$repositoryId/edit'
|
||||||
|
|
@ -557,6 +576,7 @@ interface dashboardRouteRouteChildren {
|
||||||
dashboardSettingsIndexRoute: typeof dashboardSettingsIndexRoute
|
dashboardSettingsIndexRoute: typeof dashboardSettingsIndexRoute
|
||||||
dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute
|
dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute
|
||||||
dashboardRepositoriesRepositoryIdEditRoute: typeof dashboardRepositoriesRepositoryIdEditRoute
|
dashboardRepositoriesRepositoryIdEditRoute: typeof dashboardRepositoriesRepositoryIdEditRoute
|
||||||
|
dashboardSettingsSsoNewRoute: typeof dashboardSettingsSsoNewRoute
|
||||||
dashboardBackupsBackupIdIndexRoute: typeof dashboardBackupsBackupIdIndexRoute
|
dashboardBackupsBackupIdIndexRoute: typeof dashboardBackupsBackupIdIndexRoute
|
||||||
dashboardRepositoriesRepositoryIdIndexRoute: typeof dashboardRepositoriesRepositoryIdIndexRoute
|
dashboardRepositoriesRepositoryIdIndexRoute: typeof dashboardRepositoriesRepositoryIdIndexRoute
|
||||||
dashboardBackupsBackupIdSnapshotIdRestoreRoute: typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
dashboardBackupsBackupIdSnapshotIdRestoreRoute: typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
||||||
|
|
@ -579,6 +599,7 @@ const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
|
||||||
dashboardVolumesIndexRoute: dashboardVolumesIndexRoute,
|
dashboardVolumesIndexRoute: dashboardVolumesIndexRoute,
|
||||||
dashboardRepositoriesRepositoryIdEditRoute:
|
dashboardRepositoriesRepositoryIdEditRoute:
|
||||||
dashboardRepositoriesRepositoryIdEditRoute,
|
dashboardRepositoriesRepositoryIdEditRoute,
|
||||||
|
dashboardSettingsSsoNewRoute: dashboardSettingsSsoNewRoute,
|
||||||
dashboardBackupsBackupIdIndexRoute: dashboardBackupsBackupIdIndexRoute,
|
dashboardBackupsBackupIdIndexRoute: dashboardBackupsBackupIdIndexRoute,
|
||||||
dashboardRepositoriesRepositoryIdIndexRoute:
|
dashboardRepositoriesRepositoryIdIndexRoute:
|
||||||
dashboardRepositoriesRepositoryIdIndexRoute,
|
dashboardRepositoriesRepositoryIdIndexRoute,
|
||||||
|
|
|
||||||
22
app/routes/(dashboard)/settings/sso/new.tsx
Normal file
22
app/routes/(dashboard)/settings/sso/new.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { CreateSsoProviderPage } from "~/client/modules/settings/routes/create-sso-provider";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/(dashboard)/settings/sso/new")({
|
||||||
|
component: RouteComponent,
|
||||||
|
staticData: {
|
||||||
|
breadcrumb: () => [{ label: "Settings", href: "/settings" }, { label: "Register SSO Provider" }],
|
||||||
|
},
|
||||||
|
head: () => ({
|
||||||
|
meta: [
|
||||||
|
{ title: "Zerobyte - Register SSO Provider" },
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
content: "Register a new OIDC identity provider for organization sign-in.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <CreateSsoProviderPage />;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue