diff --git a/package.json b/package.json index 0d6cc3b..3610230 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,9 @@ "sign:firefox": "cross-var node ./scripts/signFireFoxAddon.mjs \"%npm_package_name%-%npm_package_version%-firefox.zip\" \"%npm_package_version%\"", "test": "yarn playwright test", "package": "yarn run package:chromium && yarn run package:firefox", - "package:chromium": "yarn run bundle:chromium && cross-var npx bestzip \"%npm_package_name%-%npm_package_version%-chromium.zip\" dist icons manifest.json LICENSE", - "package:firefox": "yarn run bundle:firefox && cross-var npx bestzip \"%npm_package_name%-%npm_package_version%-firefox.zip\" dist icons manifest.json LICENSE && yarn run sign:firefox", - "package:firefox-no-sign": "yarn run bundle:firefox && cross-var npx bestzip \"%npm_package_name%-%npm_package_version%-firefox.zip\" dist icons manifest.json LICENSE", + "package:chromium": "yarn run bundle:chromium && cross-var npx bestzip \"%npm_package_name%-%npm_package_version%-chromium.zip\" dist static icons manifest.json LICENSE", + "package:firefox": "yarn run bundle:firefox && cross-var npx bestzip \"%npm_package_name%-%npm_package_version%-firefox.zip\" dist static icons manifest.json LICENSE && yarn run sign:firefox", + "package:firefox-no-sign": "yarn run bundle:firefox && cross-var npx bestzip \"%npm_package_name%-%npm_package_version%-firefox.zip\" dist static icons manifest.json LICENSE", "package-pre": "yarn run patch-pre && yarn run package", "patch-pre": "node ./scripts/applyPreReleasePatch.js", "pretty": "prettier --config .prettierrc --loglevel warn --write .", diff --git a/scripts/makeManifest.js b/scripts/makeManifest.js index d305b68..6df37e8 100644 --- a/scripts/makeManifest.js +++ b/scripts/makeManifest.js @@ -18,8 +18,17 @@ const manifestBase = { 48: 'icons/logo-48.png', 128: 'icons/logo-128.png', }, - permissions: ['scripting', 'webNavigation'], - host_permissions: ['*://*.github.com/*', '*://*.gitlab.com/*', '*://*.bitbucket.org/*', '*://*.dev.azure.com/*'], + permissions: ['scripting', 'webNavigation', 'cookies'], + host_permissions: [ + '*://*.github.com/*', + '*://*.gitlab.com/*', + '*://*.bitbucket.org/*', + '*://*.dev.azure.com/*', + '*://*.gitkraken.dev/*', + ], + action: { + default_popup: 'static/popup.html', + } }; const getMakeManifest = diff --git a/src/popup.ts b/src/popup.ts new file mode 100644 index 0000000..14920e5 --- /dev/null +++ b/src/popup.ts @@ -0,0 +1,143 @@ +// Note: This code runs every time the extension popup is opened. + +import { cookies } from 'webextension-polyfill'; + +interface User { + email: string; + name?: string; + username: string; +} + +// Source: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#basic_example +const sha256 = async (text: string) => { + const encoder = new TextEncoder(); + const data = encoder.encode(text); + const hash = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hash)); + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + return hashHex; +}; + +const getAccessToken = async () => { + // Attempt to get the access token cookie from GitKraken.dev + const cookie = await cookies.get({ + url: 'https://gitkraken.dev', + name: 'accessToken' + }); + + return cookie?.value; +}; + +const fetchUser = async () => { + const token = await getAccessToken(); + if (!token) { + // The user is not logged in. + return; + } + + const res = await fetch('https://api.gitkraken.dev/user', { + headers: { + Authorization: `Bearer ${token}` + } + }); + + if (!res.ok) { + // The access token is invalid or expired. + return; + } + + const user = await res.json(); + return user as User; +}; + +const logout = async () => { + const token = await getAccessToken(); + if (!token) { + // The user is not logged in. + return; + } + + const res = await fetch('https://api.gitkraken.dev/user/logout', { + method: 'POST', + headers: { + Authorization: `Bearer ${token}` + } + }); + + if (!res.ok) { + // The access token is invalid or expired. + return; + } + + // Attempt to clean up the access token cookie from GitKraken.dev + await cookies.remove({ + url: 'https://gitkraken.dev', + name: 'accessToken' + }); +}; + +const renderLoggedInContent = async (user: User) => { + const emailHash = await sha256(user.email); + + const mainEl = document.getElementById('main-content')!; + + const userEl = document.createElement('div'); + userEl.classList.add('user'); + + const gravatarEl = document.createElement('img'); + gravatarEl.src = `https://www.gravatar.com/avatar/${emailHash}?s=30&d=retro`; + gravatarEl.alt = user.name || user.username; + gravatarEl.classList.add('avatar'); + userEl.appendChild(gravatarEl); + + const userInfoEl = document.createElement('div'); + userInfoEl.classList.add('user-info'); + + const userNameEl = document.createElement('div'); + userNameEl.textContent = user.name || user.username; + userNameEl.classList.add('user-name'); + userInfoEl.appendChild(userNameEl); + + const userEmailEl = document.createElement('div'); + userEmailEl.textContent = user.email; + userEmailEl.classList.add('user-email'); + userInfoEl.appendChild(userEmailEl); + + userEl.appendChild(userInfoEl); + + mainEl.appendChild(userEl); + + const signOutBtn = document.createElement('button'); + signOutBtn.textContent = 'Sign out'; + signOutBtn.classList.add('btn'); + signOutBtn.addEventListener('click', async () => { + await logout(); + window.close(); + }); + mainEl.appendChild(signOutBtn); +}; + +const renderLoggedOutContent = () => { + const mainEl = document.getElementById('main-content')!; + + const signInLink = document.createElement('a'); + signInLink.href = 'https://gitkraken.dev/login'; + signInLink.target = '_blank'; + signInLink.textContent = 'Sign in GitKraken account'; + signInLink.classList.add('btn'); + + mainEl.appendChild(signInLink); +}; + +const main = async () => { + const user = await fetchUser(); + if (user) { + void renderLoggedInContent(user); + } else { + renderLoggedOutContent(); + } +}; + +void main(); diff --git a/static/popup.css b/static/popup.css new file mode 100644 index 0000000..25d6f4b --- /dev/null +++ b/static/popup.css @@ -0,0 +1,54 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; +} + +#main-content { + width: 287px; + padding: 8px; + font-size: 12px; +} + +#main-content > *:not(:last-child) { + margin-bottom: 4px; +} + +a.btn { + color: black; + text-decoration: none; + display: block; + padding: 5px; +} +button.btn { + background: none; + border: none; + cursor: pointer; + display: block; + width: 100%; + text-align: start; + padding: 5px; +} + +img.avatar { + border-radius: 50%; + border: 2px solid #7D868F; +} + +.user { + display: flex; + align-items: center; + border: 1px solid #D7D8DB; + border-radius: 6px; + padding: 8px; +} +.user-info { + margin-left: 8px; +} +.user-info .user-name { + font-weight: bold; +} +.user-info .user-email { + font-size: 10px; +} \ No newline at end of file diff --git a/static/popup.html b/static/popup.html new file mode 100644 index 0000000..601d3a5 --- /dev/null +++ b/static/popup.html @@ -0,0 +1,7 @@ + + + + +
+ + \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 48722ff..5d5383c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -61,6 +61,7 @@ function getExtensionConfig(mode, env) { entry: { background: './src/background.ts', 'service-worker': './src/service-worker.ts', + popup: './src/popup.ts', }, mode: mode, target: 'web',