mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
feat: add GitLab OAuth client (#4692)
This commit is contained in:
parent
c24be25024
commit
99e1963d9b
59
admin/app/authentication/components/gitlab-config.tsx
Normal file
59
admin/app/authentication/components/gitlab-config.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import Link from "next/link";
|
||||||
|
// icons
|
||||||
|
import { Settings2 } from "lucide-react";
|
||||||
|
// types
|
||||||
|
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||||
|
// ui
|
||||||
|
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
// hooks
|
||||||
|
import { useInstance } from "@/hooks/store";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
disabled: boolean;
|
||||||
|
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GitlabConfiguration: React.FC<Props> = observer((props) => {
|
||||||
|
const { disabled, updateConfig } = props;
|
||||||
|
// store
|
||||||
|
const { formattedConfig } = useInstance();
|
||||||
|
// derived values
|
||||||
|
const enableGitlabConfig = formattedConfig?.IS_GITLAB_ENABLED ?? "";
|
||||||
|
const isGitlabConfigured = !!formattedConfig?.GITLAB_CLIENT_ID && !!formattedConfig?.GITLAB_CLIENT_SECRET;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isGitlabConfigured ? (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Link href="/authentication/gitlab" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
<ToggleSwitch
|
||||||
|
value={Boolean(parseInt(enableGitlabConfig))}
|
||||||
|
onChange={() => {
|
||||||
|
Boolean(parseInt(enableGitlabConfig)) === true
|
||||||
|
? updateConfig("IS_GITLAB_ENABLED", "0")
|
||||||
|
: updateConfig("IS_GITLAB_ENABLED", "1");
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
href="/authentication/gitlab"
|
||||||
|
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
|
||||||
|
>
|
||||||
|
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
|
||||||
|
Configure
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -1,5 +1,6 @@
|
|||||||
export * from "./email-config-switch";
|
export * from "./email-config-switch";
|
||||||
export * from "./password-config-switch";
|
export * from "./password-config-switch";
|
||||||
export * from "./authentication-method-card";
|
export * from "./authentication-method-card";
|
||||||
|
export * from "./gitlab-config";
|
||||||
export * from "./github-config";
|
export * from "./github-config";
|
||||||
export * from "./google-config";
|
export * from "./google-config";
|
||||||
|
212
admin/app/authentication/gitlab/form.tsx
Normal file
212
admin/app/authentication/gitlab/form.tsx
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
import { FC, useState } from "react";
|
||||||
|
import isEmpty from "lodash/isEmpty";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
// types
|
||||||
|
import { IFormattedInstanceConfiguration, TInstanceGitlabAuthenticationConfigurationKeys } from "@plane/types";
|
||||||
|
// ui
|
||||||
|
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
|
||||||
|
// components
|
||||||
|
import {
|
||||||
|
ConfirmDiscardModal,
|
||||||
|
ControllerInput,
|
||||||
|
CopyField,
|
||||||
|
TControllerInputFormField,
|
||||||
|
TCopyField,
|
||||||
|
} from "@/components/common";
|
||||||
|
// helpers
|
||||||
|
import { API_BASE_URL, cn } from "@/helpers/common.helper";
|
||||||
|
// hooks
|
||||||
|
import { useInstance } from "@/hooks/store";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
config: IFormattedInstanceConfiguration;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GitlabConfigFormValues = Record<TInstanceGitlabAuthenticationConfigurationKeys, string>;
|
||||||
|
|
||||||
|
export const InstanceGitlabConfigForm: FC<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<GitlabConfigFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
GITLAB_HOST: config["GITLAB_HOST"],
|
||||||
|
GITLAB_CLIENT_ID: config["GITLAB_CLIENT_ID"],
|
||||||
|
GITLAB_CLIENT_SECRET: config["GITLAB_CLIENT_SECRET"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : "";
|
||||||
|
|
||||||
|
const GITLAB_FORM_FIELDS: TControllerInputFormField[] = [
|
||||||
|
{
|
||||||
|
key: "GITLAB_HOST",
|
||||||
|
type: "text",
|
||||||
|
label: "Host",
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
This is the <b>GitLab host</b> to use for login, <b>including scheme</b>.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
placeholder: "https://gitlab.com",
|
||||||
|
error: Boolean(errors.GITLAB_HOST),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "GITLAB_CLIENT_ID",
|
||||||
|
type: "text",
|
||||||
|
label: "Application ID",
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Get this from your{" "}
|
||||||
|
<a
|
||||||
|
tabIndex={-1}
|
||||||
|
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||||
|
target="_blank"
|
||||||
|
className="text-custom-primary-100 hover:underline"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
GitLab OAuth application settings
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
placeholder: "c2ef2e7fc4e9d15aa7630f5637d59e8e4a27ff01dceebdb26b0d267b9adcf3c3",
|
||||||
|
error: Boolean(errors.GITLAB_CLIENT_ID),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "GITLAB_CLIENT_SECRET",
|
||||||
|
type: "password",
|
||||||
|
label: "Secret",
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
The client secret is also found in your{" "}
|
||||||
|
<a
|
||||||
|
tabIndex={-1}
|
||||||
|
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||||
|
target="_blank"
|
||||||
|
className="text-custom-primary-100 hover:underline"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
GitLab OAuth application settings
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
placeholder: "gloas-f79cfa9a03c97f6ffab303177a5a6778a53c61e3914ba093412f68a9298a1b28",
|
||||||
|
error: Boolean(errors.GITLAB_CLIENT_SECRET),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const GITLAB_SERVICE_FIELD: TCopyField[] = [
|
||||||
|
{
|
||||||
|
key: "Callback_URL",
|
||||||
|
label: "Callback URL",
|
||||||
|
url: `${originURL}/auth/gitlab/callback/`,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
We will auto-generate this. Paste this into the <b>Redirect URI</b> field of your{" "}
|
||||||
|
<a
|
||||||
|
tabIndex={-1}
|
||||||
|
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||||
|
target="_blank"
|
||||||
|
className="text-custom-primary-100 hover:underline"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
GitLab OAuth application
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const onSubmit = async (formData: GitlabConfigFormValues) => {
|
||||||
|
const payload: Partial<GitlabConfigFormValues> = { ...formData };
|
||||||
|
|
||||||
|
await updateInstanceConfigurations(payload)
|
||||||
|
.then((response = []) => {
|
||||||
|
setToast({
|
||||||
|
type: TOAST_TYPE.SUCCESS,
|
||||||
|
title: "Success",
|
||||||
|
message: "GitLab Configuration Settings updated successfully",
|
||||||
|
});
|
||||||
|
reset({
|
||||||
|
GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value,
|
||||||
|
GITLAB_CLIENT_ID: response.find((item) => item.key === "GITLAB_CLIENT_ID")?.value,
|
||||||
|
GITLAB_CLIENT_SECRET: response.find((item) => item.key === "GITLAB_CLIENT_SECRET")?.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 grid-cols-2 gap-x-12 gap-y-8 w-full">
|
||||||
|
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1">
|
||||||
|
<div className="pt-2 text-xl font-medium">Configuration</div>
|
||||||
|
{GITLAB_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}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div className="flex flex-col gap-1 pt-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
|
||||||
|
{isSubmitting ? "Saving..." : "Save changes"}
|
||||||
|
</Button>
|
||||||
|
<Link
|
||||||
|
href="/authentication"
|
||||||
|
className={cn(getButtonStyling("link-neutral", "md"), "font-medium")}
|
||||||
|
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 px-6 py-4 my-2 bg-custom-background-80/60 rounded-lg">
|
||||||
|
<div className="pt-2 text-xl font-medium">Service provider details</div>
|
||||||
|
{GITLAB_SERVICE_FIELD.map((field) => (
|
||||||
|
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
101
admin/app/authentication/gitlab/page.tsx
Normal file
101
admin/app/authentication/gitlab/page.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import Image from "next/image";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
|
||||||
|
// components
|
||||||
|
import { PageHeader } from "@/components/core";
|
||||||
|
// hooks
|
||||||
|
import { useInstance } from "@/hooks/store";
|
||||||
|
// icons
|
||||||
|
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
|
||||||
|
// local components
|
||||||
|
import { AuthenticationMethodCard } from "../components";
|
||||||
|
import { InstanceGitlabConfigForm } from "./form";
|
||||||
|
|
||||||
|
const InstanceGitlabAuthenticationPage = observer(() => {
|
||||||
|
// store
|
||||||
|
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
|
||||||
|
// state
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||||
|
// config
|
||||||
|
const enableGitlabConfig = formattedConfig?.IS_GITLAB_ENABLED ?? "";
|
||||||
|
|
||||||
|
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||||
|
|
||||||
|
const updateConfig = async (key: "IS_GITLAB_ENABLED", value: string) => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||||
|
|
||||||
|
setPromiseToast(updateConfigPromise, {
|
||||||
|
loading: "Saving Configuration...",
|
||||||
|
success: {
|
||||||
|
title: "Configuration saved",
|
||||||
|
message: () => `GitLab authentication is now ${value ? "active" : "disabled"}.`,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: "Error",
|
||||||
|
message: () => "Failed to save configuration",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateConfigPromise
|
||||||
|
.then(() => {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
setIsSubmitting(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title="Authentication - God Mode" />
|
||||||
|
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||||
|
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||||
|
<AuthenticationMethodCard
|
||||||
|
name="GitLab"
|
||||||
|
description="Allow members to login or sign up to plane with their GitLab accounts."
|
||||||
|
icon={<Image src={GitlabLogo} height={24} width={24} alt="GitLab Logo" />}
|
||||||
|
config={
|
||||||
|
<ToggleSwitch
|
||||||
|
value={Boolean(parseInt(enableGitlabConfig))}
|
||||||
|
onChange={() => {
|
||||||
|
Boolean(parseInt(enableGitlabConfig)) === true
|
||||||
|
? updateConfig("IS_GITLAB_ENABLED", "0")
|
||||||
|
: updateConfig("IS_GITLAB_ENABLED", "1");
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
disabled={isSubmitting || !formattedConfig}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
disabled={isSubmitting || !formattedConfig}
|
||||||
|
withBorder={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md p-4">
|
||||||
|
{formattedConfig ? (
|
||||||
|
<InstanceGitlabConfigForm 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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default InstanceGitlabAuthenticationPage;
|
@ -17,12 +17,14 @@ import { useInstance } from "@/hooks/store";
|
|||||||
// images
|
// images
|
||||||
import githubLightModeImage from "@/public/logos/github-black.png";
|
import githubLightModeImage from "@/public/logos/github-black.png";
|
||||||
import githubDarkModeImage from "@/public/logos/github-white.png";
|
import githubDarkModeImage from "@/public/logos/github-white.png";
|
||||||
|
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
|
||||||
import GoogleLogo from "@/public/logos/google-logo.svg";
|
import GoogleLogo from "@/public/logos/google-logo.svg";
|
||||||
// local components
|
// local components
|
||||||
import {
|
import {
|
||||||
AuthenticationMethodCard,
|
AuthenticationMethodCard,
|
||||||
EmailCodesConfiguration,
|
EmailCodesConfiguration,
|
||||||
PasswordLoginConfiguration,
|
PasswordLoginConfiguration,
|
||||||
|
GitlabConfiguration,
|
||||||
GithubConfiguration,
|
GithubConfiguration,
|
||||||
GoogleConfiguration,
|
GoogleConfiguration,
|
||||||
} from "./components";
|
} from "./components";
|
||||||
@ -116,6 +118,13 @@ const InstanceAuthenticationPage = observer(() => {
|
|||||||
),
|
),
|
||||||
config: <GithubConfiguration disabled={isSubmitting} updateConfig={updateConfig} />,
|
config: <GithubConfiguration disabled={isSubmitting} updateConfig={updateConfig} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "gitlab",
|
||||||
|
name: "GitLab",
|
||||||
|
description: "Allow members to login or sign up to plane with their GitLab accounts.",
|
||||||
|
icon: <Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
|
||||||
|
config: <GitlabConfiguration disabled={isSubmitting} updateConfig={updateConfig} />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -31,6 +31,8 @@ export const InstanceHeader: FC = observer(() => {
|
|||||||
return "Google";
|
return "Google";
|
||||||
case "github":
|
case "github":
|
||||||
return "Github";
|
return "Github";
|
||||||
|
case "gitlab":
|
||||||
|
return "GitLab";
|
||||||
default:
|
default:
|
||||||
return pathName.toUpperCase();
|
return pathName.toUpperCase();
|
||||||
}
|
}
|
||||||
|
1
admin/public/logos/gitlab-logo.svg
Normal file
1
admin/public/logos/gitlab-logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="80 80 220 220"><defs><style>.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style></defs><g id="LOGO"><path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-2" d="M282.83,170.73l-.27-.69a88.3,88.3,0,0,0-35.15,15.8L190,229.25c19.55,14.79,36.57,27.64,36.57,27.64l40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-3" d="M153.43,256.89l19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91S209.55,244,190,229.25C170.45,244,153.43,256.89,153.43,256.89Z"/><path class="cls-2" d="M132.58,185.84A88.19,88.19,0,0,0,97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82s17-12.85,36.57-27.64Z"/></g></svg>
|
After Width: | Height: | Size: 1.0 KiB |
0
apiserver/bin/docker-entrypoint-beat.sh
Normal file → Executable file
0
apiserver/bin/docker-entrypoint-beat.sh
Normal file → Executable file
0
apiserver/bin/docker-entrypoint-migrator.sh
Normal file → Executable file
0
apiserver/bin/docker-entrypoint-migrator.sh
Normal file → Executable file
@ -33,10 +33,13 @@ AUTHENTICATION_ERROR_CODES = {
|
|||||||
"EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN": 5100,
|
"EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN": 5100,
|
||||||
"EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP": 5102,
|
"EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP": 5102,
|
||||||
# Oauth
|
# Oauth
|
||||||
|
"OAUTH_NOT_CONFIGURED": 5104,
|
||||||
"GOOGLE_NOT_CONFIGURED": 5105,
|
"GOOGLE_NOT_CONFIGURED": 5105,
|
||||||
"GITHUB_NOT_CONFIGURED": 5110,
|
"GITHUB_NOT_CONFIGURED": 5110,
|
||||||
|
"GITLAB_NOT_CONFIGURED": 5111,
|
||||||
"GOOGLE_OAUTH_PROVIDER_ERROR": 5115,
|
"GOOGLE_OAUTH_PROVIDER_ERROR": 5115,
|
||||||
"GITHUB_OAUTH_PROVIDER_ERROR": 5120,
|
"GITHUB_OAUTH_PROVIDER_ERROR": 5120,
|
||||||
|
"GITLAB_OAUTH_PROVIDER_ERROR": 5121,
|
||||||
# Reset Password
|
# Reset Password
|
||||||
"INVALID_PASSWORD_TOKEN": 5125,
|
"INVALID_PASSWORD_TOKEN": 5125,
|
||||||
"EXPIRED_PASSWORD_TOKEN": 5130,
|
"EXPIRED_PASSWORD_TOKEN": 5130,
|
||||||
|
@ -62,11 +62,7 @@ class OauthAdapter(Adapter):
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
except requests.RequestException:
|
except requests.RequestException:
|
||||||
code = (
|
code = self._provider_error_code()
|
||||||
"GOOGLE_OAUTH_PROVIDER_ERROR"
|
|
||||||
if self.provider == "google"
|
|
||||||
else "GITHUB_OAUTH_PROVIDER_ERROR"
|
|
||||||
)
|
|
||||||
raise AuthenticationException(
|
raise AuthenticationException(
|
||||||
error_code=AUTHENTICATION_ERROR_CODES[code],
|
error_code=AUTHENTICATION_ERROR_CODES[code],
|
||||||
error_message=str(code),
|
error_message=str(code),
|
||||||
@ -83,8 +79,12 @@ class OauthAdapter(Adapter):
|
|||||||
except requests.RequestException:
|
except requests.RequestException:
|
||||||
if self.provider == "google":
|
if self.provider == "google":
|
||||||
code = "GOOGLE_OAUTH_PROVIDER_ERROR"
|
code = "GOOGLE_OAUTH_PROVIDER_ERROR"
|
||||||
if self.provider == "github":
|
elif self.provider == "github":
|
||||||
code = "GITHUB_OAUTH_PROVIDER_ERROR"
|
code = "GITHUB_OAUTH_PROVIDER_ERROR"
|
||||||
|
elif self.provider == "gitlab":
|
||||||
|
code = "GITLAB_OAUTH_PROVIDER_ERROR"
|
||||||
|
else:
|
||||||
|
code = "OAUTH_NOT_CONFIGURED"
|
||||||
|
|
||||||
raise AuthenticationException(
|
raise AuthenticationException(
|
||||||
error_code=AUTHENTICATION_ERROR_CODES[code],
|
error_code=AUTHENTICATION_ERROR_CODES[code],
|
||||||
|
145
apiserver/plane/authentication/provider/oauth/gitlab.py
Normal file
145
apiserver/plane/authentication/provider/oauth/gitlab.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
# Python imports
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.authentication.adapter.oauth import OauthAdapter
|
||||||
|
from plane.license.utils.instance_value import get_configuration_value
|
||||||
|
from plane.authentication.adapter.error import (
|
||||||
|
AuthenticationException,
|
||||||
|
AUTHENTICATION_ERROR_CODES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GitLabOAuthProvider(OauthAdapter):
|
||||||
|
|
||||||
|
(GITLAB_HOST,) = get_configuration_value(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "GITLAB_HOST",
|
||||||
|
"default": os.environ.get("GITLAB_HOST", "https://gitlab.com"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not GITLAB_HOST:
|
||||||
|
raise AuthenticationException(
|
||||||
|
error_code=AUTHENTICATION_ERROR_CODES["GITLAB_NOT_CONFIGURED"],
|
||||||
|
error_message="GITLAB_NOT_CONFIGURED",
|
||||||
|
)
|
||||||
|
|
||||||
|
host = GITLAB_HOST
|
||||||
|
|
||||||
|
token_url = (
|
||||||
|
f"{host}/oauth/token"
|
||||||
|
)
|
||||||
|
userinfo_url = (
|
||||||
|
f"{host}/api/v4/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
provider = "gitlab"
|
||||||
|
scope = "read_user"
|
||||||
|
|
||||||
|
def __init__(self, request, code=None, state=None, callback=None):
|
||||||
|
|
||||||
|
GITLAB_CLIENT_ID, GITLAB_CLIENT_SECRET = get_configuration_value(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "GITLAB_CLIENT_ID",
|
||||||
|
"default": os.environ.get("GITLAB_CLIENT_ID"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "GITLAB_CLIENT_SECRET",
|
||||||
|
"default": os.environ.get("GITLAB_CLIENT_SECRET"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not (GITLAB_CLIENT_ID and GITLAB_CLIENT_SECRET):
|
||||||
|
raise AuthenticationException(
|
||||||
|
error_code=AUTHENTICATION_ERROR_CODES["GITLAB_NOT_CONFIGURED"],
|
||||||
|
error_message="GITLAB_NOT_CONFIGURED",
|
||||||
|
)
|
||||||
|
|
||||||
|
client_id = GITLAB_CLIENT_ID
|
||||||
|
client_secret = GITLAB_CLIENT_SECRET
|
||||||
|
|
||||||
|
redirect_uri = f"""{"https" if request.is_secure() else "http"}://{request.get_host()}/auth/gitlab/callback/"""
|
||||||
|
url_params = {
|
||||||
|
"client_id": client_id,
|
||||||
|
"redirect_uri": redirect_uri,
|
||||||
|
"response_type": "code",
|
||||||
|
"scope": self.scope,
|
||||||
|
"state": state,
|
||||||
|
}
|
||||||
|
auth_url = (
|
||||||
|
f"{self.host}/oauth/authorize?{urlencode(url_params)}"
|
||||||
|
)
|
||||||
|
super().__init__(
|
||||||
|
request,
|
||||||
|
self.provider,
|
||||||
|
client_id,
|
||||||
|
self.scope,
|
||||||
|
redirect_uri,
|
||||||
|
auth_url,
|
||||||
|
self.token_url,
|
||||||
|
self.userinfo_url,
|
||||||
|
client_secret,
|
||||||
|
code,
|
||||||
|
callback=callback,
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_token_data(self):
|
||||||
|
data = {
|
||||||
|
"client_id": self.client_id,
|
||||||
|
"client_secret": self.client_secret,
|
||||||
|
"code": self.code,
|
||||||
|
"redirect_uri": self.redirect_uri,
|
||||||
|
"grant_type": "authorization_code"
|
||||||
|
}
|
||||||
|
token_response = self.get_user_token(
|
||||||
|
data=data, headers={"Accept": "application/json"}
|
||||||
|
)
|
||||||
|
super().set_token_data(
|
||||||
|
{
|
||||||
|
"access_token": token_response.get("access_token"),
|
||||||
|
"refresh_token": token_response.get("refresh_token", None),
|
||||||
|
"access_token_expired_at": (
|
||||||
|
datetime.fromtimestamp(
|
||||||
|
token_response.get("created_at") + token_response.get("expires_in"),
|
||||||
|
tz=pytz.utc,
|
||||||
|
)
|
||||||
|
if token_response.get("expires_in")
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"refresh_token_expired_at": (
|
||||||
|
datetime.fromtimestamp(
|
||||||
|
token_response.get("refresh_token_expired_at"),
|
||||||
|
tz=pytz.utc,
|
||||||
|
)
|
||||||
|
if token_response.get("refresh_token_expired_at")
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"id_token": token_response.get("id_token", ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_user_data(self):
|
||||||
|
user_info_response = self.get_user_response()
|
||||||
|
email = user_info_response.get("email")
|
||||||
|
super().set_user_data(
|
||||||
|
{
|
||||||
|
"email": email,
|
||||||
|
"user": {
|
||||||
|
"provider_id": user_info_response.get("id"),
|
||||||
|
"email": email,
|
||||||
|
"avatar": user_info_response.get("avatar_url"),
|
||||||
|
"first_name": user_info_response.get("name"),
|
||||||
|
"last_name": user_info_response.get("family_name"),
|
||||||
|
"is_password_autoset": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
@ -8,6 +8,8 @@ from .views import (
|
|||||||
ChangePasswordEndpoint,
|
ChangePasswordEndpoint,
|
||||||
# App
|
# App
|
||||||
EmailCheckEndpoint,
|
EmailCheckEndpoint,
|
||||||
|
GitLabCallbackEndpoint,
|
||||||
|
GitLabOauthInitiateEndpoint,
|
||||||
GitHubCallbackEndpoint,
|
GitHubCallbackEndpoint,
|
||||||
GitHubOauthInitiateEndpoint,
|
GitHubOauthInitiateEndpoint,
|
||||||
GoogleCallbackEndpoint,
|
GoogleCallbackEndpoint,
|
||||||
@ -22,6 +24,8 @@ from .views import (
|
|||||||
ResetPasswordSpaceEndpoint,
|
ResetPasswordSpaceEndpoint,
|
||||||
# Space
|
# Space
|
||||||
EmailCheckSpaceEndpoint,
|
EmailCheckSpaceEndpoint,
|
||||||
|
GitLabCallbackSpaceEndpoint,
|
||||||
|
GitLabOauthInitiateSpaceEndpoint,
|
||||||
GitHubCallbackSpaceEndpoint,
|
GitHubCallbackSpaceEndpoint,
|
||||||
GitHubOauthInitiateSpaceEndpoint,
|
GitHubOauthInitiateSpaceEndpoint,
|
||||||
GoogleCallbackSpaceEndpoint,
|
GoogleCallbackSpaceEndpoint,
|
||||||
@ -151,6 +155,27 @@ urlpatterns = [
|
|||||||
GitHubCallbackSpaceEndpoint.as_view(),
|
GitHubCallbackSpaceEndpoint.as_view(),
|
||||||
name="github-callback",
|
name="github-callback",
|
||||||
),
|
),
|
||||||
|
## Gitlab Oauth
|
||||||
|
path(
|
||||||
|
"gitlab/",
|
||||||
|
GitLabOauthInitiateEndpoint.as_view(),
|
||||||
|
name="gitlab-initiate",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"gitlab/callback/",
|
||||||
|
GitLabCallbackEndpoint.as_view(),
|
||||||
|
name="gitlab-callback",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"spaces/gitlab/",
|
||||||
|
GitLabOauthInitiateSpaceEndpoint.as_view(),
|
||||||
|
name="gitlab-initiate",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"spaces/gitlab/callback/",
|
||||||
|
GitLabCallbackSpaceEndpoint.as_view(),
|
||||||
|
name="gitlab-callback",
|
||||||
|
),
|
||||||
# Email Check
|
# Email Check
|
||||||
path(
|
path(
|
||||||
"email-check/",
|
"email-check/",
|
||||||
|
@ -14,6 +14,10 @@ from .app.github import (
|
|||||||
GitHubCallbackEndpoint,
|
GitHubCallbackEndpoint,
|
||||||
GitHubOauthInitiateEndpoint,
|
GitHubOauthInitiateEndpoint,
|
||||||
)
|
)
|
||||||
|
from .app.gitlab import (
|
||||||
|
GitLabCallbackEndpoint,
|
||||||
|
GitLabOauthInitiateEndpoint,
|
||||||
|
)
|
||||||
from .app.google import (
|
from .app.google import (
|
||||||
GoogleCallbackEndpoint,
|
GoogleCallbackEndpoint,
|
||||||
GoogleOauthInitiateEndpoint,
|
GoogleOauthInitiateEndpoint,
|
||||||
@ -34,6 +38,11 @@ from .space.github import (
|
|||||||
GitHubOauthInitiateSpaceEndpoint,
|
GitHubOauthInitiateSpaceEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .space.gitlab import (
|
||||||
|
GitLabCallbackSpaceEndpoint,
|
||||||
|
GitLabOauthInitiateSpaceEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
from .space.google import (
|
from .space.google import (
|
||||||
GoogleCallbackSpaceEndpoint,
|
GoogleCallbackSpaceEndpoint,
|
||||||
GoogleOauthInitiateSpaceEndpoint,
|
GoogleOauthInitiateSpaceEndpoint,
|
||||||
|
131
apiserver/plane/authentication/views/app/gitlab.py
Normal file
131
apiserver/plane/authentication/views/app/gitlab.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import uuid
|
||||||
|
from urllib.parse import urlencode, urljoin
|
||||||
|
|
||||||
|
# Django import
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.authentication.provider.oauth.gitlab import GitLabOAuthProvider
|
||||||
|
from plane.authentication.utils.login import user_login
|
||||||
|
from plane.authentication.utils.redirection_path import get_redirection_path
|
||||||
|
from plane.authentication.utils.user_auth_workflow import (
|
||||||
|
post_user_auth_workflow,
|
||||||
|
)
|
||||||
|
from plane.license.models import Instance
|
||||||
|
from plane.authentication.utils.host import base_host
|
||||||
|
from plane.authentication.adapter.error import (
|
||||||
|
AuthenticationException,
|
||||||
|
AUTHENTICATION_ERROR_CODES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GitLabOauthInitiateEndpoint(View):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
# Get host and next path
|
||||||
|
request.session["host"] = base_host(request=request, is_app=True)
|
||||||
|
next_path = request.GET.get("next_path")
|
||||||
|
if next_path:
|
||||||
|
request.session["next_path"] = str(next_path)
|
||||||
|
|
||||||
|
# Check instance configuration
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
if instance is None or not instance.is_setup_done:
|
||||||
|
exc = AuthenticationException(
|
||||||
|
error_code=AUTHENTICATION_ERROR_CODES[
|
||||||
|
"INSTANCE_NOT_CONFIGURED"
|
||||||
|
],
|
||||||
|
error_message="INSTANCE_NOT_CONFIGURED",
|
||||||
|
)
|
||||||
|
params = exc.get_error_dict()
|
||||||
|
if next_path:
|
||||||
|
params["next_path"] = str(next_path)
|
||||||
|
url = urljoin(
|
||||||
|
base_host(request=request, is_app=True),
|
||||||
|
"?" + urlencode(params),
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
try:
|
||||||
|
state = uuid.uuid4().hex
|
||||||
|
provider = GitLabOAuthProvider(request=request, state=state)
|
||||||
|
request.session["state"] = state
|
||||||
|
auth_url = provider.get_auth_url()
|
||||||
|
return HttpResponseRedirect(auth_url)
|
||||||
|
except AuthenticationException as e:
|
||||||
|
params = e.get_error_dict()
|
||||||
|
if next_path:
|
||||||
|
params["next_path"] = str(next_path)
|
||||||
|
url = urljoin(
|
||||||
|
base_host(request=request, is_app=True),
|
||||||
|
"?" + urlencode(params),
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
class GitLabCallbackEndpoint(View):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
code = request.GET.get("code")
|
||||||
|
state = request.GET.get("state")
|
||||||
|
base_host = request.session.get("host")
|
||||||
|
next_path = request.session.get("next_path")
|
||||||
|
|
||||||
|
if state != request.session.get("state", ""):
|
||||||
|
exc = AuthenticationException(
|
||||||
|
error_code=AUTHENTICATION_ERROR_CODES[
|
||||||
|
"GITLAB_OAUTH_PROVIDER_ERROR"
|
||||||
|
],
|
||||||
|
error_message="GITLAB_OAUTH_PROVIDER_ERROR",
|
||||||
|
)
|
||||||
|
params = exc.get_error_dict()
|
||||||
|
if next_path:
|
||||||
|
params["next_path"] = str(next_path)
|
||||||
|
url = urljoin(
|
||||||
|
base_host,
|
||||||
|
"?" + urlencode(params),
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
if not code:
|
||||||
|
exc = AuthenticationException(
|
||||||
|
error_code=AUTHENTICATION_ERROR_CODES[
|
||||||
|
"GITLAB_OAUTH_PROVIDER_ERROR"
|
||||||
|
],
|
||||||
|
error_message="GITLAB_OAUTH_PROVIDER_ERROR",
|
||||||
|
)
|
||||||
|
params = exc.get_error_dict()
|
||||||
|
if next_path:
|
||||||
|
params["next_path"] = str(next_path)
|
||||||
|
url = urljoin(
|
||||||
|
base_host,
|
||||||
|
"?" + urlencode(params),
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
provider = GitLabOAuthProvider(
|
||||||
|
request=request,
|
||||||
|
code=code,
|
||||||
|
callback=post_user_auth_workflow,
|
||||||
|
)
|
||||||
|
user = provider.authenticate()
|
||||||
|
# Login the user and record his device info
|
||||||
|
user_login(request=request, user=user, is_app=True)
|
||||||
|
# Get the redirection path
|
||||||
|
if next_path:
|
||||||
|
path = next_path
|
||||||
|
else:
|
||||||
|
path = get_redirection_path(user=user)
|
||||||
|
# redirect to referer path
|
||||||
|
url = urljoin(base_host, path)
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
except AuthenticationException as e:
|
||||||
|
params = e.get_error_dict()
|
||||||
|
if next_path:
|
||||||
|
params["next_path"] = str(next_path)
|
||||||
|
url = urljoin(
|
||||||
|
base_host,
|
||||||
|
"?" + urlencode(params),
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(url)
|
109
apiserver/plane/authentication/views/space/gitlab.py
Normal file
109
apiserver/plane/authentication/views/space/gitlab.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# Python imports
|
||||||
|
import uuid
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
# Django import
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.authentication.provider.oauth.gitlab import GitLabOAuthProvider
|
||||||
|
from plane.authentication.utils.login import user_login
|
||||||
|
from plane.license.models import Instance
|
||||||
|
from plane.authentication.utils.host import base_host
|
||||||
|
from plane.authentication.adapter.error import (
|
||||||
|
AUTHENTICATION_ERROR_CODES,
|
||||||
|
AuthenticationException,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GitLabOauthInitiateSpaceEndpoint(View):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
# Get host and next path
|
||||||
|
request.session["host"] = base_host(request=request, is_space=True)
|
||||||
|
next_path = request.GET.get("next_path")
|
||||||
|
if next_path:
|
||||||
|
request.session["next_path"] = str(next_path)
|
||||||
|
|
||||||
|
# Check instance configuration
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
if instance is None or not instance.is_setup_done:
|
||||||
|
exc = AuthenticationException(
|
||||||
|
error_code=AUTHENTICATION_ERROR_CODES[
|
||||||
|
"INSTANCE_NOT_CONFIGURED"
|
||||||
|
],
|
||||||
|
error_message="INSTANCE_NOT_CONFIGURED",
|
||||||
|
)
|
||||||
|
params = exc.get_error_dict()
|
||||||
|
if next_path:
|
||||||
|
params["next_path"] = str(next_path)
|
||||||
|
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
state = uuid.uuid4().hex
|
||||||
|
provider = GitLabOAuthProvider(request=request, state=state)
|
||||||
|
request.session["state"] = state
|
||||||
|
auth_url = provider.get_auth_url()
|
||||||
|
return HttpResponseRedirect(auth_url)
|
||||||
|
except AuthenticationException as e:
|
||||||
|
params = e.get_error_dict()
|
||||||
|
if next_path:
|
||||||
|
params["next_path"] = str(next_path)
|
||||||
|
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
class GitLabCallbackSpaceEndpoint(View):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
code = request.GET.get("code")
|
||||||
|
state = request.GET.get("state")
|
||||||
|
base_host = request.session.get("host")
|
||||||
|
next_path = request.session.get("next_path")
|
||||||
|
|
||||||
|
if state != request.session.get("state", ""):
|
||||||
|
exc = AuthenticationException(
|
||||||
|
error_code=AUTHENTICATION_ERROR_CODES[
|
||||||
|
"GITLAB_OAUTH_PROVIDER_ERROR"
|
||||||
|
],
|
||||||
|
error_message="GITLAB_OAUTH_PROVIDER_ERROR",
|
||||||
|
)
|
||||||
|
params = exc.get_error_dict()
|
||||||
|
if next_path:
|
||||||
|
params["next_path"] = str(next_path)
|
||||||
|
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
if not code:
|
||||||
|
exc = AuthenticationException(
|
||||||
|
error_code=AUTHENTICATION_ERROR_CODES[
|
||||||
|
"GITLAB_OAUTH_PROVIDER_ERROR"
|
||||||
|
],
|
||||||
|
error_message="GITLAB_OAUTH_PROVIDER_ERROR",
|
||||||
|
)
|
||||||
|
params = exc.get_error_dict()
|
||||||
|
if next_path:
|
||||||
|
params["next_path"] = str(next_path)
|
||||||
|
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
provider = GitLabOAuthProvider(
|
||||||
|
request=request,
|
||||||
|
code=code,
|
||||||
|
)
|
||||||
|
user = provider.authenticate()
|
||||||
|
# Login the user and record his device info
|
||||||
|
user_login(request=request, user=user, is_space=True)
|
||||||
|
# Process workspace and project invitations
|
||||||
|
# redirect to referer path
|
||||||
|
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
except AuthenticationException as e:
|
||||||
|
params = e.get_error_dict()
|
||||||
|
if next_path:
|
||||||
|
params["next_path"] = str(next_path)
|
||||||
|
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||||
|
return HttpResponseRedirect(url)
|
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.2.11 on 2024-06-03 17:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0066_account_id_token_cycle_logo_props_module_logo_props'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='provider',
|
||||||
|
field=models.CharField(choices=[('google', 'Google'), ('github', 'Github'), ('gitlab', 'GitLab')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='socialloginconnection',
|
||||||
|
name='medium',
|
||||||
|
field=models.CharField(choices=[('Google', 'google'), ('Github', 'github'), ('GitLab', 'gitlab'), ('Jira', 'jira')], default=None, max_length=20),
|
||||||
|
),
|
||||||
|
]
|
@ -10,7 +10,7 @@ from .base import BaseModel
|
|||||||
class SocialLoginConnection(BaseModel):
|
class SocialLoginConnection(BaseModel):
|
||||||
medium = models.CharField(
|
medium = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
choices=(("Google", "google"), ("Github", "github"), ("Jira", "jira")),
|
choices=(("Google", "google"), ("Github", "github"), ("GitLab", "gitlab"), ("Jira", "jira")),
|
||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
last_login_at = models.DateTimeField(default=timezone.now, null=True)
|
last_login_at = models.DateTimeField(default=timezone.now, null=True)
|
||||||
|
@ -182,7 +182,7 @@ class Account(TimeAuditModel):
|
|||||||
)
|
)
|
||||||
provider_account_id = models.CharField(max_length=255)
|
provider_account_id = models.CharField(max_length=255)
|
||||||
provider = models.CharField(
|
provider = models.CharField(
|
||||||
choices=(("google", "Google"), ("github", "Github")),
|
choices=(("google", "Google"), ("github", "Github"), ("gitlab", "GitLab")),
|
||||||
)
|
)
|
||||||
access_token = models.TextField()
|
access_token = models.TextField()
|
||||||
access_token_expired_at = models.DateTimeField(null=True)
|
access_token_expired_at = models.DateTimeField(null=True)
|
||||||
|
@ -54,6 +54,7 @@ class InstanceEndpoint(BaseAPIView):
|
|||||||
IS_GOOGLE_ENABLED,
|
IS_GOOGLE_ENABLED,
|
||||||
IS_GITHUB_ENABLED,
|
IS_GITHUB_ENABLED,
|
||||||
GITHUB_APP_NAME,
|
GITHUB_APP_NAME,
|
||||||
|
IS_GITLAB_ENABLED,
|
||||||
EMAIL_HOST,
|
EMAIL_HOST,
|
||||||
ENABLE_MAGIC_LINK_LOGIN,
|
ENABLE_MAGIC_LINK_LOGIN,
|
||||||
ENABLE_EMAIL_PASSWORD,
|
ENABLE_EMAIL_PASSWORD,
|
||||||
@ -76,6 +77,10 @@ class InstanceEndpoint(BaseAPIView):
|
|||||||
"key": "GITHUB_APP_NAME",
|
"key": "GITHUB_APP_NAME",
|
||||||
"default": os.environ.get("GITHUB_APP_NAME", ""),
|
"default": os.environ.get("GITHUB_APP_NAME", ""),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "IS_GITLAB_ENABLED",
|
||||||
|
"default": os.environ.get("IS_GITLAB_ENABLED", "0"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "EMAIL_HOST",
|
"key": "EMAIL_HOST",
|
||||||
"default": os.environ.get("EMAIL_HOST", ""),
|
"default": os.environ.get("EMAIL_HOST", ""),
|
||||||
@ -115,6 +120,7 @@ class InstanceEndpoint(BaseAPIView):
|
|||||||
# Authentication
|
# Authentication
|
||||||
data["is_google_enabled"] = IS_GOOGLE_ENABLED == "1"
|
data["is_google_enabled"] = IS_GOOGLE_ENABLED == "1"
|
||||||
data["is_github_enabled"] = IS_GITHUB_ENABLED == "1"
|
data["is_github_enabled"] = IS_GITHUB_ENABLED == "1"
|
||||||
|
data["is_gitlab_enabled"] = IS_GITLAB_ENABLED == "1"
|
||||||
data["is_magic_login_enabled"] = ENABLE_MAGIC_LINK_LOGIN == "1"
|
data["is_magic_login_enabled"] = ENABLE_MAGIC_LINK_LOGIN == "1"
|
||||||
data["is_email_password_enabled"] = ENABLE_EMAIL_PASSWORD == "1"
|
data["is_email_password_enabled"] = ENABLE_EMAIL_PASSWORD == "1"
|
||||||
|
|
||||||
|
@ -59,6 +59,24 @@ class Command(BaseCommand):
|
|||||||
"category": "GITHUB",
|
"category": "GITHUB",
|
||||||
"is_encrypted": True,
|
"is_encrypted": True,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "GITLAB_HOST",
|
||||||
|
"value": os.environ.get("GITLAB_HOST"),
|
||||||
|
"category": "GITLAB",
|
||||||
|
"is_encrypted": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "GITLAB_CLIENT_ID",
|
||||||
|
"value": os.environ.get("GITLAB_CLIENT_ID"),
|
||||||
|
"category": "GITLAB",
|
||||||
|
"is_encrypted": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "GITLAB_CLIENT_SECRET",
|
||||||
|
"value": os.environ.get("GITLAB_CLIENT_SECRET"),
|
||||||
|
"category": "GITLAB",
|
||||||
|
"is_encrypted": True,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "EMAIL_HOST",
|
"key": "EMAIL_HOST",
|
||||||
"value": os.environ.get("EMAIL_HOST", ""),
|
"value": os.environ.get("EMAIL_HOST", ""),
|
||||||
@ -145,7 +163,7 @@ class Command(BaseCommand):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
keys = ["IS_GOOGLE_ENABLED", "IS_GITHUB_ENABLED"]
|
keys = ["IS_GOOGLE_ENABLED", "IS_GITHUB_ENABLED", "IS_GITLAB_ENABLED"]
|
||||||
if not InstanceConfiguration.objects.filter(key__in=keys).exists():
|
if not InstanceConfiguration.objects.filter(key__in=keys).exists():
|
||||||
for key in keys:
|
for key in keys:
|
||||||
if key == "IS_GOOGLE_ENABLED":
|
if key == "IS_GOOGLE_ENABLED":
|
||||||
@ -216,6 +234,46 @@ class Command(BaseCommand):
|
|||||||
f"{key} loaded with value from environment variable."
|
f"{key} loaded with value from environment variable."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if key == "IS_GITLAB_ENABLED":
|
||||||
|
GITLAB_HOST, GITLAB_CLIENT_ID, GITLAB_CLIENT_SECRET = (
|
||||||
|
get_configuration_value(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "GITLAB_HOST",
|
||||||
|
"default": os.environ.get(
|
||||||
|
"GITLAB_HOST", "https://gitlab.com"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "GITLAB_CLIENT_ID",
|
||||||
|
"default": os.environ.get(
|
||||||
|
"GITLAB_CLIENT_ID", ""
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "GITLAB_CLIENT_SECRET",
|
||||||
|
"default": os.environ.get(
|
||||||
|
"GITLAB_CLIENT_SECRET", ""
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if bool(GITLAB_HOST) and bool(GITLAB_CLIENT_ID) and bool(GITLAB_CLIENT_SECRET):
|
||||||
|
value = "1"
|
||||||
|
else:
|
||||||
|
value = "0"
|
||||||
|
InstanceConfiguration.objects.create(
|
||||||
|
key="IS_GITLAB_ENABLED",
|
||||||
|
value=value,
|
||||||
|
category="AUTHENTICATION",
|
||||||
|
is_encrypted=False,
|
||||||
|
)
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"{key} loaded with value from environment variable."
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
for key in keys:
|
for key in keys:
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
|
@ -60,10 +60,13 @@ export enum EAuthErrorCodes {
|
|||||||
EXPIRED_MAGIC_CODE = "5095",
|
EXPIRED_MAGIC_CODE = "5095",
|
||||||
EMAIL_CODE_ATTEMPT_EXHAUSTED = "5100",
|
EMAIL_CODE_ATTEMPT_EXHAUSTED = "5100",
|
||||||
// Oauth
|
// Oauth
|
||||||
|
OAUTH_NOT_CONFIGURED = "5104",
|
||||||
GOOGLE_NOT_CONFIGURED = "5105",
|
GOOGLE_NOT_CONFIGURED = "5105",
|
||||||
GITHUB_NOT_CONFIGURED = "5110",
|
GITHUB_NOT_CONFIGURED = "5110",
|
||||||
|
GITLAB_NOT_CONFIGURED = "5111",
|
||||||
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
|
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
|
||||||
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
|
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
|
||||||
|
GITLAB_OAUTH_PROVIDER_ERROR = "5121",
|
||||||
// Reset Password
|
// Reset Password
|
||||||
INVALID_PASSWORD_TOKEN = "5125",
|
INVALID_PASSWORD_TOKEN = "5125",
|
||||||
EXPIRED_PASSWORD_TOKEN = "5130",
|
EXPIRED_PASSWORD_TOKEN = "5130",
|
||||||
@ -215,6 +218,10 @@ const errorCodeMessages: {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Oauth
|
// Oauth
|
||||||
|
[EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED]: {
|
||||||
|
title: `OAuth not configured`,
|
||||||
|
message: () => `OAuth not configured. Please contact your administrator.`,
|
||||||
|
},
|
||||||
[EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: {
|
[EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: {
|
||||||
title: `Google not configured`,
|
title: `Google not configured`,
|
||||||
message: () => `Google not configured. Please contact your administrator.`,
|
message: () => `Google not configured. Please contact your administrator.`,
|
||||||
@ -223,6 +230,10 @@ const errorCodeMessages: {
|
|||||||
title: `GitHub not configured`,
|
title: `GitHub not configured`,
|
||||||
message: () => `GitHub not configured. Please contact your administrator.`,
|
message: () => `GitHub not configured. Please contact your administrator.`,
|
||||||
},
|
},
|
||||||
|
[EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED]: {
|
||||||
|
title: `GitLab not configured`,
|
||||||
|
message: () => `GitLab not configured. Please contact your administrator.`,
|
||||||
|
},
|
||||||
[EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR]: {
|
[EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR]: {
|
||||||
title: `Google OAuth provider error`,
|
title: `Google OAuth provider error`,
|
||||||
message: () => `Google OAuth provider error. Please try again.`,
|
message: () => `Google OAuth provider error. Please try again.`,
|
||||||
@ -231,6 +242,10 @@ const errorCodeMessages: {
|
|||||||
title: `GitHub OAuth provider error`,
|
title: `GitHub OAuth provider error`,
|
||||||
message: () => `GitHub OAuth provider error. Please try again.`,
|
message: () => `GitHub OAuth provider error. Please try again.`,
|
||||||
},
|
},
|
||||||
|
[EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR]: {
|
||||||
|
title: `GitLab OAuth provider error`,
|
||||||
|
message: () => `GitLab OAuth provider error. Please try again.`,
|
||||||
|
},
|
||||||
|
|
||||||
// Reset Password
|
// Reset Password
|
||||||
[EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: {
|
[EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: {
|
||||||
|
@ -4,7 +4,7 @@ export type TCurrentUserAccount = {
|
|||||||
user: string | undefined;
|
user: string | undefined;
|
||||||
|
|
||||||
provider_account_id: string | undefined;
|
provider_account_id: string | undefined;
|
||||||
provider: "google" | "github" | string | undefined;
|
provider: "google" | "github" | "gitlab" | string | undefined;
|
||||||
access_token: string | undefined;
|
access_token: string | undefined;
|
||||||
access_token_expired_at: Date | undefined;
|
access_token_expired_at: Date | undefined;
|
||||||
refresh_token: string | undefined;
|
refresh_token: string | undefined;
|
||||||
|
11
packages/types/src/instance/auth.d.ts
vendored
11
packages/types/src/instance/auth.d.ts
vendored
@ -3,7 +3,8 @@ export type TInstanceAuthenticationMethodKeys =
|
|||||||
| "ENABLE_MAGIC_LINK_LOGIN"
|
| "ENABLE_MAGIC_LINK_LOGIN"
|
||||||
| "ENABLE_EMAIL_PASSWORD"
|
| "ENABLE_EMAIL_PASSWORD"
|
||||||
| "IS_GOOGLE_ENABLED"
|
| "IS_GOOGLE_ENABLED"
|
||||||
| "IS_GITHUB_ENABLED";
|
| "IS_GITHUB_ENABLED"
|
||||||
|
| "IS_GITLAB_ENABLED";
|
||||||
|
|
||||||
export type TInstanceGoogleAuthenticationConfigurationKeys =
|
export type TInstanceGoogleAuthenticationConfigurationKeys =
|
||||||
| "GOOGLE_CLIENT_ID"
|
| "GOOGLE_CLIENT_ID"
|
||||||
@ -13,9 +14,15 @@ export type TInstanceGithubAuthenticationConfigurationKeys =
|
|||||||
| "GITHUB_CLIENT_ID"
|
| "GITHUB_CLIENT_ID"
|
||||||
| "GITHUB_CLIENT_SECRET";
|
| "GITHUB_CLIENT_SECRET";
|
||||||
|
|
||||||
|
export type TInstanceGitlabAuthenticationConfigurationKeys =
|
||||||
|
| "GITLAB_HOST"
|
||||||
|
| "GITLAB_CLIENT_ID"
|
||||||
|
| "GITLAB_CLIENT_SECRET";
|
||||||
|
|
||||||
type TInstanceAuthenticationConfigurationKeys =
|
type TInstanceAuthenticationConfigurationKeys =
|
||||||
| TInstanceGoogleAuthenticationConfigurationKeys
|
| TInstanceGoogleAuthenticationConfigurationKeys
|
||||||
| TInstanceGithubAuthenticationConfigurationKeys;
|
| TInstanceGithubAuthenticationConfigurationKeys
|
||||||
|
| TInstanceGitlabAuthenticationConfigurationKeys;
|
||||||
|
|
||||||
export type TInstanceAuthenticationKeys =
|
export type TInstanceAuthenticationKeys =
|
||||||
| TInstanceAuthenticationMethodKeys
|
| TInstanceAuthenticationMethodKeys
|
||||||
|
1
packages/types/src/instance/base.d.ts
vendored
1
packages/types/src/instance/base.d.ts
vendored
@ -38,6 +38,7 @@ export interface IInstance {
|
|||||||
export interface IInstanceConfig {
|
export interface IInstanceConfig {
|
||||||
is_google_enabled: boolean;
|
is_google_enabled: boolean;
|
||||||
is_github_enabled: boolean;
|
is_github_enabled: boolean;
|
||||||
|
is_gitlab_enabled: boolean;
|
||||||
is_magic_login_enabled: boolean;
|
is_magic_login_enabled: boolean;
|
||||||
is_email_password_enabled: boolean;
|
is_email_password_enabled: boolean;
|
||||||
github_app_name: string | undefined;
|
github_app_name: string | undefined;
|
||||||
|
2
packages/types/src/users.d.ts
vendored
2
packages/types/src/users.d.ts
vendored
@ -5,7 +5,7 @@ import {
|
|||||||
TStateGroups,
|
TStateGroups,
|
||||||
} from ".";
|
} from ".";
|
||||||
|
|
||||||
type TLoginMediums = "email" | "magic-code" | "github" | "google";
|
type TLoginMediums = "email" | "magic-code" | "github" | "gitlab" | "google";
|
||||||
|
|
||||||
export interface IUser {
|
export interface IUser {
|
||||||
id: string;
|
id: string;
|
||||||
|
28
packages/ui/src/icons/gitlab-icon.tsx
Normal file
28
packages/ui/src/icons/gitlab-icon.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { ISvgIcons } from "./type";
|
||||||
|
|
||||||
|
export const GitlabIcon: React.FC<ISvgIcons> = ({ width = "24", height = "24", className, color }) => (
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
className={className}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g clipPath="url(#clip0_282_232)">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10 0C4.475 0 0 4.475 0 10C0 14.425 2.8625 18.1625 6.8375 19.4875C7.3375 19.575 7.525 19.275 7.525 19.0125C7.525 18.775 7.5125 17.9875 7.5125 17.15C5 17.6125 4.35 16.5375 4.15 15.975C4.0375 15.6875 3.55 14.8 3.125 14.5625C2.775 14.375 2.275 13.9125 3.1125 13.9C3.9 13.8875 4.4625 14.625 4.65 14.925C5.55 16.4375 6.9875 16.0125 7.5625 15.75C7.65 15.1 7.9125 14.6625 8.2 14.4125C5.975 14.1625 3.65 13.3 3.65 9.475C3.65 8.3875 4.0375 7.4875 4.675 6.7875C4.575 6.5375 4.225 5.5125 4.775 4.1375C4.775 4.1375 5.6125 3.875 7.525 5.1625C8.325 4.9375 9.175 4.825 10.025 4.825C10.875 4.825 11.725 4.9375 12.525 5.1625C14.4375 3.8625 15.275 4.1375 15.275 4.1375C15.825 5.5125 15.475 6.5375 15.375 6.7875C16.0125 7.4875 16.4 8.375 16.4 9.475C16.4 13.3125 14.0625 14.1625 11.8375 14.4125C12.2 14.725 12.5125 15.325 12.5125 16.2625C12.5125 17.6 12.5 18.675 12.5 19.0125C12.5 19.275 12.6875 19.5875 13.1875 19.4875C15.1726 18.8173 16.8976 17.5414 18.1197 15.8395C19.3418 14.1375 19.9994 12.0952 20 10C20 4.475 15.525 0 10 0Z"
|
||||||
|
fill={color ? color : "rgb(var(--color-text-200))"}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_282_232">
|
||||||
|
<rect width={width} height={height} />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
@ -12,6 +12,7 @@ export * from "./dice-icon";
|
|||||||
export * from "./discord-icon";
|
export * from "./discord-icon";
|
||||||
export * from "./full-screen-panel-icon";
|
export * from "./full-screen-panel-icon";
|
||||||
export * from "./github-icon";
|
export * from "./github-icon";
|
||||||
|
export * from "./gitlab-icon";
|
||||||
export * from "./layer-stack";
|
export * from "./layer-stack";
|
||||||
export * from "./layers-icon";
|
export * from "./layers-icon";
|
||||||
export * from "./photo-filter-icon";
|
export * from "./photo-filter-icon";
|
||||||
|
@ -85,7 +85,7 @@ export const AuthRoot: FC = observer(() => {
|
|||||||
const isSMTPConfigured = config?.is_smtp_configured || false;
|
const isSMTPConfigured = config?.is_smtp_configured || false;
|
||||||
const isMagicLoginEnabled = config?.is_magic_login_enabled || false;
|
const isMagicLoginEnabled = config?.is_magic_login_enabled || false;
|
||||||
const isEmailPasswordEnabled = config?.is_email_password_enabled || false;
|
const isEmailPasswordEnabled = config?.is_email_password_enabled || false;
|
||||||
const isOAuthEnabled = (config && (config?.is_google_enabled || config?.is_github_enabled)) || false;
|
const isOAuthEnabled = (config && (config?.is_google_enabled || config?.is_github_enabled || config?.is_gitlab_enabled)) || false;
|
||||||
|
|
||||||
// submit handler- email verification
|
// submit handler- email verification
|
||||||
const handleEmailVerification = async (data: IEmailCheckData) => {
|
const handleEmailVerification = async (data: IEmailCheckData) => {
|
||||||
|
36
space/components/account/oauth/gitlab-button.tsx
Normal file
36
space/components/account/oauth/gitlab-button.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
// helpers
|
||||||
|
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||||
|
// images
|
||||||
|
import GitlabLogo from "/public/logos/gitlab-logo.svg";
|
||||||
|
|
||||||
|
export type GitlabOAuthButtonProps = {
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GitlabOAuthButton: FC<GitlabOAuthButtonProps> = (props) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const nextPath = searchParams.get("next_path") || undefined;
|
||||||
|
const { text } = props;
|
||||||
|
// hooks
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
|
const handleSignIn = () => {
|
||||||
|
window.location.assign(`${API_BASE_URL}/auth/spaces/gitlab/${nextPath ? `?next_path=${nextPath}` : ``}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`flex h-[42px] w-full items-center justify-center gap-2 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-onboarding-background-300 ${
|
||||||
|
resolvedTheme === "dark" ? "border-[#43484F] bg-[#2F3135]" : "border-[#D9E4FF]"
|
||||||
|
}`}
|
||||||
|
onClick={handleSignIn}
|
||||||
|
>
|
||||||
|
<Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />
|
||||||
|
{text}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./oauth-options";
|
export * from "./oauth-options";
|
||||||
export * from "./google-button";
|
export * from "./google-button";
|
||||||
export * from "./github-button";
|
export * from "./github-button";
|
||||||
|
export * from "./gitlab-button";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { GithubOAuthButton, GoogleOAuthButton } from "@/components/account";
|
import { GithubOAuthButton, GitlabOAuthButton, GoogleOAuthButton } from "@/components/account";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance } from "@/hooks/store";
|
import { useInstance } from "@/hooks/store";
|
||||||
|
|
||||||
@ -22,6 +22,7 @@ export const OAuthOptions: React.FC = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{config?.is_github_enabled && <GithubOAuthButton text="Sign in with Github" />}
|
{config?.is_github_enabled && <GithubOAuthButton text="Sign in with Github" />}
|
||||||
|
{config?.is_gitlab_enabled && <GitlabOAuthButton text="Sign in with GitLab" />}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -52,10 +52,13 @@ export enum EAuthenticationErrorCodes {
|
|||||||
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN = "5100",
|
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN = "5100",
|
||||||
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP = "5102",
|
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP = "5102",
|
||||||
// Oauth
|
// Oauth
|
||||||
|
OAUTH_NOT_CONFIGURED = "5104",
|
||||||
GOOGLE_NOT_CONFIGURED = "5105",
|
GOOGLE_NOT_CONFIGURED = "5105",
|
||||||
GITHUB_NOT_CONFIGURED = "5110",
|
GITHUB_NOT_CONFIGURED = "5110",
|
||||||
|
GITLAB_NOT_CONFIGURED = "5111",
|
||||||
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
|
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
|
||||||
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
|
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
|
||||||
|
GITLAB_OAUTH_PROVIDER_ERROR = "5121",
|
||||||
// Reset Password
|
// Reset Password
|
||||||
INVALID_PASSWORD_TOKEN = "5125",
|
INVALID_PASSWORD_TOKEN = "5125",
|
||||||
EXPIRED_PASSWORD_TOKEN = "5130",
|
EXPIRED_PASSWORD_TOKEN = "5130",
|
||||||
@ -220,6 +223,10 @@ const errorCodeMessages: {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Oauth
|
// Oauth
|
||||||
|
[EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED]: {
|
||||||
|
title: `OAuth not configured`,
|
||||||
|
message: () => `OAuth not configured. Please contact your administrator.`,
|
||||||
|
},
|
||||||
[EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: {
|
[EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: {
|
||||||
title: `Google not configured`,
|
title: `Google not configured`,
|
||||||
message: () => `Google not configured. Please contact your administrator.`,
|
message: () => `Google not configured. Please contact your administrator.`,
|
||||||
@ -228,6 +235,10 @@ const errorCodeMessages: {
|
|||||||
title: `GitHub not configured`,
|
title: `GitHub not configured`,
|
||||||
message: () => `GitHub not configured. Please contact your administrator.`,
|
message: () => `GitHub not configured. Please contact your administrator.`,
|
||||||
},
|
},
|
||||||
|
[EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED]: {
|
||||||
|
title: `GitLab not configured`,
|
||||||
|
message: () => `GitLab not configured. Please contact your administrator.`,
|
||||||
|
},
|
||||||
[EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR]: {
|
[EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR]: {
|
||||||
title: `Google OAuth provider error`,
|
title: `Google OAuth provider error`,
|
||||||
message: () => `Google OAuth provider error. Please try again.`,
|
message: () => `Google OAuth provider error. Please try again.`,
|
||||||
@ -236,6 +247,10 @@ const errorCodeMessages: {
|
|||||||
title: `GitHub OAuth provider error`,
|
title: `GitHub OAuth provider error`,
|
||||||
message: () => `GitHub OAuth provider error. Please try again.`,
|
message: () => `GitHub OAuth provider error. Please try again.`,
|
||||||
},
|
},
|
||||||
|
[EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR]: {
|
||||||
|
title: `GitLab OAuth provider error`,
|
||||||
|
message: () => `GitLab OAuth provider error. Please try again.`,
|
||||||
|
},
|
||||||
|
|
||||||
// Reset Password
|
// Reset Password
|
||||||
[EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: {
|
[EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: {
|
||||||
@ -347,10 +362,13 @@ export const authErrorHandler = (
|
|||||||
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP,
|
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP,
|
||||||
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN,
|
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN,
|
||||||
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP,
|
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP,
|
||||||
|
EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED,
|
||||||
EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED,
|
EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED,
|
||||||
EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED,
|
EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED,
|
||||||
|
EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED,
|
||||||
EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR,
|
EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR,
|
||||||
EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR,
|
EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR,
|
||||||
|
EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR,
|
||||||
EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN,
|
EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN,
|
||||||
EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN,
|
EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN,
|
||||||
EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD,
|
EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD,
|
||||||
|
1
space/public/logos/gitlab-logo.svg
Normal file
1
space/public/logos/gitlab-logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="80 80 220 220"><defs><style>.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style></defs><g id="LOGO"><path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-2" d="M282.83,170.73l-.27-.69a88.3,88.3,0,0,0-35.15,15.8L190,229.25c19.55,14.79,36.57,27.64,36.57,27.64l40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-3" d="M153.43,256.89l19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91S209.55,244,190,229.25C170.45,244,153.43,256.89,153.43,256.89Z"/><path class="cls-2" d="M132.58,185.84A88.19,88.19,0,0,0,97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82s17-12.85,36.57-27.64Z"/></g></svg>
|
After Width: | Height: | Size: 1.0 KiB |
1
space/types/app.d.ts
vendored
1
space/types/app.d.ts
vendored
@ -4,6 +4,7 @@ export interface IAppConfig {
|
|||||||
google_client_id: string | null;
|
google_client_id: string | null;
|
||||||
github_app_name: string | null;
|
github_app_name: string | null;
|
||||||
github_client_id: string | null;
|
github_client_id: string | null;
|
||||||
|
gitlab_client_id: string | null;
|
||||||
magic_login: boolean;
|
magic_login: boolean;
|
||||||
slack_client_id: string | null;
|
slack_client_id: string | null;
|
||||||
posthog_api_key: string | null;
|
posthog_api_key: string | null;
|
||||||
|
36
web/components/account/oauth/gitlab-button.tsx
Normal file
36
web/components/account/oauth/gitlab-button.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
// helpers
|
||||||
|
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||||
|
// images
|
||||||
|
import GitlabLogo from "/public/logos/gitlab-logo.svg";
|
||||||
|
|
||||||
|
export type GitlabOAuthButtonProps = {
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GitlabOAuthButton: FC<GitlabOAuthButtonProps> = (props) => {
|
||||||
|
const { query } = useRouter();
|
||||||
|
const { next_path } = query;
|
||||||
|
const { text } = props;
|
||||||
|
// hooks
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
|
const handleSignIn = () => {
|
||||||
|
window.location.assign(`${API_BASE_URL}/auth/gitlab/${next_path ? `?next_path=${next_path}` : ``}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`flex h-[42px] w-full items-center justify-center gap-2 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 bg-onboarding-background-200 hover:bg-onboarding-background-300 ${
|
||||||
|
resolvedTheme === "dark" ? "border-[#43484F]" : "border-[#D9E4FF]"
|
||||||
|
}`}
|
||||||
|
onClick={handleSignIn}
|
||||||
|
>
|
||||||
|
<Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />
|
||||||
|
{text}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./oauth-options";
|
export * from "./oauth-options";
|
||||||
export * from "./google-button";
|
export * from "./google-button";
|
||||||
export * from "./github-button";
|
export * from "./github-button";
|
||||||
|
export * from "./gitlab-button";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// components
|
// components
|
||||||
import { GithubOAuthButton, GoogleOAuthButton } from "@/components/account";
|
import { GithubOAuthButton, GitlabOAuthButton, GoogleOAuthButton } from "@/components/account";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance } from "@/hooks/store";
|
import { useInstance } from "@/hooks/store";
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ export const OAuthOptions: React.FC<TOAuthOptionProps> = observer(() => {
|
|||||||
// hooks
|
// hooks
|
||||||
const { config } = useInstance();
|
const { config } = useInstance();
|
||||||
|
|
||||||
const isOAuthEnabled = (config && (config?.is_google_enabled || config?.is_github_enabled)) || false;
|
const isOAuthEnabled = (config && (config?.is_google_enabled || config?.is_github_enabled || config?.is_gitlab_enabled)) || false;
|
||||||
|
|
||||||
if (!isOAuthEnabled) return null;
|
if (!isOAuthEnabled) return null;
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ export const OAuthOptions: React.FC<TOAuthOptionProps> = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{config?.is_github_enabled && <GithubOAuthButton text="Continue with Github" />}
|
{config?.is_github_enabled && <GithubOAuthButton text="Continue with Github" />}
|
||||||
|
{config?.is_gitlab_enabled && <GitlabOAuthButton text="Continue with GitLab" />}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -64,10 +64,13 @@ export enum EAuthenticationErrorCodes {
|
|||||||
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN = "5100",
|
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN = "5100",
|
||||||
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP = "5102",
|
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP = "5102",
|
||||||
// Oauth
|
// Oauth
|
||||||
|
OAUTH_NOT_CONFIGURED = "5104",
|
||||||
GOOGLE_NOT_CONFIGURED = "5105",
|
GOOGLE_NOT_CONFIGURED = "5105",
|
||||||
GITHUB_NOT_CONFIGURED = "5110",
|
GITHUB_NOT_CONFIGURED = "5110",
|
||||||
|
GITLAB_NOT_CONFIGURED = "5111",
|
||||||
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
|
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
|
||||||
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
|
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
|
||||||
|
GITLAB_OAUTH_PROVIDER_ERROR = "5121",
|
||||||
// Reset Password
|
// Reset Password
|
||||||
INVALID_PASSWORD_TOKEN = "5125",
|
INVALID_PASSWORD_TOKEN = "5125",
|
||||||
EXPIRED_PASSWORD_TOKEN = "5130",
|
EXPIRED_PASSWORD_TOKEN = "5130",
|
||||||
@ -239,6 +242,10 @@ const errorCodeMessages: {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Oauth
|
// Oauth
|
||||||
|
[EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED]: {
|
||||||
|
title: `OAuth not configured`,
|
||||||
|
message: () => `OAuth not configured. Please contact your administrator.`,
|
||||||
|
},
|
||||||
[EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: {
|
[EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: {
|
||||||
title: `Google not configured`,
|
title: `Google not configured`,
|
||||||
message: () => `Google not configured. Please contact your administrator.`,
|
message: () => `Google not configured. Please contact your administrator.`,
|
||||||
@ -247,6 +254,10 @@ const errorCodeMessages: {
|
|||||||
title: `GitHub not configured`,
|
title: `GitHub not configured`,
|
||||||
message: () => `GitHub not configured. Please contact your administrator.`,
|
message: () => `GitHub not configured. Please contact your administrator.`,
|
||||||
},
|
},
|
||||||
|
[EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED]: {
|
||||||
|
title: `GitLab not configured`,
|
||||||
|
message: () => `GitLab not configured. Please contact your administrator.`,
|
||||||
|
},
|
||||||
[EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR]: {
|
[EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR]: {
|
||||||
title: `Google OAuth provider error`,
|
title: `Google OAuth provider error`,
|
||||||
message: () => `Google OAuth provider error. Please try again.`,
|
message: () => `Google OAuth provider error. Please try again.`,
|
||||||
@ -255,6 +266,10 @@ const errorCodeMessages: {
|
|||||||
title: `GitHub OAuth provider error`,
|
title: `GitHub OAuth provider error`,
|
||||||
message: () => `GitHub OAuth provider error. Please try again.`,
|
message: () => `GitHub OAuth provider error. Please try again.`,
|
||||||
},
|
},
|
||||||
|
[EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR]: {
|
||||||
|
title: `GitLab OAuth provider error`,
|
||||||
|
message: () => `GitLab OAuth provider error. Please try again.`,
|
||||||
|
},
|
||||||
|
|
||||||
// Reset Password
|
// Reset Password
|
||||||
[EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: {
|
[EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: {
|
||||||
@ -377,10 +392,13 @@ export const authErrorHandler = (
|
|||||||
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP,
|
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP,
|
||||||
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN,
|
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN,
|
||||||
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP,
|
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP,
|
||||||
|
EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED,
|
||||||
EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED,
|
EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED,
|
||||||
EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED,
|
EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED,
|
||||||
|
EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED,
|
||||||
EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR,
|
EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR,
|
||||||
EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR,
|
EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR,
|
||||||
|
EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR,
|
||||||
EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN,
|
EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN,
|
||||||
EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN,
|
EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN,
|
||||||
EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD,
|
EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD,
|
||||||
|
1
web/public/logos/gitlab-logo.svg
Normal file
1
web/public/logos/gitlab-logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="80 80 220 220"><defs><style>.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style></defs><g id="LOGO"><path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-2" d="M282.83,170.73l-.27-.69a88.3,88.3,0,0,0-35.15,15.8L190,229.25c19.55,14.79,36.57,27.64,36.57,27.64l40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-3" d="M153.43,256.89l19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91S209.55,244,190,229.25C170.45,244,153.43,256.89,153.43,256.89Z"/><path class="cls-2" d="M132.58,185.84A88.19,88.19,0,0,0,97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82s17-12.85,36.57-27.64Z"/></g></svg>
|
After Width: | Height: | Size: 1.0 KiB |
Loading…
Reference in New Issue
Block a user