Skip to content

Commit

Permalink
Extension popup with Sign In / Sign Out (#14)
Browse files Browse the repository at this point in the history
* add 'cookies' to permissions to be able to log in using existing gk.dev cookie

* add a sign in link to the popup

* add user info element to popup

* add a sign out button to the popup

* fix build: add static files to bundle
  • Loading branch information
jdgarcia authored Jan 17, 2024
1 parent c5c468b commit 18ecd16
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 5 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand Down
13 changes: 11 additions & 2 deletions scripts/makeManifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
143 changes: 143 additions & 0 deletions src/popup.ts
Original file line number Diff line number Diff line change
@@ -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();
54 changes: 54 additions & 0 deletions static/popup.css
Original file line number Diff line number Diff line change
@@ -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;
}
7 changes: 7 additions & 0 deletions static/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<html>
<script src="/dist/popup.js"></script>
<link rel="stylesheet" href="popup.css" />
<body>
<main id="main-content"></main>
</body>
</html>
1 change: 1 addition & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 18ecd16

Please sign in to comment.