diff --git a/README.md b/README.md index 56e2b61..5212b62 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,25 @@ # sparkles

- sparkles icon + sparkles icon

-[![Netlify Status](https://api.netlify.com/api/v1/badges/c0572dda-6712-4742-a980-3a40b0d42ec2/deploy-status)](https://app.netlify.com/sites/sprkls/deploys) +
+ + Netlify Status + +
+
+ + Project License + + + Latest Version + + + Latest Commit + +
[sparkles](https://sparkles.sploot.com) is a [Micropub](https://micropub.spec.indieweb.org/) client. It supports [IndieAuth](https://indieauth.net/) for login and expects a [micropub endpoint](https://indieweb.org/Micropub/Servers) to communicate with to publish posts. It supports basic micropub content types and you can also add movies you have watched. diff --git a/index.html b/index.html index 560a0fb..c8ef14e 100644 --- a/index.html +++ b/index.html @@ -43,6 +43,6 @@ - + diff --git a/package-lock.json b/package-lock.json index cf38d2b..6722758 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sparkles", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sparkles", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "dependencies": { "cheerio": "^1.0.0-rc.12", diff --git a/package.json b/package.json index 3a44486..e1c55dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sparkles", - "version": "0.1.0", + "version": "0.1.1", "description": "micropub client", "main": "./src/js/app.js", "scripts": { diff --git a/src/functions/token.js b/src/functions/token.js index aea4fa7..d1be7ac 100644 --- a/src/functions/token.js +++ b/src/functions/token.js @@ -5,8 +5,8 @@ exports.handler = async e => { // eslint-disable-next-line camelcase const { code, client_id, redirect_uri, token_endpoint, code_verifier } = e.queryStringParameters + // https://indieauth.spec.indieweb.org/#request const params = new URLSearchParams() - params.append('grant_type', 'authorization_code') params.append('code', code) params.append('client_id', client_id) diff --git a/src/js/Components/Footer.js b/src/js/Components/Footer.js index aa6f600..8569eda 100644 --- a/src/js/Components/Footer.js +++ b/src/js/Components/Footer.js @@ -7,6 +7,11 @@ const Footer = () => { let theme = Store.getSettings('theme') || 'light' document.documentElement.setAttribute('data-theme', theme) + let ui = Store.getSettings('ui') + if (ui) { + document.documentElement.setAttribute('data-ui', ui) + } + const toggleTheme = () => { theme = theme === 'light' ? 'dark' : 'light' Store.addToSettings({ theme }) diff --git a/src/js/Controllers/Proxy.js b/src/js/Controllers/Proxy.js index dfba67e..a976451 100644 --- a/src/js/Controllers/Proxy.js +++ b/src/js/Controllers/Proxy.js @@ -15,13 +15,10 @@ const Proxy = { }), validate: params => { const session = Store.getSession() - if (!session) { - throw new Error('session not found') - } + if (!session) throw new Error('session not found') const { code } = params - if (!code) { - throw new Error('missing "code"') - } + if (!code) throw new Error('missing "code"') + return m .request({ method: 'GET', @@ -38,12 +35,9 @@ const Proxy = { }, micropub: ({ method, params, body }) => { const session = Store.getSession() - if (!session) { - throw new Error('session not found') - } - if (!session.access_token) { - throw new Error('access_token not found') - } + if (!session) throw new Error('session not found') + if (!session.access_token) throw new Error('access_token not found') + return m .request({ method: method || 'GET', diff --git a/src/js/Models/Store.js b/src/js/Models/Store.js index 72e7104..879e70e 100644 --- a/src/js/Models/Store.js +++ b/src/js/Models/Store.js @@ -13,6 +13,7 @@ const Store = { getSession: prop => Store.get(Store.sessionKey, prop), setSession: data => Store.set(Store.sessionKey, data), addToSession: data => Store.add(Store.sessionKey, data), + clearSession: () => localStorage.removeItem(Store.sessionKey), getMe: () => Store.getSession('issuer') || Store.getSession('me'), // settingKey: '_sprk', @@ -43,7 +44,8 @@ const Store = { return Store.get(Store.cacheKey, prop) }, setCache: data => Store.set(Store.cacheKey, data), - addToCache: data => Store.add(Store.cacheKey, data) + addToCache: data => Store.add(Store.cacheKey, data), + clearCache: () => Store.setCache(Store.defaultCache) } export default Store diff --git a/src/js/Pages/CallbackPage.js b/src/js/Pages/CallbackPage.js index 3851e15..67f1991 100644 --- a/src/js/Pages/CallbackPage.js +++ b/src/js/Pages/CallbackPage.js @@ -6,10 +6,12 @@ import Store from '../Models/Store' const CallbackPage = { oninit: async () => { + // https://indieauth.spec.indieweb.org/#authorization-response const parameterList = new URLSearchParams(window.location.search) const params = { code: parameterList.get('code'), state: parameterList.get('state'), + // indieauth.com does not return `iss` iss: parameterList.get('iss') } @@ -18,8 +20,15 @@ const CallbackPage = { if (params.state != state) throw new Error('"state" value does not match') if (!params.code) throw new Error('missing "code" param') + // From the spec, this should be checked and fail + // to support legacy, skip this check for now /* eslint-disable camelcase */ + // const authorization_endpoint = Store.getSession('authorization_endpoint') + // if (params.iss != authorization_endpoint) throw new Error('"iss" does not match "authorization_endpoint"') + + // https://indieauth.spec.indieweb.org/#redeeming-the-authorization-code const { access_token, scope, token_type } = await Proxy.validate(params) + // https://indieauth.spec.indieweb.org/#access-token-response Store.addToSession({ access_token, scope, token_type }) /* eslint-enable camelcase */ m.route.set('/home') diff --git a/src/js/Pages/LoginPage.js b/src/js/Pages/LoginPage.js index 78e7276..87b2e7c 100644 --- a/src/js/Pages/LoginPage.js +++ b/src/js/Pages/LoginPage.js @@ -3,19 +3,23 @@ import m from 'mithril' import Alert from '../Components/Alert' import Proxy from '../Controllers/Proxy' import Store from '../Models/Store' +import { canonicalURL } from '../utils' import { generateRandomString, generateCodeChallenge } from '../utils/crypt' const Login = () => { let loading = false - let url = '' + let urlString = '' - const canSubmit = () => url && url !== '' + const canSubmit = () => urlString && urlString !== '' const onLogin = async e => { e.preventDefault() loading = true try { + const url = canonicalURL(urlString) + if (!url) throw new Error('could not convert to canonical URL') + const data = await Proxy.discover(url) /* eslint-disable camelcase */ @@ -50,7 +54,8 @@ const Login = () => { loading = false } - Store.clear() + Store.clearSession() + Store.clearCache() return { view: () => @@ -63,8 +68,8 @@ const Login = () => { m('input', { type: 'url', placeholder: 'https://', - oninput: e => url = e.target.value, - value: url + oninput: e => urlString = e.target.value, + value: urlString }), m('button', { type: 'submit', diff --git a/src/js/Pages/SettingsPage.js b/src/js/Pages/SettingsPage.js index a73297b..3d75ca9 100644 --- a/src/js/Pages/SettingsPage.js +++ b/src/js/Pages/SettingsPage.js @@ -24,6 +24,11 @@ const SettingsPage = () => { loadFetchedValues() + let ui = Store.getSettings('ui') + if (ui) { + document.documentElement.setAttribute('data-ui', ui) + } + const loadMicropubConfig = async () => { await fetchMicropubConfig(true) loadFetchedValues() @@ -34,6 +39,16 @@ const SettingsPage = () => { loadFetchedValues() } + const updateUI = e => { + ui = e && e.target && e.target.checked ? 'simple' : null + if (ui) { + document.documentElement.setAttribute('data-ui', 'simple') + } else { + document.documentElement.removeAttribute('data-ui') + } + Store.addToSettings({ ui }) + } + return { view: () => m('section.sp-content.text-center', [ @@ -75,7 +90,15 @@ const SettingsPage = () => { s.name ]) ])) - ] + ], + m('hr'), + m('li', m('h5', 'General Settings')), + m('li', [ + m('label', [ + 'simple ui', + m('input', { type: 'checkbox', onchange: updateUI, checked: ui === 'simple' }) + ]) + ]) ]) ]) ]) diff --git a/src/js/utils/index.js b/src/js/utils/index.js index 708b8c7..f6a5855 100644 --- a/src/js/utils/index.js +++ b/src/js/utils/index.js @@ -2,7 +2,19 @@ const currentTime = () => Math.floor(Date.now() / 1000) const formatDate = t => (new Date(t * 1000)).toLocaleDateString() +// https://indieauth.spec.indieweb.org/#url-canonicalization +const canonicalURL = urlString => { + let url + try { + url = new URL(urlString) + } catch (_) { + return null + } + return url && ['http:', 'https:'].includes(url.protocol) ? url.href : null +} + export { + canonicalURL, currentTime, formatDate } \ No newline at end of file diff --git a/src/scss/_constants.scss b/src/scss/_constants.scss index 4edf844..4d4dfc9 100644 --- a/src/scss/_constants.scss +++ b/src/scss/_constants.scss @@ -24,6 +24,8 @@ --sprk-disabled: var(--sprk-fade-color); --sprk-placeholder: var(--sprk-highlight-color); + + --sprk-shadow-size: .25rem; } [data-theme='dark'] { @@ -32,4 +34,8 @@ --sprk-fade-color: #666; --sprk-dim-color: #333; --sprk-highlight-color: #d3d3d3; -} \ No newline at end of file +} + +[data-ui='simple'] { + --sprk-shadow-size: 0; +} diff --git a/src/scss/_ui.scss b/src/scss/_ui.scss index 76ff850..c2bb93b 100644 --- a/src/scss/_ui.scss +++ b/src/scss/_ui.scss @@ -10,7 +10,7 @@ border: var(--sprk-border-size) solid var(--sprk-border-color); border-radius: .7em; background-color: var(--sprk-bg); - filter: drop-shadow(.25rem .25rem 0 var(--window-shadow)); + filter: drop-shadow(var(--sprk-shadow-size) var(--sprk-shadow-size) 0 var(--window-shadow)); .sp-box-header { // position: relative; @@ -94,6 +94,10 @@ textarea, input, select { } } +input[type="checkbox"] { + cursor: inherit; +} + textarea { resize: none; }