mirror of
https://github.com/lingble/twenty.git
synced 2025-11-01 05:07:56 +00:00
fix: user has to login every time chrome sidepanel is opened (#5544)
We can pass the auth tokens to our front app via post message, which will also allow us to pass route names to navigate on it
This commit is contained in:
@@ -75,7 +75,6 @@
|
|||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"crypto-js": "^4.2.0",
|
|
||||||
"danger-plugin-todos": "^1.3.1",
|
"danger-plugin-todos": "^1.3.1",
|
||||||
"dataloader": "^2.2.2",
|
"dataloader": "^2.2.2",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
@@ -241,8 +240,6 @@
|
|||||||
"@types/better-sqlite3": "^7.6.8",
|
"@types/better-sqlite3": "^7.6.8",
|
||||||
"@types/bytes": "^3.1.1",
|
"@types/bytes": "^3.1.1",
|
||||||
"@types/chrome": "^0.0.267",
|
"@types/chrome": "^0.0.267",
|
||||||
"@types/crypto-js": "^4.2.2",
|
|
||||||
"@types/dagre": "^0.7.52",
|
|
||||||
"@types/deep-equal": "^1.0.1",
|
"@types/deep-equal": "^1.0.1",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/graphql-fields": "^1.3.6",
|
"@types/graphql-fields": "^1.3.6",
|
||||||
|
|||||||
@@ -11,10 +11,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"start": {
|
"start": {
|
||||||
"executor": "@nx/vite:dev-server",
|
"executor": "nx:run-commands",
|
||||||
|
"dependsOn": ["build"],
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "twenty-chrome-extension:build",
|
"cwd": "packages/twenty-chrome-extension",
|
||||||
"hmr": true
|
"command": "VITE_MODE=development vite"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
|
|||||||
@@ -30,17 +30,6 @@ chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'changeSidepanelUrl': {
|
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
|
|
||||||
if (isDefined(tab) && isDefined(tab.id)) {
|
|
||||||
chrome.tabs.sendMessage(tab.id, {
|
|
||||||
action: 'changeSidepanelUrl',
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -82,8 +71,16 @@ const setTokenStateFromCookie = (cookie: string) => {
|
|||||||
|
|
||||||
chrome.cookies.onChanged.addListener(async ({ cookie }) => {
|
chrome.cookies.onChanged.addListener(async ({ cookie }) => {
|
||||||
if (cookie.name === 'tokenPair') {
|
if (cookie.name === 'tokenPair') {
|
||||||
|
const store = await chrome.storage.local.get(['clientUrl']);
|
||||||
|
const clientUrl = isDefined(store.clientUrl)
|
||||||
|
? store.clientUrl
|
||||||
|
: import.meta.env.VITE_FRONT_BASE_URL;
|
||||||
|
chrome.cookies.get({ name: 'tokenPair', url: `${clientUrl}` }, (cookie) => {
|
||||||
|
if (isDefined(cookie)) {
|
||||||
setTokenStateFromCookie(cookie.value);
|
setTokenStateFromCookie(cookie.value);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// This will only run the very first time the extension loads, after we have stored the
|
// This will only run the very first time the extension loads, after we have stored the
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export const createDefaultButton = (
|
|||||||
padding: '0 1rem',
|
padding: '0 1rem',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
height: '32px',
|
height: '32px',
|
||||||
|
width: 'max-content',
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(div.style, divStyles);
|
Object.assign(div.style, divStyles);
|
||||||
|
|||||||
@@ -75,9 +75,7 @@ export const addCompany = async () => {
|
|||||||
const companyId = await createCompany(companyInputData);
|
const companyId = await createCompany(companyInputData);
|
||||||
|
|
||||||
if (isDefined(companyId)) {
|
if (isDefined(companyId)) {
|
||||||
await changeSidePanelUrl(
|
await changeSidePanelUrl(`/object/company/${companyId}`);
|
||||||
`${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${companyId}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return companyId;
|
return companyId;
|
||||||
@@ -86,16 +84,15 @@ export const addCompany = async () => {
|
|||||||
export const insertButtonForCompany = async () => {
|
export const insertButtonForCompany = async () => {
|
||||||
const companyButtonDiv = createDefaultButton('twenty-company-btn');
|
const companyButtonDiv = createDefaultButton('twenty-company-btn');
|
||||||
|
|
||||||
const parentDiv: HTMLDivElement | null = document.querySelector(
|
const companyDiv: HTMLDivElement | null = document.querySelector(
|
||||||
'.org-top-card-primary-actions__inner',
|
'.org-top-card__primary-content',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isDefined(parentDiv)) {
|
if (isDefined(companyDiv)) {
|
||||||
Object.assign(companyButtonDiv.style, {
|
Object.assign(companyButtonDiv.style, {
|
||||||
marginLeft: '.8rem',
|
marginTop: '.8rem',
|
||||||
marginTop: '.4rem',
|
|
||||||
});
|
});
|
||||||
parentDiv.prepend(companyButtonDiv);
|
companyDiv.parentElement?.append(companyButtonDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
const companyButtonSpan = companyButtonDiv.getElementsByTagName('span')[0];
|
const companyButtonSpan = companyButtonDiv.getElementsByTagName('span')[0];
|
||||||
@@ -104,19 +101,16 @@ export const insertButtonForCompany = async () => {
|
|||||||
const openCompanyOnSidePanel = (companyId: string) => {
|
const openCompanyOnSidePanel = (companyId: string) => {
|
||||||
companyButtonSpan.textContent = 'View in Twenty';
|
companyButtonSpan.textContent = 'View in Twenty';
|
||||||
companyButtonDiv.onClickHandler(async () => {
|
companyButtonDiv.onClickHandler(async () => {
|
||||||
await changeSidePanelUrl(
|
await changeSidePanelUrl(`/object/company/${companyId}`);
|
||||||
`${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${companyId}`,
|
|
||||||
);
|
|
||||||
chrome.runtime.sendMessage({ action: 'openSidepanel' });
|
chrome.runtime.sendMessage({ action: 'openSidepanel' });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isDefined(company)) {
|
if (isDefined(company)) {
|
||||||
await changeSidePanelUrl(
|
await changeSidePanelUrl(`/object/company/${company.id}`);
|
||||||
`${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${company.id}`,
|
|
||||||
);
|
|
||||||
if (isDefined(company.id)) openCompanyOnSidePanel(company.id);
|
if (isDefined(company.id)) openCompanyOnSidePanel(company.id);
|
||||||
} else {
|
} else {
|
||||||
|
await changeSidePanelUrl(`/objects/companies`);
|
||||||
companyButtonSpan.textContent = 'Add to Twenty';
|
companyButtonSpan.textContent = 'Add to Twenty';
|
||||||
|
|
||||||
companyButtonDiv.onClickHandler(async () => {
|
companyButtonDiv.onClickHandler(async () => {
|
||||||
|
|||||||
@@ -86,9 +86,7 @@ export const addPerson = async () => {
|
|||||||
const personId = await createPerson(personData);
|
const personId = await createPerson(personData);
|
||||||
|
|
||||||
if (isDefined(personId)) {
|
if (isDefined(personId)) {
|
||||||
await changeSidePanelUrl(
|
await changeSidePanelUrl(`/object/person/${personId}`);
|
||||||
`${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${personId}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return personId;
|
return personId;
|
||||||
@@ -98,15 +96,13 @@ export const insertButtonForPerson = async () => {
|
|||||||
const personButtonDiv = createDefaultButton('twenty-person-btn');
|
const personButtonDiv = createDefaultButton('twenty-person-btn');
|
||||||
|
|
||||||
if (isDefined(personButtonDiv)) {
|
if (isDefined(personButtonDiv)) {
|
||||||
const addedProfileDiv: HTMLDivElement | null = document.querySelector(
|
const addedProfileDiv = document.querySelector('.artdeco-card > .ph5');
|
||||||
'.pv-top-card-v2-ctas__custom',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isDefined(addedProfileDiv)) {
|
if (isDefined(addedProfileDiv)) {
|
||||||
Object.assign(personButtonDiv.style, {
|
Object.assign(personButtonDiv.style, {
|
||||||
marginRight: '.8rem',
|
marginTop: '.8rem',
|
||||||
});
|
});
|
||||||
addedProfileDiv.prepend(personButtonDiv);
|
addedProfileDiv.append(personButtonDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
const personButtonSpan = personButtonDiv.getElementsByTagName('span')[0];
|
const personButtonSpan = personButtonDiv.getElementsByTagName('span')[0];
|
||||||
@@ -115,19 +111,16 @@ export const insertButtonForPerson = async () => {
|
|||||||
const openPersonOnSidePanel = (personId: string) => {
|
const openPersonOnSidePanel = (personId: string) => {
|
||||||
personButtonSpan.textContent = 'View in Twenty';
|
personButtonSpan.textContent = 'View in Twenty';
|
||||||
personButtonDiv.onClickHandler(async () => {
|
personButtonDiv.onClickHandler(async () => {
|
||||||
await changeSidePanelUrl(
|
await changeSidePanelUrl(`/object/person/${personId}`);
|
||||||
`${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${personId}`,
|
|
||||||
);
|
|
||||||
chrome.runtime.sendMessage({ action: 'openSidepanel' });
|
chrome.runtime.sendMessage({ action: 'openSidepanel' });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isDefined(person)) {
|
if (isDefined(person)) {
|
||||||
await changeSidePanelUrl(
|
await changeSidePanelUrl(`/object/person/${person.id}`);
|
||||||
`${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${person.id}`,
|
|
||||||
);
|
|
||||||
if (isDefined(person.id)) openPersonOnSidePanel(person.id);
|
if (isDefined(person.id)) openPersonOnSidePanel(person.id);
|
||||||
} else {
|
} else {
|
||||||
|
await changeSidePanelUrl(`/objects/people`);
|
||||||
personButtonSpan.textContent = 'Add to Twenty';
|
personButtonSpan.textContent = 'Add to Twenty';
|
||||||
personButtonDiv.onClickHandler(async () => {
|
personButtonDiv.onClickHandler(async () => {
|
||||||
personButtonSpan.textContent = 'Saving...';
|
personButtonSpan.textContent = 'Saving...';
|
||||||
|
|||||||
@@ -5,10 +5,23 @@ 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/
|
||||||
// await insertButtonForCompany();
|
// await insertButtonForCompany();
|
||||||
(async () => {
|
|
||||||
|
const companyRoute = /^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/;
|
||||||
|
const personRoute = /^https?:\/\/(?:www\.)?linkedin\.com\/in(?:\/\S+)?/;
|
||||||
|
|
||||||
|
const executeScript = async () => {
|
||||||
|
const loc = window.location.href;
|
||||||
|
switch (true) {
|
||||||
|
case companyRoute.test(loc):
|
||||||
await insertButtonForCompany();
|
await insertButtonForCompany();
|
||||||
|
break;
|
||||||
|
case personRoute.test(loc):
|
||||||
await insertButtonForPerson();
|
await insertButtonForPerson();
|
||||||
})();
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// The content script gets executed upon load, so the the content script is executed when a user visits https://www.linkedin.com/feed/.
|
// The content script gets executed upon load, so the the content script is executed when a user visits https://www.linkedin.com/feed/.
|
||||||
// However, there would never be another reload in a single page application unless triggered manually.
|
// However, there would never be another reload in a single page application unless triggered manually.
|
||||||
@@ -16,8 +29,7 @@ import { isDefined } from '~/utils/isDefined';
|
|||||||
// e.g. create "Add to Twenty" button when a user navigates to https://www.linkedin.com/in/mabdullahabaid/ from https://www.linkedin.com/feed/
|
// e.g. create "Add to Twenty" button when a user navigates to https://www.linkedin.com/in/mabdullahabaid/ from https://www.linkedin.com/feed/
|
||||||
chrome.runtime.onMessage.addListener(async (message, _, sendResponse) => {
|
chrome.runtime.onMessage.addListener(async (message, _, sendResponse) => {
|
||||||
if (message.action === 'executeContentScript') {
|
if (message.action === 'executeContentScript') {
|
||||||
await insertButtonForCompany();
|
await executeScript();
|
||||||
await insertButtonForPerson();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sendResponse('Executing!');
|
sendResponse('Executing!');
|
||||||
@@ -26,8 +38,7 @@ chrome.runtime.onMessage.addListener(async (message, _, sendResponse) => {
|
|||||||
chrome.storage.local.onChanged.addListener(async (store) => {
|
chrome.storage.local.onChanged.addListener(async (store) => {
|
||||||
if (isDefined(store.accessToken)) {
|
if (isDefined(store.accessToken)) {
|
||||||
if (isDefined(store.accessToken.newValue)) {
|
if (isDefined(store.accessToken.newValue)) {
|
||||||
await insertButtonForCompany();
|
await executeScript();
|
||||||
await insertButtonForPerson();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
const btn = document.getElementById('twenty-settings-btn');
|
||||||
|
if (!isDefined(btn)) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src =
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=';
|
||||||
|
img.height = 20;
|
||||||
|
img.width = 20;
|
||||||
|
img.alt = 'Twenty logo';
|
||||||
|
|
||||||
|
// Write universal styles for the button
|
||||||
|
const divStyles = {
|
||||||
|
border: '1px solid black',
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: 'black',
|
||||||
|
color: 'white',
|
||||||
|
fontWeight: '600',
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '5px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '0 1rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
height: '50px',
|
||||||
|
width: '50px',
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '80px',
|
||||||
|
right: '20px',
|
||||||
|
zIndex: '9999999999999999999999999',
|
||||||
|
};
|
||||||
|
|
||||||
|
div.addEventListener('mouseenter', () => {
|
||||||
|
const hoverStyles = {
|
||||||
|
//eslint-disable-next-line @nx/workspace-no-hardcoded-colors
|
||||||
|
backgroundColor: '#5e5e5e',
|
||||||
|
//eslint-disable-next-line @nx/workspace-no-hardcoded-colors
|
||||||
|
borderColor: '#5e5e5e',
|
||||||
|
};
|
||||||
|
Object.assign(div.style, hoverStyles);
|
||||||
|
});
|
||||||
|
|
||||||
|
div.addEventListener('mouseleave', () => {
|
||||||
|
Object.assign(div.style, divStyles);
|
||||||
|
});
|
||||||
|
|
||||||
|
div.onclick = async () => {
|
||||||
|
chrome.runtime.sendMessage({ action: 'openSidepanel' });
|
||||||
|
chrome.storage.local.set({ navigateSidepanel: 'settings' });
|
||||||
|
};
|
||||||
|
|
||||||
|
div.appendChild(img);
|
||||||
|
|
||||||
|
Object.assign(div.style, divStyles);
|
||||||
|
|
||||||
|
document.body.appendChild(div);
|
||||||
|
}
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
const changeSidePanelUrl = async (url: string) => {
|
const changeSidePanelUrl = async (url: string) => {
|
||||||
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
if (isDefined(url)) {
|
||||||
action: 'getActiveTab',
|
chrome.storage.local.set({ navigateSidepanel: 'sidepanel' });
|
||||||
});
|
// we first clear the sidepanelUrl to trigger the onchange listener on sidepanel
|
||||||
if (isDefined(activeTab) && isDefined(url)) {
|
// which will pass the post meessage to handle internal navigation of iframe
|
||||||
chrome.storage.local.set({ [`sidepanelUrl_${activeTab.id}`]: url });
|
chrome.storage.local.set({ sidepanelUrl: '' });
|
||||||
chrome.runtime.sendMessage({
|
chrome.storage.local.set({ sidepanelUrl: url });
|
||||||
action: 'changeSidepanelUrl',
|
|
||||||
message: { url },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ export default defineManifest({
|
|||||||
content_scripts: [
|
content_scripts: [
|
||||||
{
|
{
|
||||||
matches: ['https://www.linkedin.com/*'],
|
matches: ['https://www.linkedin.com/*'],
|
||||||
js: ['src/contentScript/index.ts'],
|
js: [
|
||||||
|
'src/contentScript/index.ts',
|
||||||
|
'src/contentScript/insertSettingsButton.ts',
|
||||||
|
],
|
||||||
run_at: 'document_end',
|
run_at: 'document_end',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
42
packages/twenty-chrome-extension/src/options/App.tsx
Normal file
42
packages/twenty-chrome-extension/src/options/App.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import Settings from '~/options/Settings';
|
||||||
|
import Sidepanel from '~/options/Sidepanel';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const [currentScreen, setCurrentScreen] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const setCurrentScreenState = async () => {
|
||||||
|
const store = await chrome.storage.local.get(['navigateSidepanel']);
|
||||||
|
if (isDefined(store.navigateSidepanel)) {
|
||||||
|
setCurrentScreen(store.navigateSidepanel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrentScreenState();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
chrome.storage.local.onChanged.addListener((updatedStore) => {
|
||||||
|
if (
|
||||||
|
isDefined(updatedStore.navigateSidepanel) &&
|
||||||
|
isDefined(updatedStore.navigateSidepanel.newValue)
|
||||||
|
) {
|
||||||
|
setCurrentScreen(updatedStore.navigateSidepanel.newValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [setCurrentScreen]);
|
||||||
|
|
||||||
|
switch (currentScreen) {
|
||||||
|
case 'sidepanel':
|
||||||
|
return <Sidepanel />;
|
||||||
|
case 'settings':
|
||||||
|
return <Settings />;
|
||||||
|
default:
|
||||||
|
return <Settings />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { MainButton } from '@/ui/input/button/MainButton';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import { clearStore } from '~/utils/apolloClient';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
const StyledWrapper = styled.div`
|
const StyledWrapper = styled.div`
|
||||||
@@ -34,33 +36,47 @@ const StyledActionContainer = styled.div`
|
|||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
const [serverBaseUrl, setServerBaseUrl] = useState('');
|
const [serverBaseUrl, setServerBaseUrl] = useState('');
|
||||||
const [clientUrl, setClientUrl] = useState('');
|
const [clientUrl, setClientUrl] = useState('');
|
||||||
|
const [currentClientUrl, setCurrentClientUrl] = useState('');
|
||||||
|
const [currentServerUrl, setCurrentServerUrl] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getState = async () => {
|
const getState = async () => {
|
||||||
const store = await chrome.storage.local.get();
|
const store = await chrome.storage.local.get([
|
||||||
|
'serverBaseUrl',
|
||||||
|
'clientUrl',
|
||||||
|
]);
|
||||||
if (isDefined(store.serverBaseUrl)) {
|
if (isDefined(store.serverBaseUrl)) {
|
||||||
setServerBaseUrl(store.serverBaseUrl);
|
setServerBaseUrl(store.serverBaseUrl);
|
||||||
|
setCurrentServerUrl(store.serverBaseUrl);
|
||||||
} else {
|
} else {
|
||||||
setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL);
|
setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL);
|
||||||
|
setCurrentServerUrl(import.meta.env.VITE_SERVER_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDefined(store.clientUrl)) {
|
if (isDefined(store.clientUrl)) {
|
||||||
setClientUrl(store.clientUrl);
|
setClientUrl(store.clientUrl);
|
||||||
|
setCurrentClientUrl(store.clientUrl);
|
||||||
} else {
|
} else {
|
||||||
setClientUrl(import.meta.env.VITE_FRONT_BASE_URL);
|
setClientUrl(import.meta.env.VITE_FRONT_BASE_URL);
|
||||||
|
setCurrentClientUrl(import.meta.env.VITE_FRONT_BASE_URL);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
void getState();
|
void getState();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleBaseUrlChange = (value: string) => {
|
const handleSettingsChange = () => {
|
||||||
setServerBaseUrl(value);
|
chrome.storage.local.set({
|
||||||
chrome.storage.local.set({ serverBaseUrl: value });
|
serverBaseUrl,
|
||||||
|
clientUrl,
|
||||||
|
navigateSidepanel: 'sidepanel',
|
||||||
|
});
|
||||||
|
clearStore();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClientUrlChange = (value: string) => {
|
const handleCloseSettings = () => {
|
||||||
setClientUrl(value);
|
chrome.storage.local.set({
|
||||||
chrome.storage.local.set({ clientUrl: value });
|
navigateSidepanel: 'sidepanel',
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -71,17 +87,33 @@ const Settings = () => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
label="Client URL"
|
label="Client URL"
|
||||||
value={clientUrl}
|
value={clientUrl}
|
||||||
onChange={handleClientUrlChange}
|
onChange={setClientUrl}
|
||||||
placeholder="My client URL"
|
placeholder="My client URL"
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Server URL"
|
label="Server URL"
|
||||||
value={serverBaseUrl}
|
value={serverBaseUrl}
|
||||||
onChange={handleBaseUrlChange}
|
onChange={setServerBaseUrl}
|
||||||
placeholder="My server URL"
|
placeholder="My server URL"
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
|
<MainButton
|
||||||
|
title="Done"
|
||||||
|
disabled={
|
||||||
|
currentClientUrl === clientUrl &&
|
||||||
|
currentServerUrl === serverBaseUrl
|
||||||
|
}
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleSettingsChange}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<MainButton
|
||||||
|
title="Close"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handleCloseSettings}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
</StyledActionContainer>
|
</StyledActionContainer>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
|
|||||||
@@ -46,44 +46,103 @@ const Sidepanel = () => {
|
|||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
|
||||||
const setIframeState = useCallback(async () => {
|
const setIframeState = useCallback(async () => {
|
||||||
const store = await chrome.storage.local.get();
|
const store = await chrome.storage.local.get([
|
||||||
if (isDefined(store.isAuthenticated)) setIsAuthenticated(true);
|
'isAuthenticated',
|
||||||
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
'sidepanelUrl',
|
||||||
action: 'getActiveTab',
|
'clientUrl',
|
||||||
});
|
'accessToken',
|
||||||
|
'refreshToken',
|
||||||
|
]);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isDefined(activeTab) &&
|
store.isAuthenticated === true &&
|
||||||
isDefined(store[`sidepanelUrl_${activeTab.id}`])
|
isDefined(store.accessToken) &&
|
||||||
|
isDefined(store.refreshToken) &&
|
||||||
|
new Date(store.accessToken.expiresAt).getTime() >= Date.now()
|
||||||
) {
|
) {
|
||||||
const url = store[`sidepanelUrl_${activeTab.id}`];
|
setIsAuthenticated(true);
|
||||||
setClientUrl(url);
|
if (isDefined(store.sidepanelUrl)) {
|
||||||
} else if (isDefined(store.clientUrl)) {
|
if (isDefined(store.clientUrl)) {
|
||||||
|
setClientUrl(`${store.clientUrl}${store.sidepanelUrl}`);
|
||||||
|
} else {
|
||||||
|
setClientUrl(
|
||||||
|
`${import.meta.env.VITE_FRONT_BASE_URL}${store.sidepanelUrl}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chrome.storage.local.set({ isAuthenticated: false });
|
||||||
|
if (isDefined(store.clientUrl)) {
|
||||||
setClientUrl(store.clientUrl);
|
setClientUrl(store.clientUrl);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [setClientUrl]);
|
}, [setClientUrl]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initState = async () => {
|
|
||||||
const store = await chrome.storage.local.get();
|
|
||||||
if (isDefined(store.isAuthenticated)) setIsAuthenticated(true);
|
|
||||||
void setIframeState();
|
void setIframeState();
|
||||||
};
|
}, [setIframeState]);
|
||||||
void initState();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
useEffect(() => {
|
||||||
|
window.addEventListener('message', async (event) => {
|
||||||
|
const store = await chrome.storage.local.get([
|
||||||
|
'clientUrl',
|
||||||
|
'accessToken',
|
||||||
|
'refreshToken',
|
||||||
|
]);
|
||||||
|
const clientUrl = isDefined(store.clientUrl)
|
||||||
|
? store.clientUrl
|
||||||
|
: import.meta.env.VITE_FRONT_BASE_URL;
|
||||||
|
|
||||||
|
if (
|
||||||
|
isDefined(store.accessToken) &&
|
||||||
|
isDefined(store.refreshToken) &&
|
||||||
|
event.origin === clientUrl &&
|
||||||
|
event.data === 'loaded'
|
||||||
|
) {
|
||||||
|
event.source?.postMessage(
|
||||||
|
{
|
||||||
|
type: 'tokens',
|
||||||
|
value: {
|
||||||
|
accessToken: {
|
||||||
|
token: store.accessToken.token,
|
||||||
|
expiresAt: store.accessToken.expiresAt,
|
||||||
|
},
|
||||||
|
refreshToken: {
|
||||||
|
token: store.refreshToken.token,
|
||||||
|
expiresAt: store.refreshToken.expiresAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
clientUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void setIframeState();
|
chrome.storage.local.onChanged.addListener(async (updatedStore) => {
|
||||||
}, [setIframeState, clientUrl]);
|
if (isDefined(updatedStore.isAuthenticated)) {
|
||||||
|
if (updatedStore.isAuthenticated.newValue === true) {
|
||||||
useEffect(() => {
|
|
||||||
chrome.storage.local.onChanged.addListener((store) => {
|
|
||||||
if (isDefined(store.isAuthenticated)) {
|
|
||||||
if (store.isAuthenticated.newValue === true) {
|
|
||||||
setIframeState();
|
setIframeState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDefined(updatedStore.sidepanelUrl)) {
|
||||||
|
if (isDefined(updatedStore.sidepanelUrl.newValue)) {
|
||||||
|
const store = await chrome.storage.local.get(['clientUrl']);
|
||||||
|
const clientUrl = isDefined(store.clientUrl)
|
||||||
|
? store.clientUrl
|
||||||
|
: import.meta.env.VITE_FRONT_BASE_URL;
|
||||||
|
|
||||||
|
iframeRef.current?.contentWindow?.postMessage(
|
||||||
|
{
|
||||||
|
type: 'navigate',
|
||||||
|
value: updatedStore.sidepanelUrl.newValue,
|
||||||
|
},
|
||||||
|
clientUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [setIframeState]);
|
}, [setIframeState]);
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import ReactDOM from 'react-dom/client';
|
|||||||
|
|
||||||
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
|
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
|
||||||
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
|
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
|
||||||
import Sidepanel from '~/options/Sidepanel';
|
import App from '~/options/App';
|
||||||
|
|
||||||
import '~/index.css';
|
import '~/index.css';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
|
||||||
<AppThemeProvider>
|
<AppThemeProvider>
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Sidepanel />
|
<App />
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
</AppThemeProvider>,
|
</AppThemeProvider>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,34 +1,19 @@
|
|||||||
import {
|
import { ApolloClient, from, HttpLink, InMemoryCache } from '@apollo/client';
|
||||||
ApolloClient,
|
|
||||||
from,
|
|
||||||
fromPromise,
|
|
||||||
HttpLink,
|
|
||||||
InMemoryCache,
|
|
||||||
} from '@apollo/client';
|
|
||||||
import { setContext } from '@apollo/client/link/context';
|
import { setContext } from '@apollo/client/link/context';
|
||||||
import { onError } from '@apollo/client/link/error';
|
import { onError } from '@apollo/client/link/error';
|
||||||
|
|
||||||
import { renewToken } from '~/db/token.db';
|
|
||||||
import { Tokens } from '~/db/types/auth.types';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
const clearStore = () => {
|
export const clearStore = () => {
|
||||||
chrome.storage.local.remove(['loginToken', 'accessToken', 'refreshToken']);
|
chrome.storage.local.remove([
|
||||||
|
'loginToken',
|
||||||
|
'accessToken',
|
||||||
|
'refreshToken',
|
||||||
|
'sidepanelUrl',
|
||||||
|
]);
|
||||||
chrome.storage.local.set({ isAuthenticated: false });
|
chrome.storage.local.set({ isAuthenticated: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
const setStore = (tokens: Tokens) => {
|
|
||||||
if (isDefined(tokens.loginToken)) {
|
|
||||||
chrome.storage.local.set({
|
|
||||||
loginToken: tokens.loginToken,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
chrome.storage.local.set({
|
|
||||||
accessToken: tokens.accessToken,
|
|
||||||
refreshToken: tokens.refreshToken,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getServerUrl = async () => {
|
export const getServerUrl = async () => {
|
||||||
const store = await chrome.storage.local.get();
|
const store = await chrome.storage.local.get();
|
||||||
const serverUrl = `${
|
const serverUrl = `${
|
||||||
@@ -46,8 +31,6 @@ const getAuthToken = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getApolloClient = async () => {
|
const getApolloClient = async () => {
|
||||||
const store = await chrome.storage.local.get();
|
|
||||||
|
|
||||||
const authLink = setContext(async (_, { headers }) => {
|
const authLink = setContext(async (_, { headers }) => {
|
||||||
const token = await getAuthToken();
|
const token = await getAuthToken();
|
||||||
return {
|
return {
|
||||||
@@ -57,36 +40,17 @@ const getApolloClient = async () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const errorLink = onError(
|
const errorLink = onError(({ graphQLErrors, networkError }) => {
|
||||||
({ graphQLErrors, networkError, forward, operation }) => {
|
|
||||||
if (isDefined(graphQLErrors)) {
|
if (isDefined(graphQLErrors)) {
|
||||||
for (const graphQLError of graphQLErrors) {
|
for (const graphQLError of graphQLErrors) {
|
||||||
if (graphQLError.message === 'Unauthorized') {
|
if (graphQLError.message === 'Unauthorized') {
|
||||||
return fromPromise(
|
|
||||||
renewToken(store.refreshToken.token)
|
|
||||||
.then((response) => {
|
|
||||||
if (isDefined(response)) {
|
|
||||||
setStore(response.renewToken.tokens);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
clearStore();
|
clearStore();
|
||||||
}),
|
return;
|
||||||
).flatMap(() => forward(operation));
|
|
||||||
}
|
}
|
||||||
switch (graphQLError?.extensions?.code) {
|
switch (graphQLError?.extensions?.code) {
|
||||||
case 'UNAUTHENTICATED': {
|
case 'UNAUTHENTICATED': {
|
||||||
return fromPromise(
|
|
||||||
renewToken(store.refreshToken.token)
|
|
||||||
.then((response) => {
|
|
||||||
if (isDefined(response)) {
|
|
||||||
setStore(response.renewToken.tokens);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
clearStore();
|
clearStore();
|
||||||
}),
|
break;
|
||||||
).flatMap(() => forward(operation));
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
@@ -106,8 +70,7 @@ const getApolloClient = async () => {
|
|||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(`[Network error]: ${networkError}`);
|
console.error(`[Network error]: ${networkError}`);
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const httpLink = new HttpLink({
|
const httpLink = new HttpLink({
|
||||||
uri: await getServerUrl(),
|
uri: await getServerUrl(),
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { useRecoilValue } from 'recoil';
|
|||||||
|
|
||||||
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
|
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
|
||||||
import { VerifyEffect } from '@/auth/components/VerifyEffect';
|
import { VerifyEffect } from '@/auth/components/VerifyEffect';
|
||||||
|
import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect';
|
||||||
|
import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider';
|
||||||
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
|
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
|
||||||
import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
|
import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
|
||||||
import { billingState } from '@/client-config/states/billingState';
|
import { billingState } from '@/client-config/states/billingState';
|
||||||
@@ -85,9 +87,12 @@ const ProvidersThatNeedRouterContext = () => {
|
|||||||
const pageTitle = getPageTitleFromPath(pathname);
|
const pageTitle = getPageTitleFromPath(pathname);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<ApolloProvider>
|
<ApolloProvider>
|
||||||
<ClientConfigProviderEffect />
|
<ClientConfigProviderEffect />
|
||||||
<ClientConfigProvider>
|
<ClientConfigProvider>
|
||||||
|
<ChromeExtensionSidecarEffect />
|
||||||
|
<ChromeExtensionSidecarProvider>
|
||||||
<UserProviderEffect />
|
<UserProviderEffect />
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
<ApolloMetadataClientProvider>
|
<ApolloMetadataClientProvider>
|
||||||
@@ -113,8 +118,10 @@ const ProvidersThatNeedRouterContext = () => {
|
|||||||
</ObjectMetadataItemsProvider>
|
</ObjectMetadataItemsProvider>
|
||||||
</ApolloMetadataClientProvider>
|
</ApolloMetadataClientProvider>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
|
</ChromeExtensionSidecarProvider>
|
||||||
</ClientConfigProvider>
|
</ClientConfigProvider>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ export type ClientConfig = {
|
|||||||
authProviders: AuthProviders;
|
authProviders: AuthProviders;
|
||||||
billing: Billing;
|
billing: Billing;
|
||||||
captcha: Captcha;
|
captcha: Captcha;
|
||||||
|
chromeExtensionId?: Maybe<Scalars['String']>;
|
||||||
debugMode: Scalars['Boolean'];
|
debugMode: Scalars['Boolean'];
|
||||||
sentry: Sentry;
|
sentry: Sentry;
|
||||||
signInPrefilled: Scalars['Boolean'];
|
signInPrefilled: Scalars['Boolean'];
|
||||||
@@ -1186,7 +1187,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat
|
|||||||
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null } } };
|
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null } } };
|
||||||
|
|
||||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, currentCacheVersion?: string | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: string, interval?: string | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
|
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, currentCacheVersion?: string | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: string, interval?: string | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
|
||||||
|
|
||||||
@@ -2307,6 +2308,7 @@ export const GetClientConfigDocument = gql`
|
|||||||
provider
|
provider
|
||||||
siteKey
|
siteKey
|
||||||
}
|
}
|
||||||
|
chromeExtensionId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||||
|
import { isLoadingTokensFromExtensionState } from '@/chrome-extension-sidecar/states/isLoadingTokensFromExtensionState';
|
||||||
|
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { isInFrame } from '~/utils/isInIframe';
|
||||||
|
|
||||||
|
export const ChromeExtensionSidecarEffect = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||||
|
const chromeExtensionId = useRecoilValue(chromeExtensionIdState);
|
||||||
|
const setIsLoadingTokensFromExtension = useSetRecoilState(
|
||||||
|
isLoadingTokensFromExtensionState,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInFrame() && isDefined(chromeExtensionId)) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
'loaded',
|
||||||
|
`chrome-extension://${chromeExtensionId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleWindowEvents = (event: MessageEvent<any>) => {
|
||||||
|
if (event.origin === `chrome-extension://${chromeExtensionId}`) {
|
||||||
|
switch (event.data.type) {
|
||||||
|
case 'tokens': {
|
||||||
|
setTokenPair(event.data.value);
|
||||||
|
setIsLoadingTokensFromExtension(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'navigate':
|
||||||
|
navigate(event.data.value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsLoadingTokensFromExtension(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('message', handleWindowEvents);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', handleWindowEvents);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
chromeExtensionId,
|
||||||
|
setIsLoadingTokensFromExtension,
|
||||||
|
setTokenPair,
|
||||||
|
navigate,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { isLoadingTokensFromExtensionState } from '@/chrome-extension-sidecar/states/isLoadingTokensFromExtensionState';
|
||||||
|
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { isInFrame } from '~/utils/isInIframe';
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AppInaccessible = ({ message }: { message: string }) => {
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<img
|
||||||
|
src="/images/integrations/twenty-logo.svg"
|
||||||
|
alt="twenty-icon"
|
||||||
|
height={40}
|
||||||
|
width={40}
|
||||||
|
/>
|
||||||
|
<h3>{message}</h3>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChromeExtensionSidecarProvider: React.FC<
|
||||||
|
React.PropsWithChildren
|
||||||
|
> = ({ children }) => {
|
||||||
|
const isLoadingTokensFromExtension = useRecoilValue(
|
||||||
|
isLoadingTokensFromExtensionState,
|
||||||
|
);
|
||||||
|
const chromeExtensionId = useRecoilValue(chromeExtensionIdState);
|
||||||
|
|
||||||
|
if (!isInFrame()) return <>{children}</>;
|
||||||
|
|
||||||
|
if (!isDefined(chromeExtensionId))
|
||||||
|
return (
|
||||||
|
<AppInaccessible message={`Twenty is not accessible inside an iframe.`} />
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDefined(isLoadingTokensFromExtension) && !isLoadingTokensFromExtension)
|
||||||
|
return (
|
||||||
|
<AppInaccessible
|
||||||
|
message={`Unauthorized access from iframe origin. If you're trying to access from chrome extension,
|
||||||
|
please check your chrome extension ID on your server.
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return isLoadingTokensFromExtension && <>{children}</>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const isLoadingTokensFromExtensionState = createState<boolean | null>({
|
||||||
|
key: 'isLoadingTokensFromExtensionState',
|
||||||
|
defaultValue: null,
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import { useRecoilState, useSetRecoilState } from 'recoil';
|
|||||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||||
import { billingState } from '@/client-config/states/billingState';
|
import { billingState } from '@/client-config/states/billingState';
|
||||||
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
|
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
|
||||||
|
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
|
||||||
import { isClientConfigLoadedState } from '@/client-config/states/isClientConfigLoadedState';
|
import { isClientConfigLoadedState } from '@/client-config/states/isClientConfigLoadedState';
|
||||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||||
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
||||||
@@ -32,6 +33,8 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
|
|
||||||
const setCaptchaProvider = useSetRecoilState(captchaProviderState);
|
const setCaptchaProvider = useSetRecoilState(captchaProviderState);
|
||||||
|
|
||||||
|
const setChromeExtensionId = useSetRecoilState(chromeExtensionIdState);
|
||||||
|
|
||||||
const { data, loading } = useGetClientConfigQuery({
|
const { data, loading } = useGetClientConfigQuery({
|
||||||
skip: isClientConfigLoaded,
|
skip: isClientConfigLoaded,
|
||||||
});
|
});
|
||||||
@@ -63,6 +66,8 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
provider: data?.clientConfig?.captcha?.provider,
|
provider: data?.clientConfig?.captcha?.provider,
|
||||||
siteKey: data?.clientConfig?.captcha?.siteKey,
|
siteKey: data?.clientConfig?.captcha?.siteKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setChromeExtensionId(data?.clientConfig?.chromeExtensionId);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
data,
|
data,
|
||||||
@@ -77,6 +82,7 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
loading,
|
loading,
|
||||||
setIsClientConfigLoaded,
|
setIsClientConfigLoaded,
|
||||||
setCaptchaProvider,
|
setCaptchaProvider,
|
||||||
|
setChromeExtensionId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export const GET_CLIENT_CONFIG = gql`
|
|||||||
provider
|
provider
|
||||||
siteKey
|
siteKey
|
||||||
}
|
}
|
||||||
|
chromeExtensionId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const chromeExtensionIdState = createState<string | null | undefined>({
|
||||||
|
key: 'chromeExtensionIdState',
|
||||||
|
defaultValue: null,
|
||||||
|
});
|
||||||
7
packages/twenty-front/src/utils/isInIframe.ts
Normal file
7
packages/twenty-front/src/utils/isInIframe.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const isInFrame = () => {
|
||||||
|
try {
|
||||||
|
return window.self !== window.top;
|
||||||
|
} catch (e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -71,5 +71,5 @@ SIGN_IN_PREFILLED=true
|
|||||||
# API_RATE_LIMITING_TTL=
|
# API_RATE_LIMITING_TTL=
|
||||||
# API_RATE_LIMITING_LIMIT=
|
# API_RATE_LIMITING_LIMIT=
|
||||||
# MUTATION_MAXIMUM_RECORD_AFFECTED=100
|
# MUTATION_MAXIMUM_RECORD_AFFECTED=100
|
||||||
# CHROME_EXTENSION_REDIRECT_URL=https://bggmipldbceihilonnbpgoeclgbkblkp.chromiumapp.org
|
# CHROME_EXTENSION_ID=bggmipldbceihilonnbpgoeclgbkblkp
|
||||||
# PG_SSL_ALLOW_SELF_SIGNED=true
|
# PG_SSL_ALLOW_SELF_SIGNED=true
|
||||||
|
|||||||
@@ -203,7 +203,9 @@ export class AuthService {
|
|||||||
this.environmentService.get('NODE_ENV') ===
|
this.environmentService.get('NODE_ENV') ===
|
||||||
NodeEnvironment.development
|
NodeEnvironment.development
|
||||||
? authorizeAppInput.redirectUrl
|
? authorizeAppInput.redirectUrl
|
||||||
: `${this.environmentService.get('CHROME_EXTENSION_REDIRECT_URL')}`,
|
: `https://${this.environmentService.get(
|
||||||
|
'CHROME_EXTENSION_ID',
|
||||||
|
)}.chromiumapp.org/`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -96,4 +96,7 @@ export class ClientConfig {
|
|||||||
|
|
||||||
@Field(() => Captcha)
|
@Field(() => Captcha)
|
||||||
captcha: Captcha;
|
captcha: Captcha;
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
chromeExtensionId: string | undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export class ClientConfigResolver {
|
|||||||
provider: this.environmentService.get('CAPTCHA_DRIVER'),
|
provider: this.environmentService.get('CAPTCHA_DRIVER'),
|
||||||
siteKey: this.environmentService.get('CAPTCHA_SITE_KEY'),
|
siteKey: this.environmentService.get('CAPTCHA_SITE_KEY'),
|
||||||
},
|
},
|
||||||
|
chromeExtensionId: this.environmentService.get('CHROME_EXTENSION_ID'),
|
||||||
};
|
};
|
||||||
|
|
||||||
return Promise.resolve(clientConfig);
|
return Promise.resolve(clientConfig);
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ export class EnvironmentVariables {
|
|||||||
|
|
||||||
AUTH_GOOGLE_APIS_CALLBACK_URL: string;
|
AUTH_GOOGLE_APIS_CALLBACK_URL: string;
|
||||||
|
|
||||||
CHROME_EXTENSION_REDIRECT_URL: string;
|
CHROME_EXTENSION_ID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validate = (
|
export const validate = (
|
||||||
|
|||||||
24
yarn.lock
24
yarn.lock
@@ -16113,13 +16113,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/crypto-js@npm:^4.2.2":
|
|
||||||
version: 4.2.2
|
|
||||||
resolution: "@types/crypto-js@npm:4.2.2"
|
|
||||||
checksum: 760a2078f36f2a3a1089ef367b0d13229876adcf4bcd6e8824d00d9e9bfad8118dc7e6a3cc66322b083535e12be3a29044ccdc9603bfb12519ff61551a3322c6
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@types/d3-array@npm:*":
|
"@types/d3-array@npm:*":
|
||||||
version: 3.2.1
|
version: 3.2.1
|
||||||
resolution: "@types/d3-array@npm:3.2.1"
|
resolution: "@types/d3-array@npm:3.2.1"
|
||||||
@@ -16452,13 +16445,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/dagre@npm:^0.7.52":
|
|
||||||
version: 0.7.52
|
|
||||||
resolution: "@types/dagre@npm:0.7.52"
|
|
||||||
checksum: 0e196a8c17a92765d6e28b10d78d5c1cb1ee540598428cbb61ce3b90e0fedaac2b11f6dbeebf0d2f69d5332d492b12091be5f1e575f538194e20d8887979d006
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@types/debug@npm:^4.0.0":
|
"@types/debug@npm:^4.0.0":
|
||||||
version: 4.1.12
|
version: 4.1.12
|
||||||
resolution: "@types/debug@npm:4.1.12"
|
resolution: "@types/debug@npm:4.1.12"
|
||||||
@@ -23960,13 +23946,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"crypto-js@npm:^4.2.0":
|
|
||||||
version: 4.2.0
|
|
||||||
resolution: "crypto-js@npm:4.2.0"
|
|
||||||
checksum: 8fbdf9d56f47aea0794ab87b0eb9833baf80b01a7c5c1b0edc7faf25f662fb69ab18dc2199e2afcac54670ff0cd9607a9045a3f7a80336cccd18d77a55b9fdf0
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"crypto-random-string@npm:^2.0.0":
|
"crypto-random-string@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "crypto-random-string@npm:2.0.0"
|
resolution: "crypto-random-string@npm:2.0.0"
|
||||||
@@ -46918,8 +46897,6 @@ __metadata:
|
|||||||
"@types/better-sqlite3": "npm:^7.6.8"
|
"@types/better-sqlite3": "npm:^7.6.8"
|
||||||
"@types/bytes": "npm:^3.1.1"
|
"@types/bytes": "npm:^3.1.1"
|
||||||
"@types/chrome": "npm:^0.0.267"
|
"@types/chrome": "npm:^0.0.267"
|
||||||
"@types/crypto-js": "npm:^4.2.2"
|
|
||||||
"@types/dagre": "npm:^0.7.52"
|
|
||||||
"@types/deep-equal": "npm:^1.0.1"
|
"@types/deep-equal": "npm:^1.0.1"
|
||||||
"@types/dompurify": "npm:^3.0.5"
|
"@types/dompurify": "npm:^3.0.5"
|
||||||
"@types/express": "npm:^4.17.13"
|
"@types/express": "npm:^4.17.13"
|
||||||
@@ -46980,7 +46957,6 @@ __metadata:
|
|||||||
concurrently: "npm:^8.2.2"
|
concurrently: "npm:^8.2.2"
|
||||||
cross-env: "npm:^7.0.3"
|
cross-env: "npm:^7.0.3"
|
||||||
cross-var: "npm:^1.1.0"
|
cross-var: "npm:^1.1.0"
|
||||||
crypto-js: "npm:^4.2.0"
|
|
||||||
danger: "npm:^11.3.0"
|
danger: "npm:^11.3.0"
|
||||||
danger-plugin-todos: "npm:^1.3.1"
|
danger-plugin-todos: "npm:^1.3.1"
|
||||||
dataloader: "npm:^2.2.2"
|
dataloader: "npm:^2.2.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user