fix: merging gitlab changes

This commit is contained in:
sriram veeraghanta 2024-06-14 15:23:30 +05:30
commit 64bbe19f1b
39 changed files with 1448 additions and 17 deletions

View 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>
)}
</>
);
});

View File

@ -1,5 +1,6 @@
export * from "./email-config-switch";
export * from "./password-config-switch";
export * from "./authentication-method-card";
export * from "./gitlab-config";
export * from "./github-config";
export * from "./google-config";

View 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>
</>
);
};

View 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;

View File

@ -17,12 +17,14 @@ import { useInstance } from "@/hooks/store";
// images
import githubLightModeImage from "@/public/logos/github-black.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";
// local components
import {
AuthenticationMethodCard,
EmailCodesConfiguration,
PasswordLoginConfiguration,
GitlabConfiguration,
GithubConfiguration,
GoogleConfiguration,
} from "./components";
@ -116,6 +118,13 @@ const InstanceAuthenticationPage = observer(() => {
),
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 (

View File

@ -31,6 +31,8 @@ export const InstanceHeader: FC = observer(() => {
return "Google";
case "github":
return "Github";
case "gitlab":
return "GitLab";
default:
return pathName.toUpperCase();
}

View 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
View File

0
apiserver/bin/docker-entrypoint-migrator.sh Normal file → Executable file
View File

View File

@ -33,10 +33,13 @@ AUTHENTICATION_ERROR_CODES = {
"EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN": 5100,
"EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP": 5102,
# Oauth
"OAUTH_NOT_CONFIGURED": 5104,
"GOOGLE_NOT_CONFIGURED": 5105,
"GITHUB_NOT_CONFIGURED": 5110,
"GITLAB_NOT_CONFIGURED": 5111,
"GOOGLE_OAUTH_PROVIDER_ERROR": 5115,
"GITHUB_OAUTH_PROVIDER_ERROR": 5120,
"GITLAB_OAUTH_PROVIDER_ERROR": 5121,
# Reset Password
"INVALID_PASSWORD_TOKEN": 5125,
"EXPIRED_PASSWORD_TOKEN": 5130,

View File

@ -62,11 +62,7 @@ class OauthAdapter(Adapter):
response.raise_for_status()
return response.json()
except requests.RequestException:
code = (
"GOOGLE_OAUTH_PROVIDER_ERROR"
if self.provider == "google"
else "GITHUB_OAUTH_PROVIDER_ERROR"
)
code = self._provider_error_code()
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[code],
error_message=str(code),
@ -83,8 +79,12 @@ class OauthAdapter(Adapter):
except requests.RequestException:
if self.provider == "google":
code = "GOOGLE_OAUTH_PROVIDER_ERROR"
if self.provider == "github":
elif self.provider == "github":
code = "GITHUB_OAUTH_PROVIDER_ERROR"
elif self.provider == "gitlab":
code = "GITLAB_OAUTH_PROVIDER_ERROR"
else:
code = "OAUTH_NOT_CONFIGURED"
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[code],

View 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,
},
}
)

View File

@ -8,6 +8,8 @@ from .views import (
ChangePasswordEndpoint,
# App
EmailCheckEndpoint,
GitLabCallbackEndpoint,
GitLabOauthInitiateEndpoint,
GitHubCallbackEndpoint,
GitHubOauthInitiateEndpoint,
GoogleCallbackEndpoint,
@ -22,6 +24,8 @@ from .views import (
ResetPasswordSpaceEndpoint,
# Space
EmailCheckSpaceEndpoint,
GitLabCallbackSpaceEndpoint,
GitLabOauthInitiateSpaceEndpoint,
GitHubCallbackSpaceEndpoint,
GitHubOauthInitiateSpaceEndpoint,
GoogleCallbackSpaceEndpoint,
@ -151,6 +155,27 @@ urlpatterns = [
GitHubCallbackSpaceEndpoint.as_view(),
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
path(
"email-check/",

View File

@ -14,6 +14,10 @@ from .app.github import (
GitHubCallbackEndpoint,
GitHubOauthInitiateEndpoint,
)
from .app.gitlab import (
GitLabCallbackEndpoint,
GitLabOauthInitiateEndpoint,
)
from .app.google import (
GoogleCallbackEndpoint,
GoogleOauthInitiateEndpoint,
@ -34,6 +38,11 @@ from .space.github import (
GitHubOauthInitiateSpaceEndpoint,
)
from .space.gitlab import (
GitLabCallbackSpaceEndpoint,
GitLabOauthInitiateSpaceEndpoint,
)
from .space.google import (
GoogleCallbackSpaceEndpoint,
GoogleOauthInitiateSpaceEndpoint,

View 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)

View 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)

View File

@ -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),
),
]

View File

@ -10,7 +10,7 @@ from .base import BaseModel
class SocialLoginConnection(BaseModel):
medium = models.CharField(
max_length=20,
choices=(("Google", "google"), ("Github", "github"), ("Jira", "jira")),
choices=(("Google", "google"), ("Github", "github"), ("GitLab", "gitlab"), ("Jira", "jira")),
default=None,
)
last_login_at = models.DateTimeField(default=timezone.now, null=True)

View File

@ -182,7 +182,7 @@ class Account(TimeAuditModel):
)
provider_account_id = models.CharField(max_length=255)
provider = models.CharField(
choices=(("google", "Google"), ("github", "Github")),
choices=(("google", "Google"), ("github", "Github"), ("gitlab", "GitLab")),
)
access_token = models.TextField()
access_token_expired_at = models.DateTimeField(null=True)

View File

@ -54,6 +54,7 @@ class InstanceEndpoint(BaseAPIView):
IS_GOOGLE_ENABLED,
IS_GITHUB_ENABLED,
GITHUB_APP_NAME,
IS_GITLAB_ENABLED,
EMAIL_HOST,
ENABLE_MAGIC_LINK_LOGIN,
ENABLE_EMAIL_PASSWORD,
@ -76,6 +77,10 @@ class InstanceEndpoint(BaseAPIView):
"key": "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",
"default": os.environ.get("EMAIL_HOST", ""),
@ -115,6 +120,7 @@ class InstanceEndpoint(BaseAPIView):
# Authentication
data["is_google_enabled"] = IS_GOOGLE_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_email_password_enabled"] = ENABLE_EMAIL_PASSWORD == "1"

View File

@ -65,6 +65,24 @@ class Command(BaseCommand):
"category": "GITHUB",
"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",
"value": os.environ.get("EMAIL_HOST", ""),
@ -151,7 +169,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():
for key in keys:
if key == "IS_GOOGLE_ENABLED":
@ -222,6 +240,46 @@ class Command(BaseCommand):
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:
for key in keys:
self.stdout.write(

View File

@ -0,0 +1,386 @@
import { ReactNode } from "react";
import Link from "next/link";
export enum EPageTypes {
PUBLIC = "PUBLIC",
NON_AUTHENTICATED = "NON_AUTHENTICATED",
SET_PASSWORD = "SET_PASSWORD",
ONBOARDING = "ONBOARDING",
AUTHENTICATED = "AUTHENTICATED",
}
export enum EAuthModes {
SIGN_IN = "SIGN_IN",
SIGN_UP = "SIGN_UP",
}
export enum EAuthSteps {
EMAIL = "EMAIL",
PASSWORD = "PASSWORD",
UNIQUE_CODE = "UNIQUE_CODE",
}
// TODO: remove this
export enum EErrorAlertType {
BANNER_ALERT = "BANNER_ALERT",
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
INLINE_EMAIL = "INLINE_EMAIL",
INLINE_PASSWORD = "INLINE_PASSWORD",
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
}
export enum EAuthErrorCodes {
// Global
INSTANCE_NOT_CONFIGURED = "5000",
INVALID_EMAIL = "5005",
EMAIL_REQUIRED = "5010",
SIGNUP_DISABLED = "5015",
MAGIC_LINK_LOGIN_DISABLED = "5017",
PASSWORD_LOGIN_DISABLED = "5019",
SMTP_NOT_CONFIGURED = "5025",
// Password strength
INVALID_PASSWORD = "5020",
// Sign Up
USER_ACCOUNT_DEACTIVATED = "5019",
USER_ALREADY_EXIST = "5030",
AUTHENTICATION_FAILED_SIGN_UP = "5035",
REQUIRED_EMAIL_PASSWORD_SIGN_UP = "5040",
INVALID_EMAIL_SIGN_UP = "5045",
INVALID_EMAIL_MAGIC_SIGN_UP = "5050",
MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED = "5055",
// Sign In
USER_DOES_NOT_EXIST = "5060",
AUTHENTICATION_FAILED_SIGN_IN = "5065",
REQUIRED_EMAIL_PASSWORD_SIGN_IN = "5070",
INVALID_EMAIL_SIGN_IN = "5075",
INVALID_EMAIL_MAGIC_SIGN_IN = "5080",
MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED = "5085",
// Both Sign in and Sign up for magic
INVALID_MAGIC_CODE = "5090",
EXPIRED_MAGIC_CODE = "5095",
EMAIL_CODE_ATTEMPT_EXHAUSTED = "5100",
// Oauth
OAUTH_NOT_CONFIGURED = "5104",
GOOGLE_NOT_CONFIGURED = "5105",
GITHUB_NOT_CONFIGURED = "5110",
GITLAB_NOT_CONFIGURED = "5111",
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
GITLAB_OAUTH_PROVIDER_ERROR = "5121",
// Reset Password
INVALID_PASSWORD_TOKEN = "5125",
EXPIRED_PASSWORD_TOKEN = "5130",
// Change password
INCORRECT_OLD_PASSWORD = "5135",
MISSING_PASSWORD= "5138",
INVALID_NEW_PASSWORD = "5140",
// set passowrd
PASSWORD_ALREADY_SET = "5145",
// Admin
ADMIN_ALREADY_EXIST = "5150",
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5155",
INVALID_ADMIN_EMAIL = "5160",
INVALID_ADMIN_PASSWORD = "5165",
REQUIRED_ADMIN_EMAIL_PASSWORD = "5170",
ADMIN_AUTHENTICATION_FAILED = "5175",
ADMIN_USER_ALREADY_EXIST = "5180",
ADMIN_USER_DOES_NOT_EXIST = "5185",
}
export type TAuthErrorInfo = {
type: EErrorAlertType;
code: EAuthErrorCodes;
title: string;
message: ReactNode;
};
const errorCodeMessages: {
[key in EAuthErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode };
} = {
// global
[EAuthErrorCodes.INSTANCE_NOT_CONFIGURED]: {
title: `Instance not configured`,
message: () => `Instance not configured. Please contact your administrator.`,
},
[EAuthErrorCodes.SIGNUP_DISABLED]: {
title: `Sign up disabled`,
message: () => `Sign up disabled. Please contact your administrator.`,
},
[EAuthErrorCodes.INVALID_PASSWORD]: {
title: `Invalid password`,
message: () => `Invalid password. Please try again.`,
},
[EAuthErrorCodes.SMTP_NOT_CONFIGURED]: {
title: `SMTP not configured`,
message: () => `SMTP not configured. Please contact your administrator.`,
},
// email check in both sign up and sign in
[EAuthErrorCodes.INVALID_EMAIL]: {
title: `Invalid email`,
message: () => `Invalid email. Please try again.`,
},
[EAuthenticationErrorCodes.EMAIL_REQUIRED]: {
title: `Email required`,
message: () => `Email required. Please try again.`,
},
// sign up
[EAuthenticationErrorCodes.USER_ALREADY_EXIST]: {
title: `User already exists`,
message: (email = undefined) => (
<div>
Your account is already registered.&nbsp;
<Link
className="underline underline-offset-4 font-medium hover:font-bold transition-all"
href={`/sign-in${email ? `?email=${encodeURIComponent(email)}` : ``}`}
>
Sign In
</Link>
&nbsp;now.
</div>
),
},
[EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP]: {
title: `Email and password required`,
message: () => `Email and password required. Please try again.`,
},
[EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP]: {
title: `Authentication failed`,
message: () => `Authentication failed. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP]: {
title: `Invalid email`,
message: () => `Invalid email. Please try again.`,
},
[EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED]: {
title: `Email and code required`,
message: () => `Email and code required. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP]: {
title: `Invalid email`,
message: () => `Invalid email. Please try again.`,
},
// sign in
[EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED]: {
title: `User account deactivated`,
message: () => <div>Your account is deactivated. Contact support@plane.so.</div>,
},
[EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: {
title: `User does not exist`,
message: (email = undefined) => (
<div>
No account found.&nbsp;
<Link
className="underline underline-offset-4 font-medium hover:font-bold transition-all"
href={`/${email ? `?email=${encodeURIComponent(email)}` : ``}`}
>
Create one
</Link>
&nbsp;to get started.
</div>
),
},
[EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN]: {
title: `Email and password required`,
message: () => `Email and password required. Please try again.`,
},
[EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN]: {
title: `Authentication failed`,
message: () => `Authentication failed. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN]: {
title: `Invalid email`,
message: () => `Invalid email. Please try again.`,
},
[EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED]: {
title: `Email and code required`,
message: () => `Email and code required. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN]: {
title: `Invalid email`,
message: () => `Invalid email. Please try again.`,
},
// Both Sign in and Sign up
[EAuthenticationErrorCodes.INVALID_MAGIC_CODE]: {
title: `Authentication failed`,
message: () => `Invalid magic code. Please try again.`,
},
[EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE]: {
title: `Expired magic code`,
message: () => `Expired magic code. Please try again.`,
},
[EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED]: {
title: `Expired magic code`,
message: () => `Expired magic code. Please try again.`,
},
// Oauth
[EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED]: {
title: `OAuth not configured`,
message: () => `OAuth not configured. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: {
title: `Google not configured`,
message: () => `Google not configured. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED]: {
title: `GitHub not configured`,
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]: {
title: `Google OAuth provider error`,
message: () => `Google OAuth provider error. Please try again.`,
},
[EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR]: {
title: `GitHub OAuth provider error`,
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
[EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: {
title: `Invalid password token`,
message: () => `Invalid password token. Please try again.`,
},
[EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN]: {
title: `Expired password token`,
message: () => `Expired password token. Please try again.`,
},
// Change password
[EAuthenticationErrorCodes.MISSING_PASSWORD]: {
title: `Password required`,
message: () => `Password required. Please try again.`,
},
[EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD]: {
title: `Incorrect old password`,
message: () => `Incorrect old password. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_NEW_PASSWORD]: {
title: `Invalid new password`,
message: () => `Invalid new password. Please try again.`,
},
// set password
[EAuthenticationErrorCodes.PASSWORD_ALREADY_SET]: {
title: `Password already set`,
message: () => `Password already set. Please try again.`,
},
// admin
[EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: {
title: `Admin already exists`,
message: () => `Admin already exists. Please try again.`,
},
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
title: `Email, password and first name required`,
message: () => `Email, password and first name required. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: {
title: `Invalid admin email`,
message: () => `Invalid admin email. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: {
title: `Invalid admin password`,
message: () => `Invalid admin password. Please try again.`,
},
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
title: `Email and password required`,
message: () => `Email and password required. Please try again.`,
},
[EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
title: `Authentication failed`,
message: () => `Authentication failed. Please try again.`,
},
[EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST]: {
title: `Admin user already exists`,
message: () => (
<div>
Admin user already exists.&nbsp;
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
</div>
),
},
[EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: {
title: `Admin user does not exist`,
message: () => (
<div>
Admin user does not exist.&nbsp;
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
</div>
),
},
};
export const authErrorHandler = (
errorCode: EAuthenticationErrorCodes,
email?: string | undefined
): TAuthErrorInfo | undefined => {
const bannerAlertErrorCodes = [
EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED,
EAuthenticationErrorCodes.INVALID_EMAIL,
EAuthenticationErrorCodes.EMAIL_REQUIRED,
EAuthenticationErrorCodes.SIGNUP_DISABLED,
EAuthenticationErrorCodes.INVALID_PASSWORD,
EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED,
EAuthenticationErrorCodes.USER_ALREADY_EXIST,
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP,
EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP,
EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP,
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP,
EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED,
EAuthenticationErrorCodes.USER_DOES_NOT_EXIST,
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN,
EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN,
EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN,
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN,
EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED,
EAuthenticationErrorCodes.INVALID_MAGIC_CODE,
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE,
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED,
EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED,
EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED,
EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR,
EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR,
EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN,
EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN,
EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD,
EAuthenticationErrorCodes.INVALID_NEW_PASSWORD,
EAuthenticationErrorCodes.PASSWORD_ALREADY_SET,
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST,
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL,
EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD,
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD,
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST,
EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
];
if (bannerAlertErrorCodes.includes(errorCode))
return {
type: EErrorAlertType.BANNER_ALERT,
code: errorCode,
title: errorCodeMessages[errorCode]?.title || "Error",
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
};
return undefined;
};

View File

@ -4,7 +4,7 @@ export type TCurrentUserAccount = {
user: string | undefined;
provider_account_id: string | undefined;
provider: "google" | "github" | string | undefined;
provider: "google" | "github" | "gitlab" | string | undefined;
access_token: string | undefined;
access_token_expired_at: Date | undefined;
refresh_token: string | undefined;

View File

@ -3,7 +3,8 @@ export type TInstanceAuthenticationMethodKeys =
| "ENABLE_MAGIC_LINK_LOGIN"
| "ENABLE_EMAIL_PASSWORD"
| "IS_GOOGLE_ENABLED"
| "IS_GITHUB_ENABLED";
| "IS_GITHUB_ENABLED"
| "IS_GITLAB_ENABLED";
export type TInstanceGoogleAuthenticationConfigurationKeys =
| "GOOGLE_CLIENT_ID"
@ -13,9 +14,15 @@ export type TInstanceGithubAuthenticationConfigurationKeys =
| "GITHUB_CLIENT_ID"
| "GITHUB_CLIENT_SECRET";
export type TInstanceGitlabAuthenticationConfigurationKeys =
| "GITLAB_HOST"
| "GITLAB_CLIENT_ID"
| "GITLAB_CLIENT_SECRET";
type TInstanceAuthenticationConfigurationKeys =
| TInstanceGoogleAuthenticationConfigurationKeys
| TInstanceGithubAuthenticationConfigurationKeys;
| TInstanceGithubAuthenticationConfigurationKeys
| TInstanceGitlabAuthenticationConfigurationKeys;
export type TInstanceAuthenticationKeys =
| TInstanceAuthenticationMethodKeys

View File

@ -38,6 +38,7 @@ export interface IInstance {
export interface IInstanceConfig {
is_google_enabled: boolean;
is_github_enabled: boolean;
is_gitlab_enabled: boolean;
is_magic_login_enabled: boolean;
is_email_password_enabled: boolean;
github_app_name: string | undefined;

View File

@ -5,7 +5,7 @@ import {
TStateGroups,
} from ".";
type TLoginMediums = "email" | "magic-code" | "github" | "google";
type TLoginMediums = "email" | "magic-code" | "github" | "gitlab" | "google";
export interface IUser {
id: string;

View 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>
);

View File

@ -12,6 +12,7 @@ export * from "./dice-icon";
export * from "./discord-icon";
export * from "./full-screen-panel-icon";
export * from "./github-icon";
export * from "./gitlab-icon";
export * from "./layer-stack";
export * from "./layers-icon";
export * from "./photo-filter-icon";

View File

@ -85,7 +85,7 @@ export const AuthRoot: FC = observer(() => {
const isSMTPConfigured = config?.is_smtp_configured || false;
const isMagicLoginEnabled = config?.is_magic_login_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
const handleEmailVerification = async (data: IEmailCheckData) => {

View 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>
);
};

View File

@ -1,3 +1,4 @@
export * from "./oauth-options";
export * from "./google-button";
export * from "./github-button";
export * from "./gitlab-button";

View File

@ -1,6 +1,6 @@
import { observer } from "mobx-react-lite";
// components
import { GithubOAuthButton, GoogleOAuthButton } from "@/components/account";
import { GithubOAuthButton, GitlabOAuthButton, GoogleOAuthButton } from "@/components/account";
// hooks
import { useInstance } from "@/hooks/store";
@ -22,6 +22,7 @@ export const OAuthOptions: React.FC = observer(() => {
</div>
)}
{config?.is_github_enabled && <GithubOAuthButton text="Sign in with Github" />}
{config?.is_gitlab_enabled && <GitlabOAuthButton text="Sign in with GitLab" />}
</div>
</>
);

View File

@ -52,10 +52,13 @@ export enum EAuthenticationErrorCodes {
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN = "5100",
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP = "5102",
// Oauth
OAUTH_NOT_CONFIGURED = "5104",
GOOGLE_NOT_CONFIGURED = "5105",
GITHUB_NOT_CONFIGURED = "5110",
GITLAB_NOT_CONFIGURED = "5111",
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
GITLAB_OAUTH_PROVIDER_ERROR = "5121",
// Reset Password
INVALID_PASSWORD_TOKEN = "5125",
EXPIRED_PASSWORD_TOKEN = "5130",
@ -220,6 +223,10 @@ const errorCodeMessages: {
},
// Oauth
[EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED]: {
title: `OAuth not configured`,
message: () => `OAuth not configured. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: {
title: `Google not configured`,
message: () => `Google not configured. Please contact your administrator.`,
@ -228,6 +235,10 @@ const errorCodeMessages: {
title: `GitHub not configured`,
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]: {
title: `Google OAuth provider error`,
message: () => `Google OAuth provider error. Please try again.`,
@ -236,6 +247,10 @@ const errorCodeMessages: {
title: `GitHub OAuth provider error`,
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
[EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: {
@ -347,10 +362,13 @@ export const authErrorHandler = (
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP,
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN,
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP,
EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED,
EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED,
EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED,
EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED,
EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR,
EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR,
EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR,
EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN,
EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN,
EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD,

View 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

View 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>
);
};

View File

@ -1,3 +1,4 @@
export * from "./oauth-options";
export * from "./google-button";
export * from "./github-button";
export * from "./gitlab-button";

View File

@ -1,6 +1,6 @@
import { observer } from "mobx-react";
// components
import { GithubOAuthButton, GoogleOAuthButton } from "@/components/account";
import { GithubOAuthButton, GitlabOAuthButton, GoogleOAuthButton } from "@/components/account";
// hooks
import { useInstance } from "@/hooks/store";
@ -12,7 +12,7 @@ export const OAuthOptions: React.FC<TOAuthOptionProps> = observer(() => {
// hooks
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;
@ -30,6 +30,7 @@ export const OAuthOptions: React.FC<TOAuthOptionProps> = observer(() => {
</div>
)}
{config?.is_github_enabled && <GithubOAuthButton text="Continue with Github" />}
{config?.is_gitlab_enabled && <GitlabOAuthButton text="Continue with GitLab" />}
</div>
</>
);

View File

@ -64,10 +64,13 @@ export enum EAuthenticationErrorCodes {
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN = "5100",
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP = "5102",
// Oauth
OAUTH_NOT_CONFIGURED = "5104",
GOOGLE_NOT_CONFIGURED = "5105",
GITHUB_NOT_CONFIGURED = "5110",
GITLAB_NOT_CONFIGURED = "5111",
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
GITLAB_OAUTH_PROVIDER_ERROR = "5121",
// Reset Password
INVALID_PASSWORD_TOKEN = "5125",
EXPIRED_PASSWORD_TOKEN = "5130",
@ -239,6 +242,10 @@ const errorCodeMessages: {
},
// Oauth
[EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED]: {
title: `OAuth not configured`,
message: () => `OAuth not configured. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: {
title: `Google not configured`,
message: () => `Google not configured. Please contact your administrator.`,
@ -247,6 +254,10 @@ const errorCodeMessages: {
title: `GitHub not configured`,
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]: {
title: `Google OAuth provider error`,
message: () => `Google OAuth provider error. Please try again.`,
@ -255,6 +266,10 @@ const errorCodeMessages: {
title: `GitHub OAuth provider error`,
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
[EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: {
@ -377,10 +392,13 @@ export const authErrorHandler = (
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP,
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN,
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP,
EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED,
EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED,
EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED,
EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED,
EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR,
EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR,
EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR,
EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN,
EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN,
EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD,

View 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