Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#34): add gitlab linking #37

Merged
merged 4 commits into from Jul 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Expand Up @@ -13,3 +13,7 @@ NEXTAUTH_SECRET="Replace me with `openssl rand -base64 32` generated secret"
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
GITHUB_ALLOWED_ORGANIZATIONS="BearStudio"

# GitLab OAuth https://gitlab.com/-/profile/applications
GITLAB_CLIENT_ID=""
GITLAB_CLIENT_SECRET=""
3 changes: 1 addition & 2 deletions .eslintrc.json
Expand Up @@ -2,8 +2,7 @@
"extends": ["react-app", "plugin:@next/next/recommended"],
"env": {
"node": true,
"browser": true,
"jest": true
"browser": true
},
"rules": {
"jsx-a11y/anchor-is-valid": "warn"
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/github-ci.yml
Expand Up @@ -47,8 +47,5 @@ jobs:
- name: Check coding rules and types
run: yarn lint

- name: Run unit tests
run: yarn test:ci

- name: Building
run: yarn build
1 change: 0 additions & 1 deletion .gitlab-ci.yml
Expand Up @@ -31,4 +31,3 @@ test:
- echo "Checking types"
- yarn tsc --noEmit
- echo "Running unit tests"
- yarn jest --roots src --passWithNoTests
11 changes: 0 additions & 11 deletions jest.config.js

This file was deleted.

8 changes: 1 addition & 7 deletions package.json
Expand Up @@ -10,8 +10,6 @@
},
"scripts": {
"prepare": "husky install && yarn prisma generate",
"test": "jest --roots src --watch",
"test:ci": "jest --roots src",
"dev": "next dev",
"build": "next build",
"start": "next start",
Expand Down Expand Up @@ -52,7 +50,7 @@
"isomorphic-form-data": "2.0.0",
"lodash": "4.17.21",
"next": "12.1.4",
"next-auth": "4.3.1",
"next-auth": "4.10.1",
"react": "17.0.2",
"react-color-palette": "6.2.0",
"react-currency-input-field": "3.6.4",
Expand Down Expand Up @@ -82,18 +80,15 @@
"@storybook/addon-links": "6.4.21",
"@storybook/react": "6.4.21",
"@storybook/theming": "6.4.21",
"@testing-library/jest-dom": "5.16.4",
"@testing-library/react": "12.1.4",
"@testing-library/react-hooks": "7.0.2",
"@testing-library/user-event": "13.5.0",
"@trivago/prettier-plugin-sort-imports": "3.2.0",
"@types/chroma-js": "2.1.3",
"@types/jest": "27.4.1",
"@types/node": "16.11.26",
"@typescript-eslint/eslint-plugin": "5.18.0",
"@typescript-eslint/parser": "5.18.0",
"babel-eslint": "10.1.0",
"babel-jest": "27.5.1",
"babel-loader": "8.2.4",
"css-mediaquery": "0.1.2",
"eslint": "8.13.0",
Expand All @@ -104,7 +99,6 @@
"eslint-plugin-react": "7.29.4",
"eslint-plugin-react-hooks": "4.4.0",
"husky": "7.0.4",
"jest": "27.5.1",
"lint-staged": "12.3.7",
"prettier": "2.6.2",
"prisma": "3.12.0",
Expand Down
1 change: 1 addition & 0 deletions prisma/schema.prisma
Expand Up @@ -28,6 +28,7 @@ model Account {
oauth_token_secret String?
oauth_token String?
refresh_token_expires_in Int?
created_at Int?

user User @relation(fields: [userId], references: [id], onDelete: Cascade)

Expand Down
14 changes: 0 additions & 14 deletions src/app/App.spec.tsx

This file was deleted.

12 changes: 12 additions & 0 deletions src/app/App.tsx
Expand Up @@ -13,6 +13,9 @@ import { Error404, ErrorBoundary } from '@/errors';

const IssuesRoutes = React.lazy(() => import('@/app/issues/IssuesRoutes'));
const ScopesRoutes = React.lazy(() => import('@/app/scopes/ScopesRoutes'));
const AccountsRoutes = React.lazy(
() => import('@/app/accounts/AccountsRoutes')
);

export const App = () => {
return (
Expand Down Expand Up @@ -50,6 +53,15 @@ export const App = () => {
}
/>

<Route
path="accounts/*"
element={
<AuthenticatedRouteGuard>
<AccountsRoutes />
</AuthenticatedRouteGuard>
}
/>

<Route path="*" element={<Error404 />} />
</Routes>
</Suspense>
Expand Down
16 changes: 16 additions & 0 deletions src/app/accounts/AccountsRoutes.tsx
@@ -0,0 +1,16 @@
import { Route, Routes } from 'react-router-dom';

import { Error404 } from '@/errors';

import { PageAccounts } from './PageAccounts';

const IssuesRoutes = () => {
return (
<Routes>
<Route path="/" element={<PageAccounts />} />
<Route path="*" element={<Error404 />} />
</Routes>
);
};

export default IssuesRoutes;
70 changes: 70 additions & 0 deletions src/app/accounts/PageAccounts.tsx
@@ -0,0 +1,70 @@
import {
Alert,
AlertIcon,
Button,
Flex,
Heading,
Stack,
StackDivider,
Text,
} from '@chakra-ui/react';
import { signIn } from 'next-auth/react';
import { useTranslation } from 'react-i18next';
import { FiGithub, FiGitlab } from 'react-icons/fi';

import { Page, PageContent } from '@/app/layout';
import { Icon } from '@/components';
import { trpc } from '@/utils/trpc';

export const PageAccounts = () => {
const { t } = useTranslation('account');

const { data, isLoading: areAccountsLoading } = trpc.useQuery(['account.me']);

const githubAccount = data?.find((account) => account.provider === 'github');
const gitlabAccount = data?.find((account) => account.provider === 'gitlab');
Comment on lines +24 to +25
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can probably be set in the backend to avoid client side computation and enjoy tRPC typings.


const isLoading = areAccountsLoading;

return (
<Page containerSize="lg">
<PageContent>
<Stack>
<Heading size="md" mb="4">
{t('account:profile.title')}
</Heading>
<Stack w="full" divider={<StackDivider />}>
<Flex flex="1" justify="space-between" alignItems="center">
<Text>GitHub</Text>
<Button
leftIcon={<Icon icon={FiGithub} />}
isDisabled={!!githubAccount}
isLoading={isLoading}
>
{githubAccount ? githubAccount.username ?? 'Linked' : 'Link'}
</Button>
</Flex>
<Stack>
<Flex flex="1" justify="space-between" alignItems="center">
<Text>GitLab</Text>
<Button
leftIcon={<Icon icon={FiGitlab} />}
isDisabled={!!gitlabAccount}
isLoading={isLoading}
onClick={() => signIn('gitlab')}
>
{gitlabAccount ? gitlabAccount.username ?? 'Linked' : 'Link'}
</Button>
</Flex>
<Alert borderRadius="md" status="warning">
<AlertIcon />
At the moment, you can link your GitLab account but the issue
export using GitLab API is not supported yet.
</Alert>
</Stack>
</Stack>
</Stack>
</PageContent>
</Page>
);
};
17 changes: 16 additions & 1 deletion src/app/layout/AccountMenu/index.tsx
Expand Up @@ -15,7 +15,15 @@ import {
} from '@chakra-ui/react';
import { signOut, useSession } from 'next-auth/react';
import { useTranslation } from 'react-i18next';
import { FiCheck, FiCopy, FiLogOut, FiMoon, FiSun } from 'react-icons/fi';
import {
FiCheck,
FiCopy,
FiLogOut,
FiMoon,
FiSun,
FiUser,
} from 'react-icons/fi';
import { Link } from 'react-router-dom';

import appBuild from '@/../app-build.json';
import { Icon } from '@/components';
Expand Down Expand Up @@ -107,6 +115,13 @@ export const AccountMenu = ({ ...rest }) => {
maxW="12rem"
overflow="hidden"
>
<MenuItem
icon={<Icon fontSize="lg" color="gray.400" icon={FiUser} />}
as={Link}
to="/accounts"
>
{t('layout:accountMenu.accounts')}
</MenuItem>
<MenuItem
icon={
<Icon
Expand Down
88 changes: 38 additions & 50 deletions src/lib/auth.ts
@@ -1,16 +1,18 @@
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import axios from 'axios';
import { NextAuthOptions } from 'next-auth';
import GitHubProvider from 'next-auth/providers/github';
import GitHubProvider, { GithubProfile } from 'next-auth/providers/github';
import GitLabProvider, { GitLabProfile } from 'next-auth/providers/gitlab';

import { linkGitLabAccount } from '@/lib/linkGitLabAccount';
import { signInGitHubAccount } from '@/lib/signInGitHubAccount';
import { db } from '@/utils/db';

export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(db),
providers: [
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
clientId: process.env.GITHUB_CLIENT_ID ?? '',
clientSecret: process.env.GITHUB_CLIENT_SECRET ?? '',
authorization: {
url: 'https://github.com/login/oauth/authorize',
params: {
Expand All @@ -20,60 +22,39 @@ export const authOptions: NextAuthOptions = {
},
},
}),
GitLabProvider({
clientId: process.env.GITLAB_CLIENT_ID ?? '',
clientSecret: process.env.GITLAB_CLIENT_SECRET ?? '',
profile(profile: GitLabProfile) {
return {
id: profile.id.toString(),
email: profile.email,
image: profile.avatar_url,
name: profile.username,
};
},
}),
],
pages: {
signIn: '/app/login',
},
callbacks: {
async signIn({ account }) {
try {
// Create a new instance of axios so we are not bothered by the
// interceptors.
const instance = axios.create();
const response: TODO = await instance.get<Array<github.Organization>>(
'https://api.github.com/user/orgs',
{
headers: { Authorization: `token ${account.access_token}` },
}
);

const isAuthorized = response.data.some(
// Using the org id in case there is a rename.
(org) => process.env.GITHUB_ALLOWED_ORGANIZATIONS?.includes(org.login)
);

if (!isAuthorized) {
return false;
}

// This is to update the provider account on sign in. This does not
// exist in NextAuth yet.
const existingAccount = await db.account.findUnique({
where: {
provider_providerAccountId: {
provider: account.provider,
providerAccountId: account.providerAccountId,
},
},
async signIn({ account, profile }) {
if (account.provider === 'github') {
return signInGitHubAccount({
account,
profile: profile as GithubProfile,
});
}

if (!!existingAccount) {
await db.account.update({
where: {
provider_providerAccountId: {
provider: account.provider,
providerAccountId: account.providerAccountId,
},
},
data: account,
});
}

return isAuthorized;
} catch (err) {
console.error(err);
return false;
if (account.provider === 'gitlab') {
// TODO: will need to check for group / orgs when implementing complete
// sign in.
// https://github.com/BearStudio/start-repo/issues/19
return true;
}

return false;
},
async session({ session, user: u }) {
const user = await db.user.findFirst({
Expand All @@ -89,5 +70,12 @@ export const authOptions: NextAuthOptions = {
return session;
},
},
events: {
linkAccount: ({ account, profile }) => {
if (account.provider === 'gitlab') {
linkGitLabAccount({ account, profile });
}
},
},
secret: process.env.NEXTAUTH_SECRET,
};