Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 272 additions & 0 deletions apps/admin/app/(all)/(dashboard)/authentication/oidc-free/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/

import { useState } from "react";
import { isEmpty } from "lodash-es";
import Link from "next/link";
import { useForm } from "react-hook-form";
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import { Button, getButtonStyling } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IFormattedInstanceConfiguration, TInstanceOidcFreeAuthenticationConfigurationKeys } from "@plane/types";
// components
import { CodeBlock } from "@/components/common/code-block";
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
import type { TControllerInputFormField } from "@/components/common/controller-input";
import { ControllerInput } from "@/components/common/controller-input";
import type { TControllerSwitchFormField } from "@/components/common/controller-switch";
import { ControllerSwitch } from "@/components/common/controller-switch";
import type { TCopyField } from "@/components/common/copy-field";
import { CopyField } from "@/components/common/copy-field";
// hooks
import { useInstance } from "@/hooks/store";

type Props = {
config: IFormattedInstanceConfiguration;
};

type OidcFreeConfigFormValues = Record<TInstanceOidcFreeAuthenticationConfigurationKeys, string>;

export function InstanceOidcFreeConfigForm(props: Props) {
const { config } = props;
// states
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
// store hooks
const { updateInstanceConfigurations } = useInstance();
// form data
const {
handleSubmit,
control,
reset,
formState: { errors, isDirty, isSubmitting },
} = useForm<OidcFreeConfigFormValues>({
defaultValues: {
OIDC_FREE_CLIENT_ID: config["OIDC_FREE_CLIENT_ID"],
OIDC_FREE_CLIENT_SECRET: config["OIDC_FREE_CLIENT_SECRET"],
OIDC_FREE_HOST: config["OIDC_FREE_HOST"],
OIDC_FREE_SCOPE: config["OIDC_FREE_SCOPE"],
OIDC_FREE_USERINFO_URL: config["OIDC_FREE_USERINFO_URL"],
OIDC_FREE_TOKEN_URL: config["OIDC_FREE_TOKEN_URL"],
OIDC_FREE_CALLBACK_URI: config["OIDC_FREE_CALLBACK_URI"],
OIDC_FREE_AUTH_URI: config["OIDC_FREE_AUTH_URI"],
ENABLE_OIDC_FREE_SYNC: config["ENABLE_OIDC_FREE_SYNC"] || "0",
},
});

const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : "";

const OIDC_FREE_FORM_FIELDS: TControllerInputFormField[] = [
{
key: "OIDC_FREE_HOST",
type: "text",
label: "Oidc Free Host",
description: (
<>Use the URL of your Oidc Free instance.</>
),
placeholder: "https://<your domain>",
error: Boolean(errors.OIDC_FREE_HOST),
required: true,
},
{
key: "OIDC_FREE_CLIENT_ID",
type: "text",
label: "Client ID",
description: (
<>
blabla
</>
),
placeholder: "70a44354520df8bd9bcd",
error: Boolean(errors.OIDC_FREE_CLIENT_ID),
required: true,
},
{
key: "OIDC_FREE_CLIENT_SECRET",
type: "password",
label: "Client secret",
description: (
<>
blabla
</>
),
placeholder: "9b0050f94ec1b744e32ce79ea4ffacd40d4119cb",
error: Boolean(errors.OIDC_FREE_CLIENT_SECRET),
required: true,
},

{
key: "OIDC_FREE_SCOPE",
type: "text",
label: "",
description: (
<>
blabla
</>
),
placeholder: "placeholder",
error: Boolean(errors.OIDC_FREE_SCOPE),
required: true
},
{
key: "OIDC_FREE_USERINFO_URL",
type: "text",
label: "",
description: (
<>
blabla
</>
),
placeholder: "placeholder",
error: Boolean(errors.OIDC_FREE_USERINFO_URL),
required: true
},
{
key: "OIDC_FREE_TOKEN_URL",
type: "text",
label: "",
description: (
<>
blabla
</>
),
placeholder: "placeholder",
error: Boolean(errors.OIDC_FREE_TOKEN_URL),
required: true
},
{
key: "OIDC_FREE_CALLBACK_URI",
type: "text",
label: "",
description: (
<>
blabla
</>
),
placeholder: "placeholder",
error: Boolean(errors.OIDC_FREE_CALLBACK_URI),
required: true
},
{
key: "OIDC_FREE_AUTH_URI",
type: "text",
label: "",
description: (
<>
blabla
</>
),
placeholder: "placeholder",
error: Boolean(errors.OIDC_FREE_AUTH_URI),
required: true
},
];

const OIDC_FREE_FORM_SWITCH_FIELD: TControllerSwitchFormField<OidcFreeConfigFormValues> = {
name: "ENABLE_OIDC_FREE_SYNC",
label: "Oidc Free",
};

const OIDC_FREE_SERVICE_FIELD: TCopyField[] = [
{
key: "Callback_URI",
label: "Callback URI",
url: `${originURL}/auth/oidc-free/callback/`,
description: (
<>
We will auto-generate this.
</>
),
},
];

const onSubmit = async (formData: OidcFreeConfigFormValues) => {
const payload: Partial<OidcFreeConfigFormValues> = { ...formData };

try {
const response = await updateInstanceConfigurations(payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Done!",
message: "Your Oidc Free authentication is configured. You should test it now.",
});
reset({
OIDC_FREE_CLIENT_ID: response.find((item) => item.key === "OIDC_FREE_CLIENT_ID")?.value,
OIDC_FREE_CLIENT_SECRET: response.find((item) => item.key === "OIDC_FREE_CLIENT_SECRET")?.value,
OIDC_FREE_HOST: response.find((item) => item.key === "OIDC_FREE_HOST")?.value,
OIDC_FREE_SCOPE: response.find((item) => item.key === "OIDC_FREE_SCOPE")?.value,
OIDC_FREE_USERINFO_URL: response.find((item) => item.key === "OIDC_FREE_USERINFO_URL")?.value,
OIDC_FREE_TOKEN_URL: response.find((item) => item.key === "OIDC_FREE_TOKEN_URL")?.value,
OIDC_FREE_CALLBACK_URI: response.find((item) => item.key === "OIDC_FREE_CALLBACK_URI")?.value,
OIDC_FREE_AUTH_URI: response.find((item) => item.key === "OIDC_FREE_AUTH_URI")?.value,
});
} catch (err) {
console.error(err);
}
};

const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if (isDirty) {
e.preventDefault();
setIsDiscardChangesModalOpen(true);
}
};

return (
<>
<ConfirmDiscardModal
isOpen={isDiscardChangesModalOpen}
onDiscardHref="/authentication"
handleClose={() => setIsDiscardChangesModalOpen(false)}
/>
<div className="flex flex-col gap-8">
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
<div className="pt-2.5 text-18 font-medium">Oidc Free-provided details for Plane</div>
{OIDC_FREE_FORM_FIELDS.map((field) => (
<ControllerInput
key={field.key}
control={control}
type={field.type}
name={field.key}
label={field.label}
description={field.description}
placeholder={field.placeholder}
error={field.error}
required={field.required}
/>
))}
<ControllerSwitch control={control} field={OIDC_FREE_FORM_SWITCH_FIELD} />
<div className="flex flex-col gap-1 pt-4">
<div className="flex items-center gap-4">
<Button
variant="primary"
size="lg"
onClick={(e) => void handleSubmit(onSubmit)(e)}
loading={isSubmitting}
disabled={!isDirty}
>
{isSubmitting ? "Saving" : "Save changes"}
</Button>
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
Go back
</Link>
</div>
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 pt-1.5 pb-4">
<div className="pt-2 text-18 font-medium">Plane-provided details for Oidc Free</div>
{OIDC_FREE_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
</div>
</div>
</div>
</div>
</>
);
}
105 changes: 105 additions & 0 deletions apps/admin/app/(all)/(dashboard)/authentication/oidc-free/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/

import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// plane internal packages
import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui";
// assets
import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
// components
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks
import { useInstance } from "@/hooks/store";
// types
import type { Route } from "./+types/page";
// local
import { InstanceOidcFreeConfigForm } from "./form";

const InstanceOidcFreeAuthenticationPage = observer(function InstanceOidcFreeAuthenticationPage() {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// config
const enableOidcFreeConfig = formattedConfig?.IS_OIDC_FREE_ENABLED ?? "";
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());

const updateConfig = async (key: "IS_OIDC_FREE_ENABLED", value: string) => {
setIsSubmitting(true);

const payload = {
[key]: value,
};

const updateConfigPromise = updateInstanceConfigurations(payload);

setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration",
success: {
title: "Configuration saved",
message: () => `Oidc Free authentication is now ${value === "1" ? "active" : "disabled"}.`,
},
error: {
title: "Error",
message: () => "Failed to save configuration",
},
});

await updateConfigPromise
.then(() => {
setIsSubmitting(false);
})
.catch((err) => {
console.error(err);
setIsSubmitting(false);
});
};

const isOidcFreeEnabled = enableOidcFreeConfig === "1";

return (
<PageWrapper
customHeader={
<AuthenticationMethodCard
name="OidcFree"
description="Allow members to log in or sign up to plane with any OIDC provider."
icon={<img src={giteaLogo} height={24} width={24} alt="Gitea Logo" />}
config={
<ToggleSwitch
value={isOidcFreeEnabled}
onChange={() => {
updateConfig("IS_OIDC_FREE_ENABLED", isOidcFreeEnabled ? "0" : "1");
}}
size="sm"
disabled={isSubmitting || !formattedConfig}
/>
}
disabled={isSubmitting || !formattedConfig}
withBorder={false}
/>
}
>
{formattedConfig ? (
<InstanceOidcFreeConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" width="50%" />
</Loader>
)}
</PageWrapper>
);
});
export const meta: Route.MetaFunction = () => [{ title: "Oidc Free Authentication - God Mode" }];

export default InstanceOidcFreeAuthenticationPage;
1 change: 1 addition & 0 deletions apps/admin/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default [
route("authentication/gitlab", "./(all)/(dashboard)/authentication/gitlab/page.tsx"),
route("authentication/google", "./(all)/(dashboard)/authentication/google/page.tsx"),
route("authentication/gitea", "./(all)/(dashboard)/authentication/gitea/page.tsx"),
route("authentication/oidc-free", "./(all)/(dashboard)/authentication/oidc-free/page.tsx"),
route("ai", "./(all)/(dashboard)/ai/page.tsx"),
route("image", "./(all)/(dashboard)/image/page.tsx"),
]),
Expand Down
Loading