Skip to content

Commit

Permalink
Merge pull request #8 from TomCasavant/oauth
Browse files Browse the repository at this point in the history
Oauth Authentication improvements v0.0.3
  • Loading branch information
TomCasavant authored Aug 28, 2024
2 parents 2e76f73 + bd5e27a commit 228d9da
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 43 deletions.
175 changes: 155 additions & 20 deletions background.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,165 @@
// Message listener for various actions
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'searchMastodon') {
// Load saved settings from localStorage
const mastodonDomain = localStorage.getItem('domain');
const apiKey = localStorage.getItem('apiKey');
const numPosts = localStorage.getItem('numPosts') || 5;
const searchTerm = message.searchTerm;
// Load saved settings from browser.storage.local
browser.storage.local.get(['client_id', 'client_secret', 'access_token', 'domain', 'numPosts'])
.then(({ client_id, client_secret, access_token, domain, numPosts = 5 }) => {
console.log(access_token);
console.log(domain);
if (!access_token || !domain) {
sendResponse({ success: false, error: 'Missing access token or domain.' });
return;
}

const searchTerm = message.searchTerm;
fetch(`https://${domain}/api/v2/search?q=${encodeURIComponent(searchTerm)}&resolve=true&limit=${numPosts}`, {
headers: {
'Authorization': `Bearer ${access_token}`
}
})
.then(response => response.json())
.then(data => {
console.log(data.statuses); //TODO: For some reason this sendResponse does not work unless I log data.statuses. I have no idea why. Best guess is there's some sort of race condition going on? Though I'm not familiar enough with javascript to know if that's true
sendResponse({ success: true, results: data.statuses });
})
.catch(error => {
sendResponse({ success: false, error: error.message });
});

fetch(`https://${mastodonDomain}/api/v2/search?q=${encodeURIComponent(searchTerm)}&resolve=true&limit=${numPosts}`, {
headers: {
'Authorization': `Bearer ${apiKey}`
}
})
.then(response => response.json())
.then(data => {
sendResponse({ success: true, results: data.statuses });
})
.catch(error => {
return true;
})
.catch(error => {
sendResponse({ success: false, error: 'Failed to retrieve saved settings.' });
});
return true;
} else if (message.action === 'getSettings') {
browser.storage.local.get(['domain', 'numPosts'])
.then(({ domain, numPosts = 5 }) => {
sendResponse({ domain, numPosts });
})
.catch(error => {
sendResponse({ success: false, error: 'Failed to retrieve settings.' });
});

return true;
} else if (message.action === 'authorize') {
const domain = message.domain;
let appRegistrate = null;

// Register the app and start the OAuth flow
registerApp(domain).then(appRegistration => {
console.log('App registration successful:', appRegistration);
appRegistrate = appRegistration;
return launchOAuthFlow(appRegistration, domain);
}).then(redirectUrl => {
return validate(redirectUrl, domain, appRegistrate); // Pass appRegistration here
}).then(() => {
sendResponse({ success: true });
}).catch(error => {
console.error('Error during OAuth process:', error);
sendResponse({ success: false, error: error.message });
});

return true;
}
else if (message.action === 'getSettings') {
const domain = localStorage.getItem('domain');
const numPosts = parseInt(localStorage.getItem('numPosts'));
sendResponse({ domain, numPosts });
}
});

// Registers the app with a mastodon server
async function registerApp(domain) {
try {
const response = await fetch(`https://${domain}/api/v1/apps`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
client_name: 'DuckDuckSocial',
redirect_uris: browser.identity.getRedirectURL(), // Use the add-on's redirect URL
scopes: 'read write',
website: 'https://tomcasavant.com'
})
});

if (!response.ok) {
const errorText = await response.text();
console.error('Error registering app:', errorText);
throw new Error('Failed to register app');
}

const appData = await response.json();
await browser.storage.local.set({
client_id: appData.client_id,
client_secret: appData.client_secret,
redirect_uri: appData.redirect_uri
});

return appData;
} catch (error) {
console.error('Error during app registration:', error);
throw error;
}
}

// Launches OAuth flow
function launchOAuthFlow(appRegistration, domain) {
const authorizationUrl = `https://${domain}/oauth/authorize?client_id=${appRegistration.client_id}&redirect_uri=${encodeURIComponent(appRegistration.redirect_uri)}&response_type=code&scope=read`;

return browser.identity.launchWebAuthFlow({
interactive: true,
url: authorizationUrl
});
}

// Get access token from Authorization
async function exchangeCodeForToken(code, domain, appRegistration) {
try {
const response = await fetch(`https://${domain}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
client_id: appRegistration.client_id,
client_secret: appRegistration.client_secret,
redirect_uri: appRegistration.redirect_uri,
grant_type: 'authorization_code',
code: code
})
});

if (!response.ok) {
const errorText = await response.text();
console.error('Error exchanging code for token:', errorText);
throw new Error('Failed to exchange authorization code for access token');
}

const tokenData = await response.json();
await browser.storage.local.set({ access_token: tokenData.access_token });
} catch (error) {
console.error('Error during token exchange:', error);
throw error;
}
}

// Validates the redirect URL
async function validate(redirectUrl, domain, appRegistration) {
try {
console.log('Redirect URL:', redirectUrl);

if (redirectUrl) {
const code = new URL(redirectUrl).searchParams.get('code');

if (code) {
await exchangeCodeForToken(code, domain, appRegistration);
console.log('Access token saved successfully.');
} else {
throw new Error('Authorization code not found in redirect URL.');
}
} else {
throw new Error('No redirect URL returned.');
}
} catch (error) {
console.error('Validation error:', error);
throw error;
}
}
2 changes: 2 additions & 0 deletions content.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ if (searchTerm) {

browser.runtime.sendMessage({ action: 'searchMastodon', searchTerm: searchTerm })
.then(response => {
console.log(response);
if (response.success) {
console.log(response);
const results = response.results;

var div = document.createElement('div');
Expand Down
5 changes: 3 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "DuckDuckSocial",
"version": "0.0.2",
"version": "0.0.3",
"description": "Adds a scrollable list of posts from the social web that match your DuckDuckGo search. Requires API key from mastodon compatible server",
"icons": {
"512": "icons/border-512.png"
Expand All @@ -24,7 +24,8 @@
"permissions": [
"webRequest",
"webRequestBlocking",
"storage"
"storage",
"identity"
],
"browser_specific_settings": {
"gecko": {
Expand Down
61 changes: 46 additions & 15 deletions options.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,68 @@
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('settingsForm');
const domainInput = document.getElementById('domain');
const apiKeyInput = document.getElementById('apiKey');
const numPostsInput = document.getElementById('numPosts');
const messageElement = document.querySelector('.message');

// Load saved settings
loadSettings();

// Handle form submission
form.addEventListener('submit', function(event) {
event.preventDefault();
saveSettings();
});

document.getElementById('connectMastodon').addEventListener('click', function(event) {
const domain = domainInput.value;

if (!domain) {
updateMessage('Mastodon server domain is required!');
return;
}

browser.runtime.sendMessage({
action: 'authorize',
domain: domain
}).then(response => {
if (response.success) {
updateMessage('Connected to Mastodon successfully!');
} else {
updateMessage('Failed to connect to Mastodon: ' + response.error);
}
}).catch(error => {
console.error('Error during OAuth process:', error);
updateMessage('Failed to connect to Mastodon. Please try again.');
});
});

function saveSettings() {
const domain = domainInput.value;
const apiKey = apiKeyInput.value;
const numPosts = numPostsInput.value;

localStorage.setItem('domain', domain);
localStorage.setItem('apiKey', apiKey);
localStorage.setItem('numPosts', numPosts);

alert('Settings saved successfully!');
browser.storage.local.set({
domain: domain,
numPosts: numPosts
}).then(() => {
updateMessage('Settings saved successfully!');
}).catch(error => {
console.error('Failed to save settings:', error);
updateMessage('Failed to save settings.');
});
}

function loadSettings() {
const savedDomain = localStorage.getItem('domain');
const savedApiKey = localStorage.getItem('apiKey');
const savedNumPosts = localStorage.getItem('numPosts');

if (savedDomain) domainInput.value = savedDomain;
if (savedApiKey) apiKeyInput.value = savedApiKey;
if (savedNumPosts) numPostsInput.value = savedNumPosts;
browser.storage.local.get(['domain', 'numPosts'])
.then(({ domain, numPosts = 5 }) => {
if (domain) domainInput.value = domain;
if (numPosts) numPostsInput.value = numPosts;
})
.catch(error => {
console.error('Failed to load settings:', error);
});
}

function updateMessage(message) {
messageElement.textContent = message;
messageElement.style.color = 'red';
}
});
11 changes: 5 additions & 6 deletions settings.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>Fedi Search</title>
<title>DuckDuckSocial</title>
<style>
body {
font-family: Arial, sans-serif;
Expand All @@ -20,19 +20,18 @@
</style>
</head>
<body>
<h1>Fedi Search</h1>
<h1>DuckDuckSocial</h1>
<form id="settingsForm">
<label for="domain">Mastodon Compatible Domain:</label>
<input type="text" id="domain" name="domain" placeholder="mastodon.social">

<label for="apiKey">API Token:</label>
<input type="password" id="apiKey" name="apiKey" placeholder="aabbcc112233">

<label for="numPosts">Number of posts to display:</label>
<input type="number" id="numPosts" name="numPosts" min="1" value="5" />

<button type="submit">Save</button>
<button type="submit">Save Settings</button>
<button id="connectMastodon">Reconnect Mastodon</button>
</form>
<p class="message"></p>
<script src="options.js"></script>
</body>
</html>

0 comments on commit 228d9da

Please sign in to comment.