Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 56 additions & 17 deletions web-server/src/content/Dashboards/ConfigureGithubModalBody.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LoadingButton } from '@mui/lab';
import { Divider, Link, TextField, alpha } from '@mui/material';
import { Divider, Link, TextField, alpha, ToggleButton, ToggleButtonGroup } from '@mui/material';
import Image from 'next/image';
import { useSnackbar } from 'notistack';
import { FC, useCallback, useMemo } from 'react';
Expand All @@ -15,7 +15,9 @@ import { useDispatch } from '@/store';
import {
checkGitHubValidity,
linkProvider,
getMissingPATScopes
getMissingPATScopes,
getMissingFineGrainedScopes,
getTokenType
} from '@/utils/auth';
import { checkDomainWithRegex } from '@/utils/domainCheck';
import { depFn } from '@/utils/fn';
Expand All @@ -29,6 +31,8 @@ export const ConfigureGithubModalBody: FC<{
const customDomain = useEasyState('');
const dispatch = useDispatch();
const isLoading = useBoolState();
const tokenType = useEasyState<'classic' | 'fine-grained'>('classic');
const isTokenValid = useBoolState(false);

const showError = useEasyState<string>('');
const showDomainError = useEasyState<string>('');
Expand All @@ -49,13 +53,32 @@ export const ConfigureGithubModalBody: FC<{

const handleChange = (e: string) => {
token.set(e);
showError.set('');
const detectedType = getTokenType(e);

if (detectedType === 'unknown') {
setError('Invalid token format');
isTokenValid.false();
} else if (detectedType !== tokenType.value) {
setError(`Token format doesn't match selected type. Expected ${tokenType.value} token.`);
isTokenValid.false();
} else {
showError.set('');
isTokenValid.true();
}
};

const handleDomainChange = (e: string) => {
customDomain.set(e);
showDomainError.set('');
};

const handleTokenTypeChange = (value: 'classic' | 'fine-grained') => {
tokenType.set(value);
token.set(''); // Reset token when switching token types
showError.set('');
isTokenValid.false();
};

const handleSubmission = useCallback(async () => {
if (!token.value) {
setError('Please enter a valid token');
Expand All @@ -80,17 +103,18 @@ export const ConfigureGithubModalBody: FC<{
return;
}

const missingScopes = await getMissingPATScopes(
token.value,
customDomain.valueRef.current
);
const missingScopes = tokenType.value === 'classic'
? await getMissingPATScopes(token.value, customDomain.valueRef.current)
: await getMissingFineGrainedScopes(token.value, customDomain.valueRef.current);

if (missingScopes.length > 0) {
setError(`Token is missing scopes: ${missingScopes.join(', ')}`);
return;
}

await linkProvider(token.value, orgId, Integration.GITHUB, {
custom_domain: customDomain.valueRef.current
custom_domain: customDomain.valueRef.current,
token_type: tokenType.value
});

dispatch(fetchCurrentOrg());
Expand All @@ -109,6 +133,7 @@ export const ConfigureGithubModalBody: FC<{
}, [
token.value,
customDomain.value,
tokenType.value,
dispatch,
enqueueSnackbar,
isLoading.false,
Expand All @@ -123,15 +148,24 @@ export const ConfigureGithubModalBody: FC<{

const focusDomainInput = useCallback(() => {
if (!customDomain.value)
document.getElementById('gitlab-custom-domain')?.focus();
document.getElementById('github-custom-domain')?.focus();
else handleSubmission();
}, [customDomain.value, handleSubmission]);

return (
<FlexBox gap2>
<FlexBox gap={2} minWidth={'400px'} col>
<FlexBox>Enter you Github token below.</FlexBox>
<FlexBox>Enter your Github token below.</FlexBox>
<FlexBox fullWidth minHeight={'80px'} col>
<ToggleButtonGroup
value={tokenType.value}
exclusive
onChange={(_, value) => value && handleTokenTypeChange(value)}
sx={{ mb: 2 }}
>
<ToggleButton value="classic">Classic Token</ToggleButton>
<ToggleButton value="fine-grained">Fine Grained Token</ToggleButton>
</ToggleButtonGroup>
<TextField
onKeyDown={(e) => {
if (e.key === 'Enter') {
Expand All @@ -149,7 +183,7 @@ export const ConfigureGithubModalBody: FC<{
onChange={(e) => {
handleChange(e.currentTarget.value);
}}
label="Github Personal Access Token"
label={`Github ${tokenType.value === 'classic' ? 'Personal Access Token' : 'Fine Grained Token'}`}
type="password"
/>
<Line error tiny mt={1}>
Expand All @@ -158,7 +192,9 @@ export const ConfigureGithubModalBody: FC<{
<FlexBox>
<Line tiny mt={1} primary sx={{ cursor: 'pointer' }}>
<Link
href="https://github.com/settings/tokens"
href={tokenType.value === 'classic'
? "https://github.com/settings/tokens"
: "https://github.com/settings/tokens?type=beta"}
target="_blank"
rel="noopener noreferrer"
>
Expand All @@ -168,7 +204,7 @@ export const ConfigureGithubModalBody: FC<{
textUnderlineOffset: '2px'
}}
>
Generate new classic token
Generate new {tokenType.value === 'classic' ? 'classic' : 'fine-grained'} token
</Line>
</Link>
<Line ml={'5px'}>{' ->'}</Line>
Expand Down Expand Up @@ -217,10 +253,12 @@ export const ConfigureGithubModalBody: FC<{
<FlexBox col sx={{ opacity: 0.8 }}>
<Line>Learn more about Github</Line>
<Line>
Personal Access Token (PAT)
{tokenType.value === 'classic' ? 'Personal Access Token (PAT)' : 'Fine Grained Token (FGT)'}
<Link
ml={1 / 2}
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens"
href={tokenType.value === 'classic'
? "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens"
: "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#fine-grained-personal-access-tokens"}
target="_blank"
rel="noopener noreferrer"
>
Expand All @@ -231,6 +269,7 @@ export const ConfigureGithubModalBody: FC<{
<FlexBox gap={2} justifyEnd>
<LoadingButton
loading={isLoading.value}
disabled={!isTokenValid.value}
variant="contained"
onClick={handleSubmission}
>
Expand All @@ -240,12 +279,12 @@ export const ConfigureGithubModalBody: FC<{
</FlexBox>
</FlexBox>
<Divider orientation="vertical" flexItem />
<TokenPermissions />
<TokenPermissions tokenType={tokenType.value} />
</FlexBox>
);
};

const TokenPermissions = () => {
const TokenPermissions: FC<{ tokenType: 'classic' | 'fine-grained' }> = ({ tokenType }) => {
const imageLoaded = useBoolState(false);

const expandedStyles = useMemo(() => {
Expand Down
46 changes: 42 additions & 4 deletions web-server/src/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,18 @@ export const linkProvider = async (

export async function checkGitHubValidity(
good_stuff: string,
customDomain?: string
customDomain?: string,
tokenType: 'classic' | 'fine-grained' = 'classic'
): Promise<boolean> {
try {
// if customDomain is provded, the host will be customDomain/api/v3
// else it will be api.github.com(default)
const baseUrl = customDomain ? `${customDomain}/api/v3` : DEFAULT_GH_URL;
const authHeader = tokenType === 'classic'
? `token ${good_stuff}`
: `Bearer ${good_stuff}`;

await axios.get(`${baseUrl}/user`, {
headers: {
Authorization: `token ${good_stuff}`
Authorization: authHeader
}
});
return true;
Expand All @@ -49,6 +51,8 @@ export async function checkGitHubValidity(
}

const PAT_SCOPES = ['read:org', 'read:user', 'repo', 'workflow'];
const FINE_GRAINED_SCOPES = ['contents:read', 'metadata:read', 'pull_requests:read', 'workflows:read'];

export const getMissingPATScopes = async (
pat: string,
customDomain?: string
Expand All @@ -71,6 +75,33 @@ export const getMissingPATScopes = async (
}
};

export const getMissingFineGrainedScopes = async (
token: string,
customDomain?: string
) => {
const baseUrl = customDomain ? `${customDomain}/api/v3` : DEFAULT_GH_URL;
try {
const response = await axios.get(`${baseUrl}/user`, {
headers: {
Authorization: `Bearer ${token}`
}
});

// For fine-grained tokens, we need to check the token's permissions
// This is a simplified check - in reality, you'd want to verify each permission
// by making specific API calls to test access
const hasAccess = response.status === 200;
if (!hasAccess) return FINE_GRAINED_SCOPES;

// Since fine-grained tokens don't expose scopes in headers like PATs do,
// we'll need to test each required permission individually
// This is a placeholder for the actual permission checks
return [];
} catch (error) {
throw new Error('Failed to get missing fine-grained token scopes', error);
}
};

// Gitlab functions

export const checkGitLabValidity = async (
Expand Down Expand Up @@ -99,3 +130,10 @@ export const getMissingGitLabScopes = (scopes: string[]): string[] => {
);
return missingScopes;
};


export const getTokenType = (token: string): 'classic' | 'fine-grained' | 'unknown' => {
if (token.startsWith('ghp_')) return 'classic';
if (token.startsWith('github_pat_')) return 'fine-grained';
return 'unknown';
}
Loading