chore: remove OAuth from chrome extension (#5528)
Since we can access the tokens directly from cookies of our front app, we don't require the OAuth process to fetch tokens anymore
This commit is contained in:
@ -1,6 +1,3 @@
|
|||||||
import Crypto from 'crypto-js';
|
|
||||||
|
|
||||||
import { exchangeAuthorizationCode } from '~/db/auth.db';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
// Open options page programmatically in a new tab.
|
// Open options page programmatically in a new tab.
|
||||||
@ -25,12 +22,6 @@ chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'launchOAuth': {
|
|
||||||
launchOAuth(({ status, message }) => {
|
|
||||||
sendResponse({ status, message });
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'openSidepanel': {
|
case 'openSidepanel': {
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
|
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
|
||||||
if (isDefined(tab) && isDefined(tab.id)) {
|
if (isDefined(tab) && isDefined(tab.id)) {
|
||||||
@ -57,84 +48,6 @@ chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const generateRandomString = (length: number) => {
|
|
||||||
const charset =
|
|
||||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
||||||
let result = '';
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
result += charset.charAt(Math.floor(Math.random() * charset.length));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateCodeVerifierAndChallenge = () => {
|
|
||||||
const codeVerifier = generateRandomString(32);
|
|
||||||
const hash = Crypto.SHA256(codeVerifier);
|
|
||||||
const codeChallenge = hash
|
|
||||||
.toString(Crypto.enc.Base64)
|
|
||||||
.replace(/\+/g, '-')
|
|
||||||
.replace(/\//g, '_')
|
|
||||||
.replace(/=+$/, '');
|
|
||||||
|
|
||||||
return { codeVerifier, codeChallenge };
|
|
||||||
};
|
|
||||||
|
|
||||||
const launchOAuth = (
|
|
||||||
callback: ({ status, message }: { status: boolean; message: string }) => void,
|
|
||||||
) => {
|
|
||||||
const { codeVerifier, codeChallenge } = generateCodeVerifierAndChallenge();
|
|
||||||
const redirectUrl = chrome.identity.getRedirectURL();
|
|
||||||
chrome.identity
|
|
||||||
.launchWebAuthFlow({
|
|
||||||
url: `${
|
|
||||||
import.meta.env.VITE_FRONT_BASE_URL
|
|
||||||
}/authorize?clientId=chrome&codeChallenge=${codeChallenge}&redirectUrl=${redirectUrl}`,
|
|
||||||
interactive: true,
|
|
||||||
})
|
|
||||||
.then((responseUrl) => {
|
|
||||||
if (typeof responseUrl === 'string') {
|
|
||||||
const url = new URL(responseUrl);
|
|
||||||
const authorizationCode = url.searchParams.get(
|
|
||||||
'authorizationCode',
|
|
||||||
) as string;
|
|
||||||
exchangeAuthorizationCode({
|
|
||||||
authorizationCode,
|
|
||||||
codeVerifier,
|
|
||||||
}).then((tokens) => {
|
|
||||||
if (isDefined(tokens)) {
|
|
||||||
chrome.storage.local.set({
|
|
||||||
loginToken: tokens.loginToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
chrome.storage.local.set({
|
|
||||||
accessToken: tokens.accessToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
chrome.storage.local.set({
|
|
||||||
refreshToken: tokens.refreshToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
callback({ status: true, message: '' });
|
|
||||||
|
|
||||||
chrome.tabs.query(
|
|
||||||
{ active: true, currentWindow: true },
|
|
||||||
([tab]) => {
|
|
||||||
if (isDefined(tab) && isDefined(tab.id)) {
|
|
||||||
chrome.tabs.sendMessage(tab.id, {
|
|
||||||
action: 'executeContentScript',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
callback({ status: false, message: error.message });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
chrome.tabs.onUpdated.addListener(async (tabId, _, tab) => {
|
chrome.tabs.onUpdated.addListener(async (tabId, _, tab) => {
|
||||||
const isDesiredRoute =
|
const isDesiredRoute =
|
||||||
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) ||
|
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) ||
|
||||||
@ -154,3 +67,34 @@ chrome.tabs.onUpdated.addListener(async (tabId, _, tab) => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setTokenStateFromCookie = (cookie: string) => {
|
||||||
|
const decodedValue = decodeURIComponent(cookie);
|
||||||
|
const tokenPair = JSON.parse(decodedValue);
|
||||||
|
if (isDefined(tokenPair)) {
|
||||||
|
chrome.storage.local.set({
|
||||||
|
isAuthenticated: true,
|
||||||
|
accessToken: tokenPair.accessToken,
|
||||||
|
refreshToken: tokenPair.refreshToken,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.cookies.onChanged.addListener(async ({ cookie }) => {
|
||||||
|
if (cookie.name === 'tokenPair') {
|
||||||
|
setTokenStateFromCookie(cookie.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// This will only run the very first time the extension loads, after we have stored the
|
||||||
|
// cookiesRead variable to true, this will not allow to change the token state everytime background script runs
|
||||||
|
chrome.cookies.get(
|
||||||
|
{ name: 'tokenPair', url: `${import.meta.env.VITE_FRONT_BASE_URL}` },
|
||||||
|
async (cookie) => {
|
||||||
|
const store = await chrome.storage.local.get(['cookiesRead']);
|
||||||
|
if (isDefined(cookie) && !isDefined(store.cookiesRead)) {
|
||||||
|
setTokenStateFromCookie(cookie.value);
|
||||||
|
chrome.storage.local.set({ cookiesRead: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile';
|
import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile';
|
||||||
import { insertButtonForPerson } from '~/contentScript/extractPersonProfile';
|
import { insertButtonForPerson } from '~/contentScript/extractPersonProfile';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
// Inject buttons into the DOM when SPA is reloaded on the resource url.
|
// Inject buttons into the DOM when SPA is reloaded on the resource url.
|
||||||
// e.g. reload the page when on https://www.linkedin.com/in/mabdullahabaid/
|
// e.g. reload the page when on https://www.linkedin.com/in/mabdullahabaid/
|
||||||
@ -21,3 +22,12 @@ chrome.runtime.onMessage.addListener(async (message, _, sendResponse) => {
|
|||||||
|
|
||||||
sendResponse('Executing!');
|
sendResponse('Executing!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
chrome.storage.local.onChanged.addListener(async (store) => {
|
||||||
|
if (isDefined(store.accessToken)) {
|
||||||
|
if (isDefined(store.accessToken.newValue)) {
|
||||||
|
await insertButtonForCompany();
|
||||||
|
await insertButtonForPerson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@ -2,10 +2,6 @@ import { defineManifest } from '@crxjs/vite-plugin';
|
|||||||
|
|
||||||
import packageData from '../package.json';
|
import packageData from '../package.json';
|
||||||
|
|
||||||
const host_permissions =
|
|
||||||
process.env.VITE_MODE === 'development'
|
|
||||||
? ['https://www.linkedin.com/*', 'http://localhost:3001/*']
|
|
||||||
: ['https://www.linkedin.com/*'];
|
|
||||||
const external_sites =
|
const external_sites =
|
||||||
process.env.VITE_MODE === 'development'
|
process.env.VITE_MODE === 'development'
|
||||||
? [`https://app.twenty.com/*`, `http://localhost:3001/*`]
|
? [`https://app.twenty.com/*`, `http://localhost:3001/*`]
|
||||||
@ -48,9 +44,12 @@ export default defineManifest({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
permissions: ['activeTab', 'storage', 'identity', 'sidePanel'],
|
permissions: ['activeTab', 'storage', 'identity', 'sidePanel', 'cookies'],
|
||||||
|
|
||||||
host_permissions: host_permissions,
|
// setting host permissions to all http connections will allow
|
||||||
|
// for people who host on their custom domain to get access to
|
||||||
|
// extension instead of white listing individual urls
|
||||||
|
host_permissions: ['https://*/*', 'http://*/*'],
|
||||||
|
|
||||||
externally_connectable: {
|
externally_connectable: {
|
||||||
matches: external_sites,
|
matches: external_sites,
|
||||||
|
|||||||
91
packages/twenty-chrome-extension/src/options/Settings.tsx
Normal file
91
packages/twenty-chrome-extension/src/options/Settings.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
const StyledWrapper = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
background: ${({ theme }) => theme.background.primary};
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
width: 400px;
|
||||||
|
height: 350px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: ${({ theme }) => theme.spacing(8)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledActionContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 300px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Settings = () => {
|
||||||
|
const [serverBaseUrl, setServerBaseUrl] = useState('');
|
||||||
|
const [clientUrl, setClientUrl] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getState = async () => {
|
||||||
|
const store = await chrome.storage.local.get();
|
||||||
|
if (isDefined(store.serverBaseUrl)) {
|
||||||
|
setServerBaseUrl(store.serverBaseUrl);
|
||||||
|
} else {
|
||||||
|
setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(store.clientUrl)) {
|
||||||
|
setClientUrl(store.clientUrl);
|
||||||
|
} else {
|
||||||
|
setClientUrl(import.meta.env.VITE_FRONT_BASE_URL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void getState();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleBaseUrlChange = (value: string) => {
|
||||||
|
setServerBaseUrl(value);
|
||||||
|
chrome.storage.local.set({ serverBaseUrl: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClientUrlChange = (value: string) => {
|
||||||
|
setClientUrl(value);
|
||||||
|
chrome.storage.local.set({ clientUrl: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper>
|
||||||
|
<StyledContainer>
|
||||||
|
<img src="/logo/32-32.svg" alt="twenty-logo" height={40} width={40} />
|
||||||
|
<StyledActionContainer>
|
||||||
|
<TextInput
|
||||||
|
label="Client URL"
|
||||||
|
value={clientUrl}
|
||||||
|
onChange={handleClientUrlChange}
|
||||||
|
placeholder="My client URL"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Server URL"
|
||||||
|
value={serverBaseUrl}
|
||||||
|
onChange={handleBaseUrlChange}
|
||||||
|
placeholder="My server URL"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</StyledActionContainer>
|
||||||
|
</StyledContainer>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Settings;
|
||||||
@ -1,9 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { Loader } from '@/ui/display/loader/components/Loader';
|
|
||||||
import { MainButton } from '@/ui/input/button/MainButton';
|
import { MainButton } from '@/ui/input/button/MainButton';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
const StyledIframe = styled.iframe`
|
const StyledIframe = styled.iframe`
|
||||||
@ -41,126 +39,73 @@ const StyledActionContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Sidepanel = () => {
|
const Sidepanel = () => {
|
||||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
const [iframeSrc, setIframeSrc] = useState(
|
const [clientUrl, setClientUrl] = useState(
|
||||||
import.meta.env.VITE_FRONT_BASE_URL,
|
import.meta.env.VITE_FRONT_BASE_URL,
|
||||||
);
|
);
|
||||||
const [error, setError] = useState('');
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
const [serverBaseUrl, setServerBaseUrl] = useState('');
|
|
||||||
const authenticate = () => {
|
const setIframeState = useCallback(async () => {
|
||||||
setIsAuthenticating(true);
|
const store = await chrome.storage.local.get();
|
||||||
setError('');
|
if (isDefined(store.isAuthenticated)) setIsAuthenticated(true);
|
||||||
chrome.runtime.sendMessage(
|
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
||||||
{ action: 'launchOAuth' },
|
action: 'getActiveTab',
|
||||||
({ status, message }) => {
|
});
|
||||||
if (status === true) {
|
|
||||||
setIsAuthenticated(true);
|
if (
|
||||||
setIsAuthenticating(false);
|
isDefined(activeTab) &&
|
||||||
chrome.storage.local.set({ isAuthenticated: true });
|
isDefined(store[`sidepanelUrl_${activeTab.id}`])
|
||||||
} else {
|
) {
|
||||||
setError(message);
|
const url = store[`sidepanelUrl_${activeTab.id}`];
|
||||||
setIsAuthenticating(false);
|
setClientUrl(url);
|
||||||
|
} else if (isDefined(store.clientUrl)) {
|
||||||
|
setClientUrl(store.clientUrl);
|
||||||
|
}
|
||||||
|
}, [setClientUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initState = async () => {
|
||||||
|
const store = await chrome.storage.local.get();
|
||||||
|
if (isDefined(store.isAuthenticated)) setIsAuthenticated(true);
|
||||||
|
void setIframeState();
|
||||||
|
};
|
||||||
|
void initState();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void setIframeState();
|
||||||
|
}, [setIframeState, clientUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
chrome.storage.local.onChanged.addListener((store) => {
|
||||||
|
if (isDefined(store.isAuthenticated)) {
|
||||||
|
if (store.isAuthenticated.newValue === true) {
|
||||||
|
setIframeState();
|
||||||
}
|
}
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getState = async () => {
|
|
||||||
const store = await chrome.storage.local.get();
|
|
||||||
if (isDefined(store.serverBaseUrl)) {
|
|
||||||
setServerBaseUrl(store.serverBaseUrl);
|
|
||||||
} else {
|
|
||||||
setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
if (store.isAuthenticated === true) setIsAuthenticated(true);
|
}, [setIframeState]);
|
||||||
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
|
||||||
action: 'getActiveTab',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
isDefined(activeTab) &&
|
|
||||||
isDefined(store[`sidepanelUrl_${activeTab.id}`])
|
|
||||||
) {
|
|
||||||
const url = store[`sidepanelUrl_${activeTab.id}`];
|
|
||||||
setIframeSrc(url);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
void getState();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleBrowserEvents = ({ action }: { action: string }) => {
|
|
||||||
if (action === 'changeSidepanelUrl') {
|
|
||||||
setIframeSrc('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
chrome.runtime.onMessage.addListener(handleBrowserEvents);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
chrome.runtime.onMessage.removeListener(handleBrowserEvents);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getIframeState = async () => {
|
|
||||||
const store = await chrome.storage.local.get();
|
|
||||||
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
|
||||||
action: 'getActiveTab',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
isDefined(activeTab) &&
|
|
||||||
isDefined(store[`sidepanelUrl_${activeTab.id}`])
|
|
||||||
) {
|
|
||||||
const url = store[`sidepanelUrl_${activeTab.id}`];
|
|
||||||
setIframeSrc(url);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
void getIframeState();
|
|
||||||
}, [iframeSrc]);
|
|
||||||
|
|
||||||
const handleBaseUrlChange = (value: string) => {
|
|
||||||
setServerBaseUrl(value);
|
|
||||||
setError('');
|
|
||||||
chrome.storage.local.set({ serverBaseUrl: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
return isAuthenticated ? (
|
return isAuthenticated ? (
|
||||||
<StyledIframe title="twenty-website" src={iframeSrc}></StyledIframe>
|
<StyledIframe
|
||||||
|
ref={iframeRef}
|
||||||
|
title="twenty-website"
|
||||||
|
src={clientUrl}
|
||||||
|
></StyledIframe>
|
||||||
) : (
|
) : (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<img src="/logo/32-32.svg" alt="twenty-logo" height={40} width={40} />
|
<img src="/logo/32-32.svg" alt="twenty-logo" height={40} width={40} />
|
||||||
{isAuthenticating ? (
|
<StyledActionContainer>
|
||||||
<Loader />
|
<MainButton
|
||||||
) : (
|
title="Connect your account"
|
||||||
<StyledActionContainer>
|
fullWidth
|
||||||
<TextInput
|
onClick={() => {
|
||||||
label="Server URL"
|
window.open(clientUrl, '_blank');
|
||||||
value={serverBaseUrl}
|
}}
|
||||||
onChange={handleBaseUrlChange}
|
/>
|
||||||
placeholder="My base server URL"
|
</StyledActionContainer>
|
||||||
error={error}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<MainButton
|
|
||||||
title="Connect your account"
|
|
||||||
onClick={() => authenticate()}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<MainButton
|
|
||||||
title="Sign up"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() =>
|
|
||||||
window.open(`${import.meta.env.VITE_FRONT_BASE_URL}`, '_blank')
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</StyledActionContainer>
|
|
||||||
)}
|
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user