diff --git a/apps/builder/assets/logos/GitlabLogo.tsx b/apps/builder/assets/logos/GitlabLogo.tsx new file mode 100644 index 000000000..97d0253c6 --- /dev/null +++ b/apps/builder/assets/logos/GitlabLogo.tsx @@ -0,0 +1,13 @@ +import { IconProps, Icon } from '@chakra-ui/react' + +export const GitlabLogo = (props: IconProps) => ( + + + + + + + + + +) diff --git a/apps/builder/assets/logos/index.tsx b/apps/builder/assets/logos/index.tsx index e582658dd..454eacdcb 100644 --- a/apps/builder/assets/logos/index.tsx +++ b/apps/builder/assets/logos/index.tsx @@ -1,4 +1,5 @@ export * from './GiphyLogo' +export * from './GitlabLogo' export * from './GoogleAnalyticsLogo' export * from './GoogleSheetsLogo' export * from './GtmLogo' diff --git a/apps/builder/components/auth/SocialLoginButtons.tsx b/apps/builder/components/auth/SocialLoginButtons.tsx index 51f167cc9..0c9805cbf 100644 --- a/apps/builder/components/auth/SocialLoginButtons.tsx +++ b/apps/builder/components/auth/SocialLoginButtons.tsx @@ -4,7 +4,7 @@ import { signIn, useSession } from 'next-auth/react' import { useRouter } from 'next/router' import React from 'react' import { stringify } from 'qs' -import { FacebookLogo, GoogleLogo } from 'assets/logos' +import { FacebookLogo, GoogleLogo, GitlabLogo } from 'assets/logos' export const SocialLoginButtons = () => { const { query } = useRouter() @@ -25,6 +25,11 @@ export const SocialLoginButtons = () => { callbackUrl: `/typebots?${stringify(query)}`, }) + const handleGitlabClick = async () => + signIn('gitlab', { + callbackUrl: `/typebots?${stringify(query)}`, + }) + return ( {process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID && ( @@ -60,6 +65,17 @@ export const SocialLoginButtons = () => { Continue with Facebook )} + {process.env.NEXT_PUBLIC_GITLAB_CLIENT_ID && ( + + )} ) } diff --git a/apps/builder/pages/api/auth/[...nextauth].ts b/apps/builder/pages/api/auth/[...nextauth].ts index a7f0583d0..81e6608ce 100644 --- a/apps/builder/pages/api/auth/[...nextauth].ts +++ b/apps/builder/pages/api/auth/[...nextauth].ts @@ -1,6 +1,7 @@ -import NextAuth from 'next-auth' +import NextAuth, { Account } from 'next-auth' import EmailProvider from 'next-auth/providers/email' import GitHubProvider from 'next-auth/providers/github' +import GitlabProvider from 'next-auth/providers/gitlab' import GoogleProvider from 'next-auth/providers/google' import FacebookProvider from 'next-auth/providers/facebook' import prisma from 'libs/prisma' @@ -60,6 +61,22 @@ if ( }) ) +if ( + process.env.NEXT_PUBLIC_GITLAB_CLIENT_ID && + process.env.GITLAB_CLIENT_SECRET +) { + const BASE_URL = process.env.GITLAB_BASE_URL || 'https://gitlab.com' + providers.push( + GitlabProvider({ + clientId: process.env.NEXT_PUBLIC_GITLAB_CLIENT_ID, + clientSecret: process.env.GITLAB_CLIENT_SECRET, + authorization: `${BASE_URL}/oauth/authorize?scope=read_api`, + token: `${BASE_URL}/oauth/token`, + userinfo: `${BASE_URL}/api/v4/user`, + }) + ) +} + const handler = (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'HEAD') { res.status(200) @@ -81,6 +98,14 @@ const handler = (req: NextApiRequest, res: NextApiResponse) => { user: userFromDb, } }, + signIn: async ({ account }) => { + const requiredGroups = getRequiredGroups(account.provider) + if (requiredGroups.length > 0) { + const userGroups = await getUserGroups(account) + return checkHasGroups(userGroups, requiredGroups) + } + return true + }, }, }) } @@ -98,4 +123,31 @@ const updateLastActivityDate = async (user: User) => { }) } +const getUserGroups = async (account: Account): Promise => { + switch (account.provider) { + case 'gitlab': { + const res = await fetch( + `${process.env.GITLAB_BASE_URL || 'https://gitlab.com'}/api/v4/groups`, + { headers: { Authorization: `Bearer ${account.access_token}` } } + ) + const userGroups = await res.json() + return userGroups.map((group: { full_path: string }) => group.full_path) + } + default: + return [] + } +} + +const getRequiredGroups = (provider: string): string[] => { + switch (provider) { + case 'gitlab': + return process.env.GITLAB_REQUIRED_GROUPS?.split(',') || [] + default: + return [] + } +} + +const checkHasGroups = (userGroups: string[], requiredGroups: string[]) => + userGroups?.some((userGroup) => requiredGroups?.includes(userGroup)) + export default withSentry(handler) diff --git a/apps/docs/docs/self-hosting/configuration.md b/apps/docs/docs/self-hosting/configuration.md index 12433e661..732745768 100644 --- a/apps/docs/docs/self-hosting/configuration.md +++ b/apps/docs/docs/self-hosting/configuration.md @@ -80,6 +80,23 @@ You can create your own GitHub OAuth app [here](https://github.com/settings/deve

+

GitLab (Auth)

+

+ +Used for authenticating with GitLab. +Follow the official GitLab guide for creating OAuth2 applications [here](https://docs.gitlab.com/ee/integration/oauth_provider.html). +The Authorization callback URL should be `$NEXTAUTH_URL/api/auth/callback/gitlab` + +| Parameter | Default | Description | +| ---------------------------- | ------------------ | ------------------------------------------------------------------------------------ | --- | +| NEXT_PUBLIC_GITLAB_CLIENT_ID | -- | Application client ID. Also used to check if it is enabled in the front-end | +| GITLAB_CLIENT_SECRET | -- | Application secret | +| GITLAB_BASE_URL | https://gitlab.com | Base URL of the GitLab instance | | +| GITLAB_REQUIRED_GROUPS | -- | Comma-separated list of groups the user has to be a direct member of, e.g. `foo,bar` | +| NEXT_PUBLIC_GITLAB_NAME | GitLab | Name of the GitLab instance, used for the SSO Login Button | + +

+

Facebook (Auth)