commit 82c515b2e913704466bf9a67deb4039931974f91 Author: Alex Thomassen Date: Sun Sep 11 21:08:59 2022 +0000 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8d28567 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# http://editorconfig.org +root = true + +[*] +end_of_line = lf +indent_style = space +indent_size = 4 +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/worker/.gitignore b/worker/.gitignore new file mode 100644 index 0000000..a8f1819 --- /dev/null +++ b/worker/.gitignore @@ -0,0 +1,2 @@ +node_modules +wrangler.toml \ No newline at end of file diff --git a/worker/index.js b/worker/index.js new file mode 100644 index 0000000..f7833fd --- /dev/null +++ b/worker/index.js @@ -0,0 +1,146 @@ +async function hashString(input) +{ + const data = new TextEncoder().encode(input); + const hash = await crypto.subtle.digest('SHA-256', data); + return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join(''); +} + +/** + * Refreshes Patreon OAuth token + * + * @param {Object} token + * @returns {Promise} + */ +async function refreshToken(token) +{ + const url = new URL('https://www.patreon.com/api/oauth2/token'); + url.searchParams.set('grant_type', 'refresh_token'); + url.searchParams.set('refresh_token', token.refresh_token); + url.searchParams.set('client_id', PATREON_CLIENT_ID); + url.searchParams.set('client_secret', PATREON_CLIENT_SECRET); + + const response = await fetch(url, { + method: 'POST', + }); + + return await response.json(); +} + +/** + * Returns the current Patreon OAuth access token. Will trigger `refreshToken()` if necessary. + * + * @returns {Promise} + */ +async function getToken() +{ + let token = await PATREON_CONFIG.get('credentials', { + type: 'json', + }); + + if (token.expires_at && token.expires_at > Date.now()) { + return token.access_token; + } + + token = await refreshToken(token); + token.expires_at = Date.now() + (token.expires_in * 1000); + + await PATREON_CONFIG.put('credentials', JSON.stringify(token)); + + return token.access_token; +} + +/** + * Fetches data from the Patreon API. + * Will cache responses for up to 15 minutes, based on final URL (with GET parameters). + * + * @param {String} path + * @param {Object} params + * @returns {Promise} + */ +async function fetchApi(path, params) +{ + if (path[0] !== '/') { + path = '/' + path; + } + + const url = new URL(`https://www.patreon.com/api/oauth2/v2${path}`); + + for (const key in params) { + url.searchParams.set(key, params[key]); + } + + const cacheKey = `CACHE_${await hashString(url.toString())}`; + const cache = await PATREON_CONFIG.get(cacheKey, { + type: 'json', + }); + + if (cache) { + return cache; + } + + const token = await getToken(); + const response = await fetch(url, { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + const data = await response.json(); + await PATREON_CONFIG.put(cacheKey, JSON.stringify(data), { + // Cache for 15 minutes + expirationTtl: 15 * 60, + }); + + return data; +} + +/** + * Get campaign goals + * + * @returns {Promise} + */ +async function getGoals() +{ + const goals = await fetchApi('campaigns', { + 'fields[goal]': 'amount_cents,completed_percentage,created_at,description,reached_at,title', + 'include': 'goals', + }); + + return goals; +} + +/** + * Helper function for returning a JSON response. + * + * @param {Object} input + * @param {Number} statusCode + * @returns {Response} + */ +function response(input, statusCode = 200) +{ + return new Response(JSON.stringify(input), { + headers: { + 'Content-Type': 'application/json', + }, + status: statusCode, + }); +} + +async function handleRequest(request) { + const url = new URL(request.url); + + if (url.pathname === '/goals') { + return response(await getGoals()); + } + + return response( + { + error: 'Not found', + }, + 404 + ); +} + +addEventListener('fetch', function(event) { + return event.respondWith(handleRequest(event.request)) +}); diff --git a/worker/package.json b/worker/package.json new file mode 100644 index 0000000..d33879b --- /dev/null +++ b/worker/package.json @@ -0,0 +1,17 @@ +{ + "private": true, + "name": "patreon-api-proxy", + "version": "1.0.0", + "description": "Cloudflare Worker that works as a basic API proxy, so we don't have to deal with refreshing tokens in WordPress", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "format": "prettier --write '**/*.{js,css,json,md}'" + }, + "author": "Alex Thomassen ", + "license": "MIT", + "devDependencies": { + "prettier": "^1.18.2" + } + } + \ No newline at end of file diff --git a/worker/wrangler.sample.toml b/worker/wrangler.sample.toml new file mode 100644 index 0000000..75925c0 --- /dev/null +++ b/worker/wrangler.sample.toml @@ -0,0 +1,14 @@ +name = "patreon-api-proxy" +account_id = "" +workers_dev = true +compatibility_date = "2022-09-10" +main = "index.js" + +[vars] +PATREON_CLIENT_ID = "" +PATREON_CLIENT_SECRET = "" + +[[kv_namespaces]] + binding = "PATREON_TOKENS" + id = "" + preview_id = "" \ No newline at end of file