diff --git a/ui/admin/app/components/oauth-apps/ConfigureOAuthApp.tsx b/ui/admin/app/components/oauth-apps/ConfigureOAuthApp.tsx new file mode 100644 index 000000000..303bf5b71 --- /dev/null +++ b/ui/admin/app/components/oauth-apps/ConfigureOAuthApp.tsx @@ -0,0 +1,135 @@ +import { SettingsIcon } from "lucide-react"; +import { toast } from "sonner"; +import { mutate } from "swr"; + +import { OAuthAppParams } from "~/lib/model/oauthApps"; +import { OAuthProvider } from "~/lib/model/oauthApps/oauth-helpers"; +import { OauthAppService } from "~/lib/service/api/oauthAppService"; + +import { Button } from "~/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogTitle, + DialogTrigger, +} from "~/components/ui/dialog"; +import { ScrollArea } from "~/components/ui/scroll-area"; +import { + useOAuthAppInfo, + useOAuthAppList, +} from "~/hooks/oauthApps/useOAuthApps"; +import { useAsync } from "~/hooks/useAsync"; +import { useDisclosure } from "~/hooks/useDisclosure"; + +import { OAuthAppForm } from "./OAuthAppForm"; +import { OAuthAppTypeIcon } from "./OAuthAppTypeIcon"; + +export function ConfigureOAuthApp({ type }: { type: OAuthProvider }) { + const spec = useOAuthAppInfo(type); + const { customApp } = spec; + const isEdit = !!customApp; + + const modal = useDisclosure(); + const successModal = useDisclosure(); + + const createApp = useAsync(async (data: OAuthAppParams) => { + await OauthAppService.createOauthApp({ + type, + refName: type, + ...data, + }); + + await mutate(useOAuthAppList.key()); + + modal.onClose(); + successModal.onOpen(); + toast.success(`${spec.displayName} OAuth configuration created`); + }); + + const updateApp = useAsync(async (data: OAuthAppParams) => { + if (!customApp) throw new Error("Custom app not found"); + + await OauthAppService.updateOauthApp(customApp.id, { + type: customApp.type, + refName: customApp.refName, + ...data, + }); + + await mutate(useOAuthAppList.key()); + + modal.onClose(); + successModal.onOpen(); + toast.success(`${spec.displayName} OAuth configuration updated`); + }); + + return ( + <> + + + + + + + + + Configure {spec.displayName} OAuth App + + + + + + + + + + + + + + Successfully Configured {spec.displayName} OAuth App + + + + Otto will now use your custom {spec.displayName} OAuth + app to authenticate users. + + + + + + + + + + + ); +} diff --git a/ui/admin/app/components/oauth-apps/CreateOauthApp.tsx b/ui/admin/app/components/oauth-apps/CreateOauthApp.tsx deleted file mode 100644 index 8b06e9cc8..000000000 --- a/ui/admin/app/components/oauth-apps/CreateOauthApp.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { DialogDescription } from "@radix-ui/react-dialog"; -import { SettingsIcon } from "lucide-react"; -import { toast } from "sonner"; -import { mutate } from "swr"; - -import { OAuthAppParams } from "~/lib/model/oauthApps"; -import { OAuthProvider } from "~/lib/model/oauthApps/oauth-helpers"; -import { OauthAppService } from "~/lib/service/api/oauthAppService"; - -import { Button } from "~/components/ui/button"; -import { - Dialog, - DialogContent, - DialogTitle, - DialogTrigger, -} from "~/components/ui/dialog"; -import { ScrollArea } from "~/components/ui/scroll-area"; -import { - useOAuthAppInfo, - useOAuthAppList, -} from "~/hooks/oauthApps/useOAuthApps"; -import { useAsync } from "~/hooks/useAsync"; -import { useDisclosure } from "~/hooks/useDisclosure"; - -import { OAuthAppForm } from "./OAuthAppForm"; -import { OAuthAppTypeIcon } from "./OAuthAppTypeIcon"; - -export function CreateOauthApp({ type }: { type: OAuthProvider }) { - const spec = useOAuthAppInfo(type); - const modal = useDisclosure(); - - const createApp = useAsync(async (data: OAuthAppParams) => { - await OauthAppService.createOauthApp({ - type, - refName: type, - ...data, - }); - - await mutate(useOAuthAppList.key()); - - modal.onClose(); - toast.success(`${spec.displayName} OAuth app created`); - }); - - return ( - - - - - - - - - Configure {spec.displayName} OAuth App - - - - - - - - - - ); -} diff --git a/ui/admin/app/components/oauth-apps/DeleteOAuthApp.tsx b/ui/admin/app/components/oauth-apps/DeleteOAuthApp.tsx index a31889f52..6d85730b7 100644 --- a/ui/admin/app/components/oauth-apps/DeleteOAuthApp.tsx +++ b/ui/admin/app/components/oauth-apps/DeleteOAuthApp.tsx @@ -1,7 +1,7 @@ -import { TrashIcon } from "lucide-react"; import { toast } from "sonner"; import { mutate } from "swr"; +import { OAuthProvider } from "~/lib/model/oauthApps/oauth-helpers"; import { OauthAppService } from "~/lib/service/api/oauthAppService"; import { ConfirmationDialog } from "~/components/composed/ConfirmationDialog"; @@ -13,21 +13,28 @@ import { TooltipProvider, TooltipTrigger, } from "~/components/ui/tooltip"; -import { useOAuthAppList } from "~/hooks/oauthApps/useOAuthApps"; +import { + useOAuthAppInfo, + useOAuthAppList, +} from "~/hooks/oauthApps/useOAuthApps"; import { useAsync } from "~/hooks/useAsync"; export function DeleteOAuthApp({ id, disableTooltip, + type, }: { id: string; disableTooltip?: boolean; + type: OAuthProvider; }) { + const spec = useOAuthAppInfo(type); + const deleteOAuthApp = useAsync(async () => { await OauthAppService.deleteOauthApp(id); await mutate(useOAuthAppList.key()); - toast.success("OAuth app deleted"); + toast.success(`${spec.displayName} OAuth configuration deleted`); }); return ( @@ -35,12 +42,12 @@ export function DeleteOAuthApp({ @@ -51,10 +58,9 @@ export function DeleteOAuthApp({ > {deleteOAuthApp.isLoading ? ( - ) : ( - - )} - Delete OAuth App + ) : null} + Reset {spec.displayName} OAuth to use Acorn + Gateway diff --git a/ui/admin/app/components/oauth-apps/EditOAuthApp.tsx b/ui/admin/app/components/oauth-apps/EditOAuthApp.tsx deleted file mode 100644 index fbbb239d3..000000000 --- a/ui/admin/app/components/oauth-apps/EditOAuthApp.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { GearIcon } from "@radix-ui/react-icons"; -import { toast } from "sonner"; -import { mutate } from "swr"; - -import { OAuthAppParams } from "~/lib/model/oauthApps"; -import { OAuthProvider } from "~/lib/model/oauthApps/oauth-helpers"; -import { OauthAppService } from "~/lib/service/api/oauthAppService"; - -import { Button } from "~/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogTitle, - DialogTrigger, -} from "~/components/ui/dialog"; -import { - useOAuthAppInfo, - useOAuthAppList, -} from "~/hooks/oauthApps/useOAuthApps"; -import { useAsync } from "~/hooks/useAsync"; -import { useDisclosure } from "~/hooks/useDisclosure"; - -import { OAuthAppForm } from "./OAuthAppForm"; -import { OAuthAppTypeIcon } from "./OAuthAppTypeIcon"; - -export function EditOAuthApp({ type }: { type: OAuthProvider }) { - const spec = useOAuthAppInfo(type); - const modal = useDisclosure(); - - const { customApp } = spec; - - const updateApp = useAsync(async (data: OAuthAppParams) => { - if (!customApp) return; - - await OauthAppService.updateOauthApp(customApp.id, { - type: customApp.type, - refName: customApp.refName, - ...data, - }); - await mutate(useOAuthAppList.key()); - modal.onClose(); - toast.success(`${spec.displayName} OAuth app updated`); - }); - - if (!customApp) return null; - - return ( - - - - - - - - Edit {spec.displayName}{" "} - OAuth Configuration - - - - - - - - ); -} diff --git a/ui/admin/app/components/oauth-apps/OAuthAppDetail.tsx b/ui/admin/app/components/oauth-apps/OAuthAppDetail.tsx index ffc7b7fb8..87e638083 100644 --- a/ui/admin/app/components/oauth-apps/OAuthAppDetail.tsx +++ b/ui/admin/app/components/oauth-apps/OAuthAppDetail.tsx @@ -19,9 +19,8 @@ import { } from "~/components/ui/dialog"; import { useOAuthAppInfo } from "~/hooks/oauthApps/useOAuthApps"; -import { CreateOauthApp } from "./CreateOauthApp"; +import { ConfigureOAuthApp } from "./ConfigureOAuthApp"; import { DeleteOAuthApp } from "./DeleteOAuthApp"; -import { EditOAuthApp } from "./EditOAuthApp"; import { OAuthAppTypeIcon } from "./OAuthAppTypeIcon"; export function OAuthAppDetail({ @@ -71,16 +70,16 @@ function EmptyContent({ spec }: { spec: OAuthAppSpec }) { return (
- {spec.displayName} OAuth is automatically being handled by the - Acorn Gateway + {spec.displayName} OAuth is currently enabled. No action is + needed here. - If you would like Otto to use your own custom {spec.displayName}{" "} - OAuth App, you can configure it by clicking the button below. + You can also configure your own {spec.displayName} OAuth by + clicking the button below. - +
); } @@ -89,7 +88,9 @@ function Content({ app, spec }: { app: OAuthApp; spec: OAuthAppSpec }) { return (
- You have a custom configuration for {spec.displayName} OAuth. + Otto only supports one custom {spec.displayName} OAuth. If you + need to use a different configuration, you can replace the + current configuration with a new one. @@ -109,8 +110,8 @@ function Content({ app, spec }: { app: OAuthApp; spec: OAuthAppSpec }) { ****************
- - + + ); } diff --git a/ui/admin/app/components/oauth-apps/OAuthAppForm.tsx b/ui/admin/app/components/oauth-apps/OAuthAppForm.tsx index db5676343..228450250 100644 --- a/ui/admin/app/components/oauth-apps/OAuthAppForm.tsx +++ b/ui/admin/app/components/oauth-apps/OAuthAppForm.tsx @@ -28,8 +28,6 @@ type OAuthAppFormProps = { export function OAuthAppForm({ type, onSubmit, isLoading }: OAuthAppFormProps) { const spec = useOAuthAppInfo(type); - const isEdit = !!spec.customApp; - const fields = useMemo(() => { return Object.entries(spec.schema.shape).map(([key]) => ({ key: key as keyof OAuthAppParams, @@ -37,20 +35,11 @@ export function OAuthAppForm({ type, onSubmit, isLoading }: OAuthAppFormProps) { }, [spec.schema]); const defaultValues = useMemo(() => { - const app = spec.customApp; - return fields.reduce((acc, { key }) => { - acc[key] = app?.[key] ?? ""; - - // if editing, use placeholder to show secret value exists - // use a uuid to ensure it never collides with a real secret - if (key === "clientSecret" && isEdit) { - acc.clientSecret = SECRET_PLACEHOLDER; - } - + acc[key] = ""; return acc; }, {} as OAuthAppParams); - }, [fields, spec.customApp, isEdit]); + }, [fields]); const form = useForm({ defaultValues, @@ -61,17 +50,7 @@ export function OAuthAppForm({ type, onSubmit, isLoading }: OAuthAppFormProps) { form.reset(defaultValues); }, [defaultValues, form]); - const handleSubmit = form.handleSubmit((data) => { - const { clientSecret, ...rest } = data; - - // if the user skips editing the client secret, we don't want to submit an empty string - // because that will clear it out on the server - if (isEdit && clientSecret === SECRET_PLACEHOLDER) { - onSubmit(rest); - } else { - onSubmit(data); - } - }); + const handleSubmit = form.handleSubmit(onSubmit); return (
@@ -109,11 +88,7 @@ export function OAuthAppForm({ type, onSubmit, isLoading }: OAuthAppFormProps) { name={step.input as keyof OAuthAppParams} label={step.label} control={form.control} - {...(step.input === "clientSecret" && { - onBlur: onBlurClientSecret, - onFocus: onFocusClientSecret, - type: "password", - })} + type={step.inputType} /> ); } @@ -122,26 +97,4 @@ export function OAuthAppForm({ type, onSubmit, isLoading }: OAuthAppFormProps) { } } } - - function onBlurClientSecret() { - if (!isEdit) return; - - const { clientSecret } = form.getValues(); - - if (!clientSecret) { - form.setValue("clientSecret", SECRET_PLACEHOLDER); - } - } - - function onFocusClientSecret() { - if (!isEdit) return; - - const { clientSecret } = form.getValues(); - - if (clientSecret === SECRET_PLACEHOLDER) { - form.setValue("clientSecret", ""); - } - } } - -const SECRET_PLACEHOLDER = crypto.randomUUID(); diff --git a/ui/admin/app/components/oauth-apps/OAuthAppList.tsx b/ui/admin/app/components/oauth-apps/OAuthAppList.tsx index 0302dd848..d7e854394 100644 --- a/ui/admin/app/components/oauth-apps/OAuthAppList.tsx +++ b/ui/admin/app/components/oauth-apps/OAuthAppList.tsx @@ -9,11 +9,12 @@ export function OAuthAppList() { return (
- Pre-configured OAuth Apps + Supported OAuth Apps + - These apps are pre-configured and ready to use. For the most - part, you should not need to configure any additional OAuth - apps. + These are the currently supported OAuth apps for Otto. These + are here to allow users to access the following services via + tools.
diff --git a/ui/admin/app/components/oauth-apps/OAuthAppTile.tsx b/ui/admin/app/components/oauth-apps/OAuthAppTile.tsx index 3c3e1d94c..da83fc55b 100644 --- a/ui/admin/app/components/oauth-apps/OAuthAppTile.tsx +++ b/ui/admin/app/components/oauth-apps/OAuthAppTile.tsx @@ -16,7 +16,7 @@ export function OAuthAppTile({ type }: { type: OAuthProvider }) { const { displayName } = info; return ( - + {displayName} & { + classNames?: { content?: string; overlay?: string }; + hideCloseButton?: boolean; +}; + const DialogContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & { - classNames?: { - content?: string; - overlay?: string; - }; - } ->(({ className, children, classNames = {}, ...props }, ref) => ( - - - - {children} - - - Close - - - -)); + DialogContentProps +>((props, ref) => { + const { + className, + children, + classNames = {}, + hideCloseButton = false, + ...dialogProps + } = props; + + return ( + + + + {children} + {!hideCloseButton && ( + + + Close + + )} + + + ); +}); DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ diff --git a/ui/admin/app/lib/model/oauthApps/github.ts b/ui/admin/app/lib/model/oauthApps/github.ts index 273261b7c..7e5d6bd74 100644 --- a/ui/admin/app/lib/model/oauthApps/github.ts +++ b/ui/admin/app/lib/model/oauthApps/github.ts @@ -1,32 +1,32 @@ import { z } from "zod"; +import { BaseUrl } from "~/lib/routers/baseRouter"; import { assetUrl } from "~/lib/utils"; import { OAuthAppSpec, OAuthFormStep, getOAuthLinks } from "./oauth-helpers"; const schema = z.object({ - clientID: z.string(), - clientSecret: z.string(), + clientID: z.string().min(1, "Client ID is required"), + clientSecret: z.string().min(1, "Client Secret is required"), }); const steps: OAuthFormStep[] = [ { type: "markdown", - text: "### Step 1: Create a new GitHub OAuth App\n", + text: + "### Step 1: Create a new GitHub OAuth App\n" + + "1. In [GitHub's Developer Settings](https://github.com/settings/developers), select `New OAuth App`.\n" + + "2. Specify an `Application name`\n" + + "3. Fill in the `Homepage URL` with the link below\n", }, { - type: "markdown", - text: - "#### If you haven't already, create a new GitHub OAuth App\n" + - "1. Navigate to [GitHub's Developer Settings](https://github.com/settings/developers) and select `New OAuth App`.\n" + - "2. The form will prompt you for an `Authorization callback Url` Make sure to use the link below: \n\n", + type: "copy", + text: BaseUrl, }, + { type: "markdown", - text: - "#### If you already have a github OAuth app created\n" + - "1. you can edit it by going to [Github's Developer Settings](https://github.com/settings/developers), and selecting `Edit` on your OAuth App\n" + - "2. Near the bottom is the `Authorization callback URL` field. Make sure it matches the link below: \n\n", + text: "4. Fill in the `Authorization callback URL` with the link below\n", }, { type: "copy", @@ -35,11 +35,23 @@ const steps: OAuthFormStep[] = [ { type: "markdown", text: - "### Step 2: Register OAuth App in Otto\n" + - "Once you've created your OAuth App in GitHub, copy the client ID and client secret into this form", + "5. Click `Register application` to create the OAuth app. It will now take you to the OAuth app's settings page.\n" + + "### Step 2: Register GitHub OAuth in Otto\n" + + "1. Locate the `Client ID` on the OAuth app's settings page and copy the `Client ID` into the input below\n", }, { type: "input", input: "clientID", label: "Client ID" }, - { type: "input", input: "clientSecret", label: "Client Secret" }, + { + type: "markdown", + text: + "2. Locate `Client Secrets` on the OAuth app's settings page, click `Generate new client secret`, and complete the authorization flow to generate a new secret.\n" + + "3. Copy the newly generated `Client Secret` into the input below.", + }, + { + type: "input", + input: "clientSecret", + label: "Client Secret", + inputType: "password", + }, ]; export const GitHubOAuthApp = { diff --git a/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts b/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts index 143f0bcb5..3a765b84d 100644 --- a/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts +++ b/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts @@ -9,7 +9,12 @@ export type OAuthProvider = (typeof OAuthProvider)[keyof typeof OAuthProvider]; export type OAuthFormStep> = | { type: "markdown"; text: string; copy?: string } - | { type: "input"; input: keyof T; label: string } + | { + type: "input"; + input: keyof T; + label: string; + inputType?: "password" | "text"; + } | { type: "copy"; text: string }; export type OAuthAppSpec = { @@ -23,8 +28,8 @@ export type OAuthAppSpec = { export function getOAuthLinks(type: OAuthProvider) { return { - authorizeURL: `${apiBaseUrl}/app/oauth/authorize/${type}`, - redirectURL: `${apiBaseUrl}/app/oauth/callback/${type}`, - refreshURL: `${apiBaseUrl}/app/oauth/refresh/${type}`, + authorizeURL: `${apiBaseUrl}/app-oauth/authorize/${type}`, + redirectURL: `${apiBaseUrl}/app-oauth/callback/${type}`, + refreshURL: `${apiBaseUrl}/app-oauth/refresh/${type}`, }; } diff --git a/ui/admin/app/lib/routers/baseRouter.ts b/ui/admin/app/lib/routers/baseRouter.ts new file mode 100644 index 000000000..ee85ee7bc --- /dev/null +++ b/ui/admin/app/lib/routers/baseRouter.ts @@ -0,0 +1 @@ +export const BaseUrl = "http://localhost:3000";