Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
11 changes: 7 additions & 4 deletions distribution/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@
<strong>Personal token</strong> (optional, <a id="personal-token-link" href="https://github.com/settings/tokens/new?description=Refined%20GitHub&scopes=repo" target="_blank">generate one</a>)<br>
<!-- placeholder is set to enable use of :placeholder-shown CSS selector -->
<input type="text" name="personalToken" spellcheck="false" autocomplete="off" pattern="[\da-f]{40}" placeholder=" ">
<span id="validation"></span>
</label>
</p>
<p>
The token enables <a href="https://github.com/sindresorhus/refined-github/search?q=github-helpers+api" target="_blank">some features</a> to <strong>read</strong> data from public repositories
</p>
<ul>
<li>The token enables <a href="https://github.com/sindresorhus/refined-github/search?q=github-helpers+api" target="_blank">some features</a> to read data from public repositories
<li>The <code>public_repo</code> scope lets them edit your public repositories
<li>The <code>repo</code> scope lets them edit private repositories as well
<li>The <code>delete_repo</code> scope is only used by the <code>quick-fork-deletion</code> feature
<li data-validation data-scope="public_repo">The <code>public_repo</code> scope lets them <strong>edit</strong> your public repositories
<li data-validation data-scope="repo">The <code>repo</code> scope lets them <strong>edit private</strong> repositories as well
<li data-validation data-scope="delete_repo">The <code>delete_repo</code> scope is only used by the <code>quick-repo-deletion</code> feature
</ul>

<hr>
Expand Down
25 changes: 24 additions & 1 deletion source/options.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,29 @@ p {
}

ul {
padding-left: 1.5em;
padding-left: 0;
list-style: none;
}

li[data-validation] {
margin-bottom: 0.3em;
}

[data-validation]::before {
content: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" fill="gray" d="M8 5.5a2.5 2.5 0 100 5 2.5 2.5 0 000-5zM4 8a4 4 0 118 0 4 4 0 01-8 0z"></path></svg>');
width: 16px;
height: 16px;
vertical-align: -4px;
margin-right: 0.3em;
display: inline-block;
}

[data-validation='valid']::before {
content: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" fill="%2328a745" d="M8 16A8 8 0 108 0a8 8 0 000 16zm3.78-9.72a.75.75 0 00-1.06-1.06L6.75 9.19 5.28 7.72a.75.75 0 00-1.06 1.06l2 2a.75.75 0 001.06 0l4.5-4.5z"></path></svg>');
}

[data-validation='invalid']::before {
content: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" fill="%23cb2431" d="M1.5 8a6.5 6.5 0 0110.535-5.096l-9.131 9.131A6.472 6.472 0 011.5 8zm2.465 5.096a6.5 6.5 0 009.131-9.131l-9.131 9.131zM8 0a8 8 0 100 16A8 8 0 008 0z"></path></svg>');
}

:root [name='customCSS'],
Expand All @@ -25,6 +47,7 @@ ul {

[name='personalToken'] {
width: 20em !important; /* https://github.com/sindresorhus/refined-github/issues/1374#issuecomment-397906701 */
display: inline-block !important;
}

[name='personalToken']:invalid {
Expand Down
82 changes: 80 additions & 2 deletions source/options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,77 @@ import * as indentTextarea from 'indent-textarea';

import {perDomainOptions} from './options-storage';

interface Status {
error?: true;
text?: string;
scopes?: string[];
}

function reportStatus({error, text, scopes}: Status): void {
const tokenStatus = select('#validation')!;
tokenStatus.textContent = text ?? '';
if (error) {
tokenStatus.dataset.validation = 'invalid';
} else {
delete tokenStatus.dataset.validation;
}

for (const scope of select.all('[data-scope]')) {
if (scopes) {
scope.dataset.validation = scopes.includes(scope.dataset.scope!) ? 'valid' : 'invalid';
} else {
scope.dataset.validation = '';
}
}
}

async function getTokenScopes(personalToken: string): Promise<string[]> {
const tokenLink = select('a#personal-token-link')!;
const url = tokenLink.host === 'github.com' ?
'https://api.github.com/' :
`${tokenLink.origin}/api/v3/`;

const response = await fetch(url, {
cache: 'no-store',
headers: {
'User-Agent': 'Refined GitHub',
Accept: 'application/vnd.github.v3+json',
Authorization: `token ${personalToken}`
}
});

if (!response.ok) {
const details = await response.json();
throw new Error(details.message);
}

const scopes = response.headers.get('X-OAuth-Scopes')!.split(', ');
if (scopes.includes('repo')) {
scopes.push('public_repo');
}

return scopes;
}

async function validateToken(): Promise<void> {
reportStatus({});
const tokenField = select('input[name="personalToken"]')!;
if (!tokenField.validity.valid || tokenField.value.length === 0) {
return;
}

reportStatus({text: 'Validating…'});

try {
reportStatus({
scopes: await getTokenScopes(tokenField.value)
});
} catch (error: unknown) {
reportStatus({error: true, text: (error as Error).message});
throw error;
}
}

function moveDisabledFeaturesToTop(): void {
const container = select('.js-features')!;
for (const unchecked of select.all('.feature [type=checkbox]:not(:checked)', container).reverse()) {
Expand Down Expand Up @@ -90,6 +161,7 @@ async function generateDom(): Promise<void> {
// Decorate list
moveDisabledFeaturesToTop();
void highlightNewFeatures();
void validateToken();

// Move debugging tools higher when side-loaded
if (process.env.NODE_ENV === 'development') {
Expand All @@ -99,8 +171,11 @@ async function generateDom(): Promise<void> {

function addEventListeners(): void {
// Update domain-dependent page content when the domain is changed
select('.js-options-sync-selector')?.addEventListener('change', ({currentTarget: dropdown}) => {
select('a#personal-token-link')!.host = (dropdown as HTMLSelectElement).value;
select('.OptionsSyncPerDomain-picker select')?.addEventListener('change', ({currentTarget: dropdown}) => {
const host = (dropdown as HTMLSelectElement).value;
select('a#personal-token-link')!.host = host === 'default' ? 'github.com' : host;
// Delay validating to let options load first
setTimeout(validateToken, 100);
});

// Refresh page when permissions are changed (because the dropdown selector needs to be regenerated)
Expand All @@ -121,6 +196,9 @@ function addEventListeners(): void {
// Add cache clearer
select('#clear-cache')!.addEventListener('click', clearCacheHandler);

// Add token validation
select('[name="personalToken"]')!.addEventListener('input', validateToken);

// Ensure all links open in a new tab #3181
delegate(document, '[href^="http"]', 'click', (event: delegate.Event<MouseEvent, HTMLAnchorElement>) => {
event.preventDefault();
Expand Down