From 99e1963d9bf3eb9664bb784d99da88204e497a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jon=20=E2=9A=9D?= Date: Fri, 14 Jun 2024 11:25:59 +0200 Subject: [PATCH] feat: add GitLab OAuth client (#4692) --- .../components/gitlab-config.tsx | 59 +++++ admin/app/authentication/components/index.ts | 1 + admin/app/authentication/gitlab/form.tsx | 212 ++++++++++++++++++ admin/app/authentication/gitlab/page.tsx | 101 +++++++++ admin/app/authentication/page.tsx | 9 + admin/components/auth-header.tsx | 2 + admin/public/logos/gitlab-logo.svg | 1 + apiserver/bin/docker-entrypoint-beat.sh | 0 apiserver/bin/docker-entrypoint-migrator.sh | 0 .../plane/authentication/adapter/error.py | 3 + .../plane/authentication/adapter/oauth.py | 12 +- .../authentication/provider/oauth/gitlab.py | 145 ++++++++++++ apiserver/plane/authentication/urls.py | 25 +++ .../plane/authentication/views/__init__.py | 9 + .../plane/authentication/views/app/gitlab.py | 131 +++++++++++ .../authentication/views/space/gitlab.py | 109 +++++++++ .../0067_alter_account_provider_and_more.py | 23 ++ .../plane/db/models/social_connection.py | 2 +- apiserver/plane/db/models/user.py | 2 +- apiserver/plane/license/api/views/instance.py | 6 + .../management/commands/configure_instance.py | 60 ++++- packages/constants/src/auth.ts | 15 ++ packages/types/src/current-user/accounts.d.ts | 2 +- packages/types/src/instance/auth.d.ts | 11 +- packages/types/src/instance/base.d.ts | 1 + packages/types/src/users.d.ts | 2 +- packages/ui/src/icons/gitlab-icon.tsx | 28 +++ packages/ui/src/icons/index.ts | 1 + .../account/auth-forms/auth-root.tsx | 2 +- .../account/oauth/gitlab-button.tsx | 36 +++ space/components/account/oauth/index.ts | 1 + .../account/oauth/oauth-options.tsx | 3 +- space/helpers/authentication.helper.tsx | 18 ++ space/public/logos/gitlab-logo.svg | 1 + space/types/app.d.ts | 1 + .../account/oauth/gitlab-button.tsx | 36 +++ web/components/account/oauth/index.ts | 1 + .../account/oauth/oauth-options.tsx | 5 +- web/helpers/authentication.helper.tsx | 18 ++ web/public/logos/gitlab-logo.svg | 1 + 40 files changed, 1078 insertions(+), 17 deletions(-) create mode 100644 admin/app/authentication/components/gitlab-config.tsx create mode 100644 admin/app/authentication/gitlab/form.tsx create mode 100644 admin/app/authentication/gitlab/page.tsx create mode 100644 admin/public/logos/gitlab-logo.svg mode change 100644 => 100755 apiserver/bin/docker-entrypoint-beat.sh mode change 100644 => 100755 apiserver/bin/docker-entrypoint-migrator.sh create mode 100644 apiserver/plane/authentication/provider/oauth/gitlab.py create mode 100644 apiserver/plane/authentication/views/app/gitlab.py create mode 100644 apiserver/plane/authentication/views/space/gitlab.py create mode 100644 apiserver/plane/db/migrations/0067_alter_account_provider_and_more.py create mode 100644 packages/ui/src/icons/gitlab-icon.tsx create mode 100644 space/components/account/oauth/gitlab-button.tsx create mode 100644 space/public/logos/gitlab-logo.svg create mode 100644 web/components/account/oauth/gitlab-button.tsx create mode 100644 web/public/logos/gitlab-logo.svg diff --git a/admin/app/authentication/components/gitlab-config.tsx b/admin/app/authentication/components/gitlab-config.tsx new file mode 100644 index 000000000..a16645649 --- /dev/null +++ b/admin/app/authentication/components/gitlab-config.tsx @@ -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 = 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 ? ( +
+ + Edit + + { + Boolean(parseInt(enableGitlabConfig)) === true + ? updateConfig("IS_GITLAB_ENABLED", "0") + : updateConfig("IS_GITLAB_ENABLED", "1"); + }} + size="sm" + disabled={disabled} + /> +
+ ) : ( + + + Configure + + )} + + ); +}); diff --git a/admin/app/authentication/components/index.ts b/admin/app/authentication/components/index.ts index d76d61f57..2c13b7728 100644 --- a/admin/app/authentication/components/index.ts +++ b/admin/app/authentication/components/index.ts @@ -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"; diff --git a/admin/app/authentication/gitlab/form.tsx b/admin/app/authentication/gitlab/form.tsx new file mode 100644 index 000000000..2cb46baf9 --- /dev/null +++ b/admin/app/authentication/gitlab/form.tsx @@ -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; + +export const InstanceGitlabConfigForm: FC = (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({ + 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 GitLab host to use for login, including scheme. + + ), + 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{" "} + + GitLab OAuth application settings + + . + + ), + 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{" "} + + GitLab OAuth application settings + + . + + ), + 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 Redirect URI field of your{" "} + + GitLab OAuth application + + . + + ), + }, + ]; + + const onSubmit = async (formData: GitlabConfigFormValues) => { + const payload: Partial = { ...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) => { + if (isDirty) { + e.preventDefault(); + setIsDiscardChangesModalOpen(true); + } + }; + + return ( + <> + setIsDiscardChangesModalOpen(false)} + /> +
+
+
+
Configuration
+ {GITLAB_FORM_FIELDS.map((field) => ( + + ))} +
+
+ + + Go back + +
+
+
+
+
+
Service provider details
+ {GITLAB_SERVICE_FIELD.map((field) => ( + + ))} +
+
+
+
+ + ); +}; diff --git a/admin/app/authentication/gitlab/page.tsx b/admin/app/authentication/gitlab/page.tsx new file mode 100644 index 000000000..48a08f5b0 --- /dev/null +++ b/admin/app/authentication/gitlab/page.tsx @@ -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(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 ( + <> + +
+
+ } + config={ + { + Boolean(parseInt(enableGitlabConfig)) === true + ? updateConfig("IS_GITLAB_ENABLED", "0") + : updateConfig("IS_GITLAB_ENABLED", "1"); + }} + size="sm" + disabled={isSubmitting || !formattedConfig} + /> + } + disabled={isSubmitting || !formattedConfig} + withBorder={false} + /> +
+
+ {formattedConfig ? ( + + ) : ( + + + + + + + + )} +
+
+ + ); +}); + +export default InstanceGitlabAuthenticationPage; diff --git a/admin/app/authentication/page.tsx b/admin/app/authentication/page.tsx index c44b74b49..883796094 100644 --- a/admin/app/authentication/page.tsx +++ b/admin/app/authentication/page.tsx @@ -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: , }, + { + key: "gitlab", + name: "GitLab", + description: "Allow members to login or sign up to plane with their GitLab accounts.", + icon: GitLab Logo, + config: , + }, ]; return ( diff --git a/admin/components/auth-header.tsx b/admin/components/auth-header.tsx index 4becf928f..c2f64468d 100644 --- a/admin/components/auth-header.tsx +++ b/admin/components/auth-header.tsx @@ -31,6 +31,8 @@ export const InstanceHeader: FC = observer(() => { return "Google"; case "github": return "Github"; + case "gitlab": + return "GitLab"; default: return pathName.toUpperCase(); } diff --git a/admin/public/logos/gitlab-logo.svg b/admin/public/logos/gitlab-logo.svg new file mode 100644 index 000000000..dab4d8b74 --- /dev/null +++ b/admin/public/logos/gitlab-logo.svg @@ -0,0 +1 @@ + diff --git a/apiserver/bin/docker-entrypoint-beat.sh b/apiserver/bin/docker-entrypoint-beat.sh old mode 100644 new mode 100755 diff --git a/apiserver/bin/docker-entrypoint-migrator.sh b/apiserver/bin/docker-entrypoint-migrator.sh old mode 100644 new mode 100755 diff --git a/apiserver/plane/authentication/adapter/error.py b/apiserver/plane/authentication/adapter/error.py index 55ff10988..90e236a80 100644 --- a/apiserver/plane/authentication/adapter/error.py +++ b/apiserver/plane/authentication/adapter/error.py @@ -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, diff --git a/apiserver/plane/authentication/adapter/oauth.py b/apiserver/plane/authentication/adapter/oauth.py index b1a92e79e..1878126d9 100644 --- a/apiserver/plane/authentication/adapter/oauth.py +++ b/apiserver/plane/authentication/adapter/oauth.py @@ -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], diff --git a/apiserver/plane/authentication/provider/oauth/gitlab.py b/apiserver/plane/authentication/provider/oauth/gitlab.py new file mode 100644 index 000000000..a251e5f8b --- /dev/null +++ b/apiserver/plane/authentication/provider/oauth/gitlab.py @@ -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, + }, + } + ) diff --git a/apiserver/plane/authentication/urls.py b/apiserver/plane/authentication/urls.py index ee860f41f..a375d94cb 100644 --- a/apiserver/plane/authentication/urls.py +++ b/apiserver/plane/authentication/urls.py @@ -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/", diff --git a/apiserver/plane/authentication/views/__init__.py b/apiserver/plane/authentication/views/__init__.py index 51ea3e60a..af58a9cbd 100644 --- a/apiserver/plane/authentication/views/__init__.py +++ b/apiserver/plane/authentication/views/__init__.py @@ -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, diff --git a/apiserver/plane/authentication/views/app/gitlab.py b/apiserver/plane/authentication/views/app/gitlab.py new file mode 100644 index 000000000..02a44aeb4 --- /dev/null +++ b/apiserver/plane/authentication/views/app/gitlab.py @@ -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) diff --git a/apiserver/plane/authentication/views/space/gitlab.py b/apiserver/plane/authentication/views/space/gitlab.py new file mode 100644 index 000000000..7ebd9d187 --- /dev/null +++ b/apiserver/plane/authentication/views/space/gitlab.py @@ -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) diff --git a/apiserver/plane/db/migrations/0067_alter_account_provider_and_more.py b/apiserver/plane/db/migrations/0067_alter_account_provider_and_more.py new file mode 100644 index 000000000..c8f7571f2 --- /dev/null +++ b/apiserver/plane/db/migrations/0067_alter_account_provider_and_more.py @@ -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), + ), + ] diff --git a/apiserver/plane/db/models/social_connection.py b/apiserver/plane/db/models/social_connection.py index 96fbbb967..2a21c55fd 100644 --- a/apiserver/plane/db/models/social_connection.py +++ b/apiserver/plane/db/models/social_connection.py @@ -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) diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index c083b631c..2a88df8b6 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -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) diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index 1ec09fbb5..8d885083a 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -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" diff --git a/apiserver/plane/license/management/commands/configure_instance.py b/apiserver/plane/license/management/commands/configure_instance.py index 5a6eadc2e..350dabee7 100644 --- a/apiserver/plane/license/management/commands/configure_instance.py +++ b/apiserver/plane/license/management/commands/configure_instance.py @@ -59,6 +59,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", ""), @@ -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(): for key in keys: if key == "IS_GOOGLE_ENABLED": @@ -216,6 +234,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( diff --git a/packages/constants/src/auth.ts b/packages/constants/src/auth.ts index c12b63d63..a91b1cc78 100644 --- a/packages/constants/src/auth.ts +++ b/packages/constants/src/auth.ts @@ -60,10 +60,13 @@ export enum EAuthErrorCodes { 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", @@ -215,6 +218,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.`, @@ -223,6 +230,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.`, @@ -231,6 +242,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]: { diff --git a/packages/types/src/current-user/accounts.d.ts b/packages/types/src/current-user/accounts.d.ts index 6c5146a7a..d328f0529 100644 --- a/packages/types/src/current-user/accounts.d.ts +++ b/packages/types/src/current-user/accounts.d.ts @@ -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; diff --git a/packages/types/src/instance/auth.d.ts b/packages/types/src/instance/auth.d.ts index 0366ce660..67f6b9f41 100644 --- a/packages/types/src/instance/auth.d.ts +++ b/packages/types/src/instance/auth.d.ts @@ -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 diff --git a/packages/types/src/instance/base.d.ts b/packages/types/src/instance/base.d.ts index 53804dec3..1332102b7 100644 --- a/packages/types/src/instance/base.d.ts +++ b/packages/types/src/instance/base.d.ts @@ -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; diff --git a/packages/types/src/users.d.ts b/packages/types/src/users.d.ts index 3adec55d1..90967df16 100644 --- a/packages/types/src/users.d.ts +++ b/packages/types/src/users.d.ts @@ -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; diff --git a/packages/ui/src/icons/gitlab-icon.tsx b/packages/ui/src/icons/gitlab-icon.tsx new file mode 100644 index 000000000..958a68641 --- /dev/null +++ b/packages/ui/src/icons/gitlab-icon.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const GitlabIcon: React.FC = ({ width = "24", height = "24", className, color }) => ( + + + + + + + + + + +); diff --git a/packages/ui/src/icons/index.ts b/packages/ui/src/icons/index.ts index c51375282..0cd98fe61 100644 --- a/packages/ui/src/icons/index.ts +++ b/packages/ui/src/icons/index.ts @@ -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"; diff --git a/space/components/account/auth-forms/auth-root.tsx b/space/components/account/auth-forms/auth-root.tsx index e02947af0..7f0a7dbdb 100644 --- a/space/components/account/auth-forms/auth-root.tsx +++ b/space/components/account/auth-forms/auth-root.tsx @@ -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) => { diff --git a/space/components/account/oauth/gitlab-button.tsx b/space/components/account/oauth/gitlab-button.tsx new file mode 100644 index 000000000..072a2f628 --- /dev/null +++ b/space/components/account/oauth/gitlab-button.tsx @@ -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 = (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 ( + + ); +}; diff --git a/space/components/account/oauth/index.ts b/space/components/account/oauth/index.ts index ff953cf20..535e21b14 100644 --- a/space/components/account/oauth/index.ts +++ b/space/components/account/oauth/index.ts @@ -1,3 +1,4 @@ export * from "./oauth-options"; export * from "./google-button"; export * from "./github-button"; +export * from "./gitlab-button"; diff --git a/space/components/account/oauth/oauth-options.tsx b/space/components/account/oauth/oauth-options.tsx index 011c7f189..af35452d9 100644 --- a/space/components/account/oauth/oauth-options.tsx +++ b/space/components/account/oauth/oauth-options.tsx @@ -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(() => { )} {config?.is_github_enabled && } + {config?.is_gitlab_enabled && } ); diff --git a/space/helpers/authentication.helper.tsx b/space/helpers/authentication.helper.tsx index 0e5ab0186..409a75150 100644 --- a/space/helpers/authentication.helper.tsx +++ b/space/helpers/authentication.helper.tsx @@ -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, diff --git a/space/public/logos/gitlab-logo.svg b/space/public/logos/gitlab-logo.svg new file mode 100644 index 000000000..dab4d8b74 --- /dev/null +++ b/space/public/logos/gitlab-logo.svg @@ -0,0 +1 @@ + diff --git a/space/types/app.d.ts b/space/types/app.d.ts index bd4af3b0c..1f7851995 100644 --- a/space/types/app.d.ts +++ b/space/types/app.d.ts @@ -4,6 +4,7 @@ export interface IAppConfig { google_client_id: string | null; github_app_name: string | null; github_client_id: string | null; + gitlab_client_id: string | null; magic_login: boolean; slack_client_id: string | null; posthog_api_key: string | null; diff --git a/web/components/account/oauth/gitlab-button.tsx b/web/components/account/oauth/gitlab-button.tsx new file mode 100644 index 000000000..a9ffb01b9 --- /dev/null +++ b/web/components/account/oauth/gitlab-button.tsx @@ -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 = (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 ( + + ); +}; diff --git a/web/components/account/oauth/index.ts b/web/components/account/oauth/index.ts index ff953cf20..535e21b14 100644 --- a/web/components/account/oauth/index.ts +++ b/web/components/account/oauth/index.ts @@ -1,3 +1,4 @@ export * from "./oauth-options"; export * from "./google-button"; export * from "./github-button"; +export * from "./gitlab-button"; diff --git a/web/components/account/oauth/oauth-options.tsx b/web/components/account/oauth/oauth-options.tsx index 2cdc8531b..10396f239 100644 --- a/web/components/account/oauth/oauth-options.tsx +++ b/web/components/account/oauth/oauth-options.tsx @@ -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 = 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 = observer(() => { )} {config?.is_github_enabled && } + {config?.is_gitlab_enabled && } ); diff --git a/web/helpers/authentication.helper.tsx b/web/helpers/authentication.helper.tsx index f2438299e..0cb2c80a7 100644 --- a/web/helpers/authentication.helper.tsx +++ b/web/helpers/authentication.helper.tsx @@ -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, diff --git a/web/public/logos/gitlab-logo.svg b/web/public/logos/gitlab-logo.svg new file mode 100644 index 000000000..dab4d8b74 --- /dev/null +++ b/web/public/logos/gitlab-logo.svg @@ -0,0 +1 @@ +