diff --git a/content/faq.md b/content/faq.md index 5798a4f..e525d7a 100644 --- a/content/faq.md +++ b/content/faq.md @@ -1,5 +1,3 @@ -# FAQ - [[toc]] ## What is Keyoxide? diff --git a/content/getting-started.md b/content/getting-started.md index a5463d6..9a4f4f1 100644 --- a/content/getting-started.md +++ b/content/getting-started.md @@ -1,5 +1,3 @@ -# Getting started - Glad you made it here :) Let's get you started on setting up your online identity and profile page. diff --git a/nodemon.json b/nodemon.json index 669432b..b00468a 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,3 +1,9 @@ { + "env": { + "NODE_ENV": "development", + "KX_HIGHLIGHTS_1_NAME": "Yarmo Mackenbach", + "KX_HIGHLIGHTS_1_DESCRIPTION": "Admin and developer of keyoxide.org", + "KX_HIGHLIGHTS_1_FINGERPRINT": "9f0048ac0b23301e1f77e994909f6bd6f80f485d" + }, "ext": "js,json,css,pug,md" } \ No newline at end of file diff --git a/routes/main.js b/routes/main.js index 642a064..b8f0955 100644 --- a/routes/main.js +++ b/routes/main.js @@ -43,13 +43,25 @@ if (process.env.ONION_URL) { } router.get('/', (req, res) => { - res.render('index'); + let highlights = [] + for (let index = 1; index < 4; index++) { + if (process.env[`KX_HIGHLIGHTS_${index}_NAME`] + && process.env[`KX_HIGHLIGHTS_${index}_FINGERPRINT`]) { + highlights.push({ + name: process.env[`KX_HIGHLIGHTS_${index}_NAME`], + description: process.env[`KX_HIGHLIGHTS_${index}_DESCRIPTION`], + fingerprint: process.env[`KX_HIGHLIGHTS_${index}_FINGERPRINT`], + }) + } + } + + res.render('index', {highlights: highlights}); }); router.get('/getting-started', (req, res) => { let rawContent = fs.readFileSync(`./content/getting-started.md`, "utf8"); const content = md.render(rawContent); - res.render(`basic`, { title: `Getting started - Keyoxide`, content: content }); + res.render(`long-form-content`, { title: `Getting started`, content: content }); }); router.get('/faq', (req, res) => { @@ -60,7 +72,7 @@ router.get('/faq', (req, res) => { let rawContent = fs.readFileSync(`./content/faq.md`, "utf8"); rawContent = rawContent.replace('${domain}', req.app.get('domain')); const content = mdAlt.render(rawContent); - res.render(`basic`, { title: `Frequently Asked Questions - Keyoxide`, content: content }); + res.render(`long-form-content`, { title: `Frequently Asked Questions`, content: content }); }); router.get('/guides', (req, res) => { @@ -69,12 +81,18 @@ router.get('/guides', (req, res) => { router.get('/guides/:guideId', (req, res) => { let env = {}; - let rawContent = fs.readFileSync(`./content/guides/${req.params.guideId}.md`, "utf8", (err, data) => { - if (err) throw err; - return data; - }); + let rawContent + try { + rawContent = fs.readFileSync(`./content/guides/${req.params.guideId}.md`, "utf8", (err, data) => { + if (err) throw err; + return data; + }); + } catch (error) { + res.render(`404`) + return + } const content = md.render(rawContent, env); - res.render(`basic`, { title: `${env.title} - Keyoxide`, content: content }); + res.render(`long-form-content`, { title: `${env.title}`, content: content }); }); module.exports = router; diff --git a/static/img/logo_circle.png b/static/img/logo_circle.png new file mode 100644 index 0000000..4f62467 Binary files /dev/null and b/static/img/logo_circle.png differ diff --git a/static/scripts.js b/static/scripts.js index 5e6a14c..727726c 100644 --- a/static/scripts.js +++ b/static/scripts.js @@ -167,76 +167,202 @@ async function encryptMessage(opts) { elBtn.setAttribute("disabled", "true"); }; -async function verifyProofs(opts) { - // Init - const elRes = document.body.querySelector("#result"); - let keyData, feedback = "", message, encrypted; +// async function verifyProofs(opts) { +// // Init +// const elRes = document.body.querySelector("#result"); +// let keyData, feedback = "", message, encrypted; - // Reset feedback - elRes.innerHTML = ""; - elRes.classList.remove('green'); - elRes.classList.remove('red'); +// // Reset feedback +// elRes.innerHTML = ""; +// elRes.classList.remove('green'); +// elRes.classList.remove('red'); - try { - // Get key data - keyData = await fetchKeys(opts); - } catch (e) { - console.error(e); - elRes.innerHTML = e; - elRes.classList.remove('green'); - elRes.classList.add('red'); - return; - } +// try { +// // Get key data +// keyData = await fetchKeys(opts); +// } catch (e) { +// console.error(e); +// elRes.innerHTML = e; +// elRes.classList.remove('green'); +// elRes.classList.add('red'); +// return; +// } - let notations = [], notationsRaw = []; - for (var i = 0; i < keyData.publicKey.users.length; i++) { - notationsRaw = notationsRaw.concat(keyData.publicKey.users[i].selfCertifications[0].notations); - } - notationsRaw.forEach((item, i) => { - if (item[0] == "proof@metacode.biz") { - notations.push(item[1]); - } - }); - notations = Array.from(new Set(notations)); // Deduplicate (ES6) +// let notations = [], notationsRaw = []; +// for (var i = 0; i < keyData.publicKey.users.length; i++) { +// notationsRaw = notationsRaw.concat(keyData.publicKey.users[i].selfCertifications[0].notations); +// } +// notationsRaw.forEach((item, i) => { +// if (item[0] == "proof@metacode.biz") { +// notations.push(item[1]); +// } +// }); +// notations = Array.from(new Set(notations)); // Deduplicate (ES6) - // Display feedback - elRes.innerHTML = "Verifying proofs…"; +// // Display feedback +// elRes.innerHTML = "Verifying proofs…"; - let notation, isVerified, verifications = []; - for (var i = 0; i < notations.length; i++) { - notation = notations[i]; - verifications.push(await verifyProof(notation, keyData.fingerprint)); - } +// let notation, isVerified, verifications = []; +// for (var i = 0; i < notations.length; i++) { +// notation = notations[i]; +// verifications.push(await verifyProof(notation, keyData.fingerprint)); +// } - // One-line sorting function (order verifications by type) - verifications = verifications.sort((a,b) => (a.type > b.type) ? 1 : ((b.type > a.type) ? -1 : 0)); +// // One-line sorting function (order verifications by type) +// verifications = verifications.sort((a,b) => (a.type > b.type) ? 1 : ((b.type > a.type) ? -1 : 0)); - // Generate feedback - feedback += `

`; - for (var i = 0; i < verifications.length; i++) { - if (verifications[i].type == null) { continue; } - feedback += `${verifications[i].type}: `; - feedback += `${verifications[i].display}`; - if (verifications[i].isVerified) { - feedback += `verified ✔`; - } else { - feedback += `unverified`; - } - feedback += `
`; - } - feedback += `

`; +// // Generate feedback +// feedback += `

`; +// for (var i = 0; i < verifications.length; i++) { +// if (verifications[i].type == null) { continue; } +// feedback += `${verifications[i].type}: `; +// feedback += `${verifications[i].display}`; +// if (verifications[i].isVerified) { +// feedback += `verified ✔`; +// } else { +// feedback += `unverified`; +// } +// feedback += `
`; +// } +// feedback += `

`; - // Display feedback - elRes.innerHTML = feedback; -} +// // Display feedback +// elRes.innerHTML = feedback; +// } async function displayProfile(opts) { + /// UTILITY FUNCTIONS + // Sort claims by name and filter for errors + const sortClaims = (claims) => { + claims = claims.filter((a) => (a && a.errors.length == 0 && a.serviceproviderData)); + claims = claims.sort((a,b) => (a.serviceproviderData.serviceprovider.name > b.serviceproviderData.serviceprovider.name) ? 1 : ((b.serviceproviderData.serviceprovider.name > a.serviceproviderData.serviceprovider.name) ? -1 : 0)); + return claims; + } + // Find the primary claims + const getPrimaryClaims = (primaryUserId, userIds, verifications) => { + let primaryClaims = null; + userIds.forEach((userId, i) => { + if (!primaryClaims && userId.userId && userId.userId.email === primaryUserId.email) { + primaryClaims = sortClaims(verifications[i]); + } + }); + return primaryClaims; + } + // Generate a HTML string for the profile header + const generateProfileHeaderHTML = (data) => { + if (!data) { + data = { + profileHide: true, + name: '', + fingerprint: '', + key: { + url: '', + mode: '', + server: '', + id: '', + }, + avatarURL: '/static/img/avatar_placeholder.png', + } + } + + // TODO Add support for custom HKP server + return ` +
+ + avatar + +
+
+

${data.name}

+

+ ${data.fingerprint} +

+
+ Encrypt message + Verify signature +
+
+ `; + } + // Generate a HTML string for each userId and associated claims + const generateProfileUserIdHTML = (userId, claims, opts) => { + // Init output + let output = ''; + + // Add claim header to output + output += `

${userId.email}${opts.isPrimary ? ' primary' : ''}

`; + + // Handle claims identical to primary + if ('isIdenticalToPrimary' in opts && opts.isIdenticalToPrimary) { + + output += ` +
+
+
+

Identical to primary claims

+
+
+
+ `; + return output; + } + + // Handle "no claims" + if (claims.length == 0) { + output += ` +
+
+
+

No claims associated

+
+
+
+ `; + return output; + } + + claims = sortClaims(claims); + + // Generate output for each claim + claims.forEach((claim, i) => { + const claimData = claim.serviceproviderData + if (!claimData.serviceprovider.name) { + return; + } + + output += ` +
+
+
+

${claimData.profile.display}

+
+ +
+
${claim.isVerified ? "✔" : "✕"}
+
+ `; + // if (claim.isVerified && claimData.profile.qr) { + // feedback += `${icon_qr}`; + // } + }) + + return output; + } + + /// MAIN + // Init variables let keyData, keyLink, sigVerification, sigKeyUri, fingerprint, feedback = "", verifications = []; let icon_qr = ''; // Reset the avatar - document.body.querySelector('#profileAvatar').style = 'display: none'; - document.body.querySelector('#profileAvatar').src = '/static/img/avatar_placeholder.png'; + document.body.querySelector('#profileHeader').src = generateProfileHeaderHTML(null) if (opts.mode == 'sig') { try { @@ -273,11 +399,13 @@ async function displayProfile(opts) { } } + // Get data of primary userId const userPrimary = await keyData.getPrimaryUser(); const userData = userPrimary.user.userId; const userName = userData.name ? userData.name : userData.email; const userMail = userData.email ? userData.email : null; + // TODO Get image from user attribute let imgUri = null; // Determine WKD or HKP link @@ -332,53 +460,22 @@ async function displayProfile(opts) { break; } - // Fill in various data - document.body.querySelector('#profileName').innerHTML = userName; - document.body.querySelector('#profileAvatar').style = ""; + // Generate profile header const profileHash = openpgp.util.str_to_hex(openpgp.util.Uint8Array_to_str(await openpgp.crypto.hash.md5(openpgp.util.str_to_Uint8Array(userData.email)))); - if (imgUri) { - document.body.querySelector('#profileAvatar').src = imgUri; - } else { - document.body.querySelector('#profileAvatar').src = `https://www.gravatar.com/avatar/${profileHash}?s=128&d=mm`; - } + document.body.querySelector('#profileHeader').innerHTML = generateProfileHeaderHTML({ + profileHide: false, + name: userName, + fingerprint: fingerprint, + key: { + url: keyLink, + mode: keyUriMode, + server: keyUriServer, + id: keyUriId, + }, + avatarURL: imgUri ? imgUri : `https://www.gravatar.com/avatar/${profileHash}?s=128&d=mm` + }) document.title = `${userName} - Keyoxide`; - // Generate feedback - feedback += `
`; - feedback += `
`; - feedback += `
General information
`; - feedback += `
`; - - feedback += `
`; - feedback += `
fingerprint
`; - feedback += `
${fingerprint}`; - if (opts.mode == "hkp") { - feedback += `${icon_qr}`; - } - feedback += `
`; - - feedback += `
`; - feedback += `
`; - feedback += `
`; - feedback += `
Verifying proofs…
`; - feedback += `
`; - feedback += `
`; - feedback += `
`; - feedback += `
`; - feedback += `
Actions
`; - feedback += `
`; - feedback += `
`; - feedback += `
`; - feedback += `
Verify signature
`; - feedback += `
`; - feedback += `
`; - feedback += `
`; - feedback += `
Encrypt message
`; - feedback += `
`; - - // Display feedback - document.body.querySelector('#profileData').innerHTML = feedback; - try { if (sigVerification) { verifications = sigVerification.claims @@ -401,515 +498,434 @@ async function displayProfile(opts) { feedback = ""; if (opts.mode === 'sig') { - feedback += `
`; - feedback += `
`; - feedback += `
proofs
`; - feedback += `
`; + const claims = sortClaims(verifications); + feedback += generateProfileUserIdHTML(userData, claims, {isPrimary: false}); - verifications = verifications.filter((a) => (a && a.errors.length == 0 && a.serviceproviderData)) - verifications = verifications.sort((a,b) => (a.serviceproviderData.serviceprovider.name > b.serviceproviderData.serviceprovider.name) ? 1 : ((b.serviceproviderData.serviceprovider.name > a.serviceproviderData.serviceprovider.name) ? -1 : 0)); + // feedback += `
`; + // feedback += `
`; + // feedback += `
proofs
`; + // feedback += `
`; - verifications.forEach((claim, i) => { - const claimData = claim.serviceproviderData; - if (!claimData.serviceprovider.name) { - return; - } - feedback += `
`; - feedback += `
${capitalizeLetteredServices(claimData.serviceprovider.name)}
`; - feedback += `
`; - feedback += `${claimData.profile.display}`; - if (claim.isVerified) { - feedback += `verified ✔`; - } else { - feedback += `unverified`; - } - if (claim.isVerified && claimData.profile.qr) { - feedback += `${icon_qr}`; - } - feedback += `
`; - feedback += `
`; - }); + // verifications = verifications.filter((a) => (a && a.errors.length == 0 && a.serviceproviderData)) + // verifications = verifications.sort((a,b) => (a.serviceproviderData.serviceprovider.name > b.serviceproviderData.serviceprovider.name) ? 1 : ((b.serviceproviderData.serviceprovider.name > a.serviceproviderData.serviceprovider.name) ? -1 : 0)); + + // verifications.forEach((claim, i) => { + // const claimData = claim.serviceproviderData; + // if (!claimData.serviceprovider.name) { + // return; + // } + // feedback += `
`; + // feedback += `
${capitalizeLetteredServices(claimData.serviceprovider.name)}
`; + // feedback += `
`; + // feedback += `${claimData.profile.display}`; + // if (claim.isVerified) { + // feedback += `verified ✔`; + // } else { + // feedback += `unverified`; + // } + // if (claim.isVerified && claimData.profile.qr) { + // feedback += `${icon_qr}`; + // } + // feedback += `
`; + // feedback += `
`; + // }); } else { - let primaryClaims; + const primaryClaims = getPrimaryClaims(userData, keyData.users, verifications); + feedback += generateProfileUserIdHTML(userData, primaryClaims, {isPrimary: true}); - if (userMail) { - verifications.forEach((userId, i) => { - if (!keyData.users[i].userId) { - keyData.users[i].userId = { - email: 'email not specified' - } - } - - if (keyData.users[i].userId.email != userMail) { - return; - } - - feedback += `
`; - feedback += `
`; - // feedback += `
${keyData.users[i].userId.email} (primary)
`; - feedback += `
${keyData.users[i].userId.email} primary
`; - feedback += `
`; - - if (userId.length == 0) { - feedback += `
`; - feedback += `
`; - feedback += `
No claims associated
`; - feedback += `
`; - return; - } - - userId = userId.filter((a) => (a && a.errors.length == 0 && a.serviceproviderData)) - userId = userId.sort((a,b) => (a.serviceproviderData.serviceprovider.name > b.serviceproviderData.serviceprovider.name) ? 1 : ((b.serviceproviderData.serviceprovider.name > a.serviceproviderData.serviceprovider.name) ? -1 : 0)); - - primaryClaims = userId; - - userId.forEach((claim, i) => { - const claimData = claim.serviceproviderData; - if (!claimData.serviceprovider.name) { - return; - } - feedback += `
`; - feedback += `
${capitalizeLetteredServices(claimData.serviceprovider.name)}
`; - feedback += `
`; - feedback += `${claimData.profile.display}`; - if (claim.isVerified) { - feedback += `verified ✔`; - } else { - feedback += `unverified`; - } - if (claim.isVerified && claimData.profile.qr) { - feedback += `${icon_qr}`; - } - feedback += `
`; - feedback += `
`; - }); - }); - } - - verifications.forEach((userId, i) => { - if (userMail && keyData.users[i].userId.email == userMail) { + keyData.users.forEach((user, i) => { + if (!user.userId || userData.email && user.userId && user.userId.email === userData.email) { return; } - feedback += `
`; - feedback += `
`; - feedback += `
${keyData.users[i].userId.email}
`; - feedback += `
`; - - if (userId.length === 0) { - feedback += `
`; - feedback += `
`; - feedback += `
No claims associated
`; - feedback += `
`; - return; + const claims = sortClaims(verifications[i]) + const opts = { + isPrimary: false, + isIdenticaltoPrimary: primaryClaims && primaryClaims.toString() === claims.toString() } - userId = userId.filter((a) => (a && a.errors.length == 0 && a.serviceproviderData)) - userId = userId.sort((a,b) => (a.serviceproviderData.serviceprovider.name > b.serviceproviderData.serviceprovider.name) ? 1 : ((b.serviceproviderData.serviceprovider.name > a.serviceproviderData.serviceprovider.name) ? -1 : 0)); - - if (primaryClaims && primaryClaims.toString() == userId.toString()) { - feedback += `
`; - feedback += `
`; - feedback += `
Identical to primary
`; - feedback += `
`; - return; - } - - userId.forEach((claim, i) => { - const claimData = claim.serviceproviderData; - if (!claimData.serviceprovider.name) { - return; - } - feedback += `
`; - feedback += `
${capitalizeLetteredServices(claimData.serviceprovider.name)}
`; - feedback += `
`; - feedback += `${claimData.profile.display}`; - if (claim.isVerified) { - feedback += `verified ✔`; - } else { - feedback += `unverified`; - } - if (claim.isVerified && claimData.profile.qr) { - feedback += `${icon_qr}`; - } - feedback += `
`; - feedback += `
`; - }); - }); + feedback += generateProfileUserIdHTML(user.userId, claims, opts); + }) } + feedback += ` + `; + // Display feedback document.body.querySelector('#profileProofs').innerHTML = feedback; } -async function verifyProof(url, fingerprint) { - // Init - let reVerify, match, output = {url: url, type: null, proofUrl: url, proofUrlFetch: null, isVerified: false, display: null, qr: null}; +// async function verifyProof(url, fingerprint) { +// // Init +// let reVerify, match, output = {url: url, type: null, proofUrl: url, proofUrlFetch: null, isVerified: false, display: null, qr: null}; - try { - // DNS - if (/^dns:/.test(url)) { - output.type = "domain"; - output.display = url.replace(/dns:/, '').replace(/\?type=TXT/, ''); - output.proofUrl = `https://dns.shivering-isles.com/dns-query?name=${output.display}&type=TXT`; - output.proofUrlFetch = output.proofUrl; - output.url = `https://${output.display}`; +// try { +// // DNS +// if (/^dns:/.test(url)) { +// output.type = "domain"; +// output.display = url.replace(/dns:/, '').replace(/\?type=TXT/, ''); +// output.proofUrl = `https://dns.shivering-isles.com/dns-query?name=${output.display}&type=TXT`; +// output.proofUrlFetch = output.proofUrl; +// output.url = `https://${output.display}`; - try { - response = await fetch(output.proofUrlFetch, { - headers: { - Accept: 'application/json' - }, - credentials: 'omit' - }); - if (!response.ok) { - throw new Error('Response failed: ' + response.status); - } - json = await response.json(); - reVerify = new RegExp(`openpgp4fpr:${fingerprint}`, 'i'); - json.Answer.forEach((item, i) => { - if (reVerify.test(item.data)) { - output.isVerified = true; - } - }); - } catch (e) { - } finally { - return output; - } - } - // XMPP - if (/^xmpp:/.test(url)) { - output.type = "xmpp"; - match = url.match(/xmpp:([a-zA-Z0-9\.\-\_]*)@([a-zA-Z0-9\.\-\_]*)(?:\?(.*))?/); - output.display = `${match[1]}@${match[2]}`; - output.proofUrl = `https://PLACEHOLDER__XMPP_VCARD_SERVER_DOMAIN/api/vcard/${output.display}/DESC`; - output.qr = url; +// try { +// response = await fetch(output.proofUrlFetch, { +// headers: { +// Accept: 'application/json' +// }, +// credentials: 'omit' +// }); +// if (!response.ok) { +// throw new Error('Response failed: ' + response.status); +// } +// json = await response.json(); +// reVerify = new RegExp(`openpgp4fpr:${fingerprint}`, 'i'); +// json.Answer.forEach((item, i) => { +// if (reVerify.test(item.data)) { +// output.isVerified = true; +// } +// }); +// } catch (e) { +// } finally { +// return output; +// } +// } +// // XMPP +// if (/^xmpp:/.test(url)) { +// output.type = "xmpp"; +// match = url.match(/xmpp:([a-zA-Z0-9\.\-\_]*)@([a-zA-Z0-9\.\-\_]*)(?:\?(.*))?/); +// output.display = `${match[1]}@${match[2]}`; +// output.proofUrl = `https://PLACEHOLDER__XMPP_VCARD_SERVER_DOMAIN/api/vcard/${output.display}/DESC`; +// output.qr = url; - try { - response = await fetchWithTimeout(output.proofUrl); - if (!response.ok) { - throw new Error('Response failed: ' + response.status); - } - json = await response.json(); - reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`, 'i'); - if (reVerify.test(json)) { - output.isVerified = true; - } - } catch (e) { - } finally { - return output; - } - } - // Twitter - if (/^https:\/\/twitter.com/.test(url)) { - output.type = "twitter"; - match = url.match(/https:\/\/twitter\.com\/(.*)\/status\/([0-9]*)(?:\?.*)?/); - output.display = `@${match[1]}`; - output.url = `https://twitter.com/${match[1]}`; - output.proofUrlFetch = `/server/verify/twitter -?tweetId=${encodeURIComponent(match[2])} -&account=${encodeURIComponent(match[1])} -&fingerprint=${fingerprint}`; - try { - response = await fetch(output.proofUrlFetch); - if (!response.ok) { - throw new Error('Response failed: ' + response.status); - } - json = await response.json(); - output.isVerified = json.isVerified; - } catch (e) { - } finally { - return output; - } - } - // HN - if (/^https:\/\/news.ycombinator.com/.test(url)) { - output.type = "hackernews"; - match = url.match(/https:\/\/news.ycombinator.com\/user\?id=(.*)/); - output.display = match[1]; - output.proofUrl = `https://hacker-news.firebaseio.com/v0/user/${match[1]}.json`; - output.proofUrlFetch = output.proofUrl; - try { - response = await fetch(output.proofUrlFetch, { - headers: { - Accept: 'application/json' - }, - credentials: 'omit' - }); - if (!response.ok) { - throw new Error('Response failed: ' + response.status); - } - json = await response.json(); - reVerify = new RegExp(`openpgp4fpr:${fingerprint}`, 'i'); - if (reVerify.test(json.about)) { - output.isVerified = true; - } - } catch (e) { - } +// try { +// response = await fetchWithTimeout(output.proofUrl); +// if (!response.ok) { +// throw new Error('Response failed: ' + response.status); +// } +// json = await response.json(); +// reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`, 'i'); +// if (reVerify.test(json)) { +// output.isVerified = true; +// } +// } catch (e) { +// } finally { +// return output; +// } +// } +// // Twitter +// if (/^https:\/\/twitter.com/.test(url)) { +// output.type = "twitter"; +// match = url.match(/https:\/\/twitter\.com\/(.*)\/status\/([0-9]*)(?:\?.*)?/); +// output.display = `@${match[1]}`; +// output.url = `https://twitter.com/${match[1]}`; +// output.proofUrlFetch = `/server/verify/twitter +// ?tweetId=${encodeURIComponent(match[2])} +// &account=${encodeURIComponent(match[1])} +// &fingerprint=${fingerprint}`; +// try { +// response = await fetch(output.proofUrlFetch); +// if (!response.ok) { +// throw new Error('Response failed: ' + response.status); +// } +// json = await response.json(); +// output.isVerified = json.isVerified; +// } catch (e) { +// } finally { +// return output; +// } +// } +// // HN +// if (/^https:\/\/news.ycombinator.com/.test(url)) { +// output.type = "hackernews"; +// match = url.match(/https:\/\/news.ycombinator.com\/user\?id=(.*)/); +// output.display = match[1]; +// output.proofUrl = `https://hacker-news.firebaseio.com/v0/user/${match[1]}.json`; +// output.proofUrlFetch = output.proofUrl; +// try { +// response = await fetch(output.proofUrlFetch, { +// headers: { +// Accept: 'application/json' +// }, +// credentials: 'omit' +// }); +// if (!response.ok) { +// throw new Error('Response failed: ' + response.status); +// } +// json = await response.json(); +// reVerify = new RegExp(`openpgp4fpr:${fingerprint}`, 'i'); +// if (reVerify.test(json.about)) { +// output.isVerified = true; +// } +// } catch (e) { +// } - if (!output.isVerified) { - output.proofUrlFetch = `/server/verify/proxy -?url=${encodeURIComponent(output.proofUrl)} -&fingerprint=${fingerprint} -&checkRelation=contains -&checkPath=about -&checkClaimFormat=message`; - try { - response = await fetch(output.proofUrlFetch); - if (!response.ok) { - throw new Error('Response failed: ' + response.status); - } - json = await response.json(); - output.isVerified = json.verified; - } catch (e) { - } - } - return output; - } - // dev.to - if (/^https:\/\/dev\.to\//.test(url)) { - output.type = "dev.to"; - match = url.match(/https:\/\/dev\.to\/(.*)\/(.*)/); - output.display = match[1]; - output.url = `https://dev.to/${match[1]}`; - output.proofUrlFetch = `https://dev.to/api/articles/${match[1]}/${match[2]}`; - try { - response = await fetch(output.proofUrlFetch); - if (!response.ok) { - throw new Error('Response failed: ' + response.status); - } - json = await response.json(); - reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`, 'i'); - if (reVerify.test(json.body_markdown)) { - output.isVerified = true; - } - } catch (e) { - } finally { - return output; - } - } - // Reddit - if (/^https:\/\/(?:www\.)?reddit\.com\/user/.test(url)) { - output.type = "reddit"; - match = url.match(/https:\/\/(?:www\.)?reddit\.com\/user\/(.*)\/comments\/(.*)\/([^/]*)/); - output.display = match[1]; - output.url = `https://www.reddit.com/user/${match[1]}`; - output.proofUrl = `https://www.reddit.com/user/${match[1]}/comments/${match[2]}.json`; - output.proofUrlFetch = `/server/verify/proxy -?url=${encodeURIComponent(output.proofUrl)} -&fingerprint=${fingerprint} -&checkRelation=contains -&checkPath=data,children,data,selftext -&checkClaimFormat=message`; - try { - response = await fetch(output.proofUrlFetch); - if (!response.ok) { - throw new Error('Response failed: ' + response.status); - } - json = await response.json(); - output.isVerified = json.isVerified; - } catch (e) { - } finally { - return output; - } - } - // Gitea - if (/\/gitea_proof$/.test(url)) { - output.type = "gitea"; - match = url.match(/https:\/\/(.*)\/(.*)\/gitea_proof/); - output.display = `${match[2]}@${match[1]}`; - output.url = `https://${match[1]}/${match[2]}`; - output.proofUrl = `https://${match[1]}/api/v1/repos/${match[2]}/gitea_proof`; - output.proofUrlFetch = `/server/verify/proxy -?url=${encodeURIComponent(output.proofUrl)} -&fingerprint=${fingerprint} -&checkRelation=eq -&checkPath=description -&checkClaimFormat=message`; - output.proofUrl = url; // Actually set the proof URL to something user-friendly - try { - response = await fetch(output.proofUrlFetch); - if (!response.ok) { - throw new Error('Response failed: ' + response.status); - } - json = await response.json(); - output.isVerified = json.isVerified; - } catch (e) { - } finally { - return output; - } - } - // Github - if (/^https:\/\/gist.github.com/.test(url)) { - output.type = "github"; - match = url.match(/https:\/\/gist.github.com\/(.*)\/(.*)/); - output.display = match[1]; - output.url = `https://github.com/${match[1]}`; - output.proofUrlFetch = `https://api.github.com/gists/${match[2]}`; - try { - response = await fetch(output.proofUrlFetch, { - headers: { - Accept: 'application/json' - }, - credentials: 'omit' - }); - if (!response.ok) { - throw new Error('Response failed: ' + response.status); - } - json = await response.json(); - reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`, 'i'); - if (reVerify.test(json.files["openpgp.md"].content)) { - output.isVerified = true; - } - } catch (e) { - } finally { - return output; - } - } - // GitLab - if (/\/gitlab_proof$/.test(url)) { - output.type = "gitlab"; - match = url.match(/https:\/\/(.*)\/(.*)\/gitlab_proof/); - output.display = `${match[2]}@${match[1]}`; - output.url = `https://${match[1]}/${match[2]}`; - output.proofUrlFetch = `https://${match[1]}/api/v4/users?username=${match[2]}`; - try { - const opts = { - headers: { - Accept: 'application/json' - }, - credentials: 'omit' - }; - // Get user - response = await fetch(output.proofUrlFetch, opts); - if (!response.ok) { - throw new Error('Response failed: ' + response.status); - } - json = await response.json(); - const user = json.find(user => user.username === match[2]); - if (!user) { - throw new Error('No user with username ' + match[2]); - } - // Get project - output.proofUrlFetch = `https://${match[1]}/api/v4/users/${user.id}/projects`; - response = await fetch(output.proofUrlFetch, opts); - if (!response.ok) { - throw new Error('Response failed: ' + response.status); - } - json = await response.json(); - const project = json.find(proj => proj.path === 'gitlab_proof'); - if (!project) { - throw new Error('No project at ' + url); - } - reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`, 'i'); - if (reVerify.test(project.description)) { - output.isVerified = true; - } - } catch (e) { - } finally { - return output; - } - } - // Lobsters - if (/^https:\/\/lobste.rs/.test(url)) { - output.type = "lobsters"; - match = url.match(/https:\/\/lobste.rs\/u\/(.*)/); - output.display = match[1]; - output.proofUrl = `https://lobste.rs/u/${match[1]}.json`; - output.proofUrlFetch = `/server/verify/proxy -?url=${encodeURIComponent(output.proofUrl)} -&fingerprint=${fingerprint} -&checkRelation=contains -&checkPath=about -&checkClaimFormat=message`; - try { - response = await fetch(output.proofUrlFetch); - if (!response.ok) { - throw new Error('Response failed: ' + response.status); - } - json = await response.json(); - output.isVerified = json.isVerified; - } catch (e) { - } finally { - return output; - } - } - // Catchall - // Fediverse - try { - response = await fetch(url, { - headers: { - Accept: 'application/json' - }, - credentials: 'omit' - }); - if (!response.ok) { - throw new Error('Response failed: ' + response.status); - } - json = await response.json(); - if ('attachment' in json) { - match = url.match(/https:\/\/(.*)\/@(.*)/); - json.attachment.forEach((item, i) => { - reVerify = new RegExp(fingerprint, 'i'); - if (reVerify.test(item.value)) { - output.type = "fediverse"; - output.display = `@${json.preferredUsername}@${[match[1]]}`; - output.proofUrlFetch = json.url; - output.isVerified = true; - } - }); - } - if (!output.type && 'summary' in json) { - match = url.match(/https:\/\/(.*)\/users\/(.*)/); - reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`, 'i'); - if (reVerify.test(json.summary)) { - output.type = "fediverse"; - output.display = `@${json.preferredUsername}@${[match[1]]}`; - output.proofUrlFetch = json.url; - output.isVerified = true; - } - } - if (output.type) { - return output; - } - } catch (e) { - console.warn(e); - } - // Discourse - try { - match = url.match(/https:\/\/(.*)\/u\/(.*)/); - output.proofUrl = `${url}.json`; - output.proofUrlFetch = `/server/verify/proxy -?url=${encodeURIComponent(output.proofUrl)} -&fingerprint=${fingerprint} -&checkRelation=contains -&checkPath=user,bio_raw -&checkClaimFormat=message`; - try { - response = await fetch(output.proofUrlFetch); - if (!response.ok) { - throw new Error('Response failed: ' + response.status); - } - json = await response.json(); - if (json.isVerified) { - output.type = "discourse"; - output.display = `${match[2]}@${match[1]}`; - output.isVerified = json.isVerified; - return output; - } - } catch (e) { - console.warn(e); - } - } catch (e) { - console.warn(e); - } - } catch (e) { - console.warn(e); - } +// if (!output.isVerified) { +// output.proofUrlFetch = `/server/verify/proxy +// ?url=${encodeURIComponent(output.proofUrl)} +// &fingerprint=${fingerprint} +// &checkRelation=contains +// &checkPath=about +// &checkClaimFormat=message`; +// try { +// response = await fetch(output.proofUrlFetch); +// if (!response.ok) { +// throw new Error('Response failed: ' + response.status); +// } +// json = await response.json(); +// output.isVerified = json.verified; +// } catch (e) { +// } +// } +// return output; +// } +// // dev.to +// if (/^https:\/\/dev\.to\//.test(url)) { +// output.type = "dev.to"; +// match = url.match(/https:\/\/dev\.to\/(.*)\/(.*)/); +// output.display = match[1]; +// output.url = `https://dev.to/${match[1]}`; +// output.proofUrlFetch = `https://dev.to/api/articles/${match[1]}/${match[2]}`; +// try { +// response = await fetch(output.proofUrlFetch); +// if (!response.ok) { +// throw new Error('Response failed: ' + response.status); +// } +// json = await response.json(); +// reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`, 'i'); +// if (reVerify.test(json.body_markdown)) { +// output.isVerified = true; +// } +// } catch (e) { +// } finally { +// return output; +// } +// } +// // Reddit +// if (/^https:\/\/(?:www\.)?reddit\.com\/user/.test(url)) { +// output.type = "reddit"; +// match = url.match(/https:\/\/(?:www\.)?reddit\.com\/user\/(.*)\/comments\/(.*)\/([^/]*)/); +// output.display = match[1]; +// output.url = `https://www.reddit.com/user/${match[1]}`; +// output.proofUrl = `https://www.reddit.com/user/${match[1]}/comments/${match[2]}.json`; +// output.proofUrlFetch = `/server/verify/proxy +// ?url=${encodeURIComponent(output.proofUrl)} +// &fingerprint=${fingerprint} +// &checkRelation=contains +// &checkPath=data,children,data,selftext +// &checkClaimFormat=message`; +// try { +// response = await fetch(output.proofUrlFetch); +// if (!response.ok) { +// throw new Error('Response failed: ' + response.status); +// } +// json = await response.json(); +// output.isVerified = json.isVerified; +// } catch (e) { +// } finally { +// return output; +// } +// } +// // Gitea +// if (/\/gitea_proof$/.test(url)) { +// output.type = "gitea"; +// match = url.match(/https:\/\/(.*)\/(.*)\/gitea_proof/); +// output.display = `${match[2]}@${match[1]}`; +// output.url = `https://${match[1]}/${match[2]}`; +// output.proofUrl = `https://${match[1]}/api/v1/repos/${match[2]}/gitea_proof`; +// output.proofUrlFetch = `/server/verify/proxy +// ?url=${encodeURIComponent(output.proofUrl)} +// &fingerprint=${fingerprint} +// &checkRelation=eq +// &checkPath=description +// &checkClaimFormat=message`; +// output.proofUrl = url; // Actually set the proof URL to something user-friendly +// try { +// response = await fetch(output.proofUrlFetch); +// if (!response.ok) { +// throw new Error('Response failed: ' + response.status); +// } +// json = await response.json(); +// output.isVerified = json.isVerified; +// } catch (e) { +// } finally { +// return output; +// } +// } +// // Github +// if (/^https:\/\/gist.github.com/.test(url)) { +// output.type = "github"; +// match = url.match(/https:\/\/gist.github.com\/(.*)\/(.*)/); +// output.display = match[1]; +// output.url = `https://github.com/${match[1]}`; +// output.proofUrlFetch = `https://api.github.com/gists/${match[2]}`; +// try { +// response = await fetch(output.proofUrlFetch, { +// headers: { +// Accept: 'application/json' +// }, +// credentials: 'omit' +// }); +// if (!response.ok) { +// throw new Error('Response failed: ' + response.status); +// } +// json = await response.json(); +// reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`, 'i'); +// if (reVerify.test(json.files["openpgp.md"].content)) { +// output.isVerified = true; +// } +// } catch (e) { +// } finally { +// return output; +// } +// } +// // GitLab +// if (/\/gitlab_proof$/.test(url)) { +// output.type = "gitlab"; +// match = url.match(/https:\/\/(.*)\/(.*)\/gitlab_proof/); +// output.display = `${match[2]}@${match[1]}`; +// output.url = `https://${match[1]}/${match[2]}`; +// output.proofUrlFetch = `https://${match[1]}/api/v4/users?username=${match[2]}`; +// try { +// const opts = { +// headers: { +// Accept: 'application/json' +// }, +// credentials: 'omit' +// }; +// // Get user +// response = await fetch(output.proofUrlFetch, opts); +// if (!response.ok) { +// throw new Error('Response failed: ' + response.status); +// } +// json = await response.json(); +// const user = json.find(user => user.username === match[2]); +// if (!user) { +// throw new Error('No user with username ' + match[2]); +// } +// // Get project +// output.proofUrlFetch = `https://${match[1]}/api/v4/users/${user.id}/projects`; +// response = await fetch(output.proofUrlFetch, opts); +// if (!response.ok) { +// throw new Error('Response failed: ' + response.status); +// } +// json = await response.json(); +// const project = json.find(proj => proj.path === 'gitlab_proof'); +// if (!project) { +// throw new Error('No project at ' + url); +// } +// reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`, 'i'); +// if (reVerify.test(project.description)) { +// output.isVerified = true; +// } +// } catch (e) { +// } finally { +// return output; +// } +// } +// // Lobsters +// if (/^https:\/\/lobste.rs/.test(url)) { +// output.type = "lobsters"; +// match = url.match(/https:\/\/lobste.rs\/u\/(.*)/); +// output.display = match[1]; +// output.proofUrl = `https://lobste.rs/u/${match[1]}.json`; +// output.proofUrlFetch = `/server/verify/proxy +// ?url=${encodeURIComponent(output.proofUrl)} +// &fingerprint=${fingerprint} +// &checkRelation=contains +// &checkPath=about +// &checkClaimFormat=message`; +// try { +// response = await fetch(output.proofUrlFetch); +// if (!response.ok) { +// throw new Error('Response failed: ' + response.status); +// } +// json = await response.json(); +// output.isVerified = json.isVerified; +// } catch (e) { +// } finally { +// return output; +// } +// } +// // Catchall +// // Fediverse +// try { +// response = await fetch(url, { +// headers: { +// Accept: 'application/json' +// }, +// credentials: 'omit' +// }); +// if (!response.ok) { +// throw new Error('Response failed: ' + response.status); +// } +// json = await response.json(); +// if ('attachment' in json) { +// match = url.match(/https:\/\/(.*)\/@(.*)/); +// json.attachment.forEach((item, i) => { +// reVerify = new RegExp(fingerprint, 'i'); +// if (reVerify.test(item.value)) { +// output.type = "fediverse"; +// output.display = `@${json.preferredUsername}@${[match[1]]}`; +// output.proofUrlFetch = json.url; +// output.isVerified = true; +// } +// }); +// } +// if (!output.type && 'summary' in json) { +// match = url.match(/https:\/\/(.*)\/users\/(.*)/); +// reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`, 'i'); +// if (reVerify.test(json.summary)) { +// output.type = "fediverse"; +// output.display = `@${json.preferredUsername}@${[match[1]]}`; +// output.proofUrlFetch = json.url; +// output.isVerified = true; +// } +// } +// if (output.type) { +// return output; +// } +// } catch (e) { +// console.warn(e); +// } +// // Discourse +// try { +// match = url.match(/https:\/\/(.*)\/u\/(.*)/); +// output.proofUrl = `${url}.json`; +// output.proofUrlFetch = `/server/verify/proxy +// ?url=${encodeURIComponent(output.proofUrl)} +// &fingerprint=${fingerprint} +// &checkRelation=contains +// &checkPath=user,bio_raw +// &checkClaimFormat=message`; +// try { +// response = await fetch(output.proofUrlFetch); +// if (!response.ok) { +// throw new Error('Response failed: ' + response.status); +// } +// json = await response.json(); +// if (json.isVerified) { +// output.type = "discourse"; +// output.display = `${match[2]}@${match[1]}`; +// output.isVerified = json.isVerified; +// return output; +// } +// } catch (e) { +// console.warn(e); +// } +// } catch (e) { +// console.warn(e); +// } +// } catch (e) { +// console.warn(e); +// } - // Return output without confirmed proof - return output; -} +// // Return output without confirmed proof +// return output; +// } async function fetchKeys(opts) { // Init @@ -1410,7 +1426,7 @@ if (elUtilProfileURL) { } function capitalizeLetteredServices(serviceName) { - var servName = serviceName.toLowerCase(); + const servName = serviceName.toLowerCase(); if (servName === 'dns' || servName === 'xmpp') { return servName.toUpperCase(); } diff --git a/static/styles-old.css b/static/styles-old.css new file mode 100644 index 0000000..a08e984 --- /dev/null +++ b/static/styles-old.css @@ -0,0 +1,439 @@ +/* +Copyright (C) 2021 Yarmo Mackenbach + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU Affero General Public License as published by the Free +Software Foundation, either version 3 of the License, or (at your option) +any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +details. + +You should have received a copy of the GNU Affero General Public License along +with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer network, +you should also make sure that it provides a way for users to get its source. +For example, if your program is a web application, its interface could display +a "Source" link that leads users to an archive of the code. There are many +ways you could offer source, and different solutions will be better for different +programs; see section 13 for the specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. For +more information on this, and how to apply and follow the GNU AGPL, see . +*/ +* { + box-sizing: border-box; +} +body { + margin: 0; + color: #444; + font-family: sans-serif; + background-color: #9dd3f0; + background-image: url('/static/img/background.svg'); + background-repeat: repeat; + background-size: 512px; + background-position: -16px -16px; +} +header { + height: 64px; + padding: 8px; + margin: 0 0 48px; + font-size: 1.1em; + background-color: #fff; + box-shadow: 0 8px 16px rgba(0,0,0,0.15); +} +header .container { + display: flex; + align-items: center; + height: 100%; +} +header a.logo { + display: inline-block; + height: 100%; +} +header .logo img { + height: 100%; + margin: 0 1em 0 0; +} +nav a { + margin-left: 12px; +} +footer { + color: #777; + margin: 64px 0; + padding: 0 32px; + font-size: 0.9em; + overflow-wrap: break-word; +} +footer a { + color: #777; +} +.container { + /*max-width: 720px;*/ + max-width: 770px; + width: 100%; + margin: 0 auto; +} +.content { + padding: 16px 32px 32px; + background-color: #fff; + border-radius: 8px; + box-shadow: 0 8px 16px rgba(0,0,0,0.15); +} +.spacer { + flex: 1; +} +.flex-column-container { + display: flex; + flex-wrap: wrap; +} +.flex-column { + flex: 1 0 250px; +} +.bigBtn { + display: inline-block; + margin-bottom: 12px; + padding: 6px 12px; + color: #fff; + font-size: 1.1em; + text-transform: uppercase; + text-decoration: none; + background: #3f9acc; + background: linear-gradient(0deg, #3892c2 0%, #6abae5 100%); + border: 0; + border-radius: 8px; + cursor: pointer; +} +.bigBtn:hover { + color: #fff; + background-color: #72bde6; + background: linear-gradient(0deg, #4da4d2 0%, #82c5ea 100%); +} +.fancyBtn { + display: inline-block; + margin-bottom: 12px; + padding: 8px 24px; + color: #fff; + font-size: 1.1em; + text-transform: uppercase; + text-decoration: none; + font-weight: bold; + background: #8b76f2; + /* background: linear-gradient(90deg, #8b76f2 0%, #6abae5 100%); */ + background: linear-gradient(90deg, #6957c4 0%, #43afea 100%); + border: 0; + border-radius: 64px; + cursor: pointer; +} +.fancyBtn:hover { + color: #fff; + background-color: #a595f4; + /* background: linear-gradient(90deg, #a595f4 0%, #93d9ff 100%); */ + /* background: linear-gradient(90deg, #6957c4 0%, #43afea 100%); */ + background: linear-gradient(90deg, #8b76f2 0%, #6abae5 100%); +} +.full-width { + width: 100% !important; +} + +h1 { + margin: 0 0 24px 0; + color: #222; + /* background-color: #9dd3f0; */ + background-color: #fff; + text-align: center; + cursor: default; +} +h1::after { + content: ""; + display: block; + width: 100%; + height: 4px; + margin-top: 12px; + background: linear-gradient(90deg, #8b76f2 0%, #6abae5 100%); + border-radius: 2px; +} +h2, h3 { + margin-top: 32px; + color: #222; + cursor: default; +} +h2 { + padding-right: 32px; +} +h2::after { + content: ""; + display: block; + width: 100%; + height: 2px; + /* background: linear-gradient(90deg, #8b76f2 0%, #3892c2 50%, #6abae5 100%); */ + background: linear-gradient(90deg, #3892c2 0%, #6abae5 100%); + border-radius: 1px; +} +p { + line-height: 1.4em; + font-size: 1.1em; +} +a { + color: #3f9acc; +} +a:hover { + color: #6957c4; +} +a.bigBtn { + margin-right: 8px; +} +a.header-anchor { + text-decoration: none; + opacity: 0.5; +} +a.header-anchor:hover { + opacity: 1; +} +ul { + list-style: "- "; +} +pre { + white-space: pre-wrap; +} +code { + padding: 2px; + background-color: #eee; + border: solid 1px #ddd; + user-select: all; +} +pre code { + display: block; + padding: 8px; + word-break: break-word; + /* word-break: break-all; */ +} +textarea { + width: 100%; + height: 128px; + resize: vertical; + font-size: 0.9rem; +} +input[type="text"] { + margin: 0 12px 12px 0; + width: 45%; + font-size: 0.9rem; +} +input[type="radio"] { + vertical-align: sub; +} +input[type="submit"] { + width: 100%; +} +input[type="submit"][disabled="true"] { + cursor: default; + pointer-events: none; + opacity: 0.3; +} +select { + margin: 0 0 16px 0; +} +.green, a.proofUrl.proofUrl--verified { + color: #499539; +} +.red { + color: red; +} +.label { + display: inline-block; + margin: 0 0 8px; +} +.modes { + display: none; +} +.modes.modes--visible { + display: block; +} + +.container--profile { + margin-top: 64px; +} +.container--profile .content { + padding-top: 32px; + font-size: 1.2em; +} +.container--profile footer { + text-align: center; +} + +.guides { + display: flex; + flex-wrap: wrap; +} +.guides__section { + flex: 1 0 250px; +} +.guides__section a { + line-height: 1.8em; +} + +#profileHeader { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 32px; + background-color: #c4e3f657; + padding: 15px; + border-radius: 15px; +} +#profileAvatar { + width: 100%; + max-width: 128px; + border-radius: 100%; + margin-right: 32px; +} +#profileName { + font-size: 1.6em; + font-weight: bold; + display: inline-block; + max-width: 100%; + white-space: wrap; + overflow: hidden; + text-overflow: ellipsis; +} + +#profileData { + background-color: #dceef957; + padding: 15px; + border-radius: 15px; +} + +.profileDataItem { + position: relative; + display: flex; + flex-wrap: wrap; + justify-content: space-between; +} +.profileDataItem--separator { + margin-top: 1em; + font-weight: bold; +} +.profileDataItem__label { + display: inline-block; + position: relative; + flex: 1; + min-height: 32px; + padding: 0 8px; + max-width: 20%; + font-size: 0.9em; + line-height: 1.6em; + color: #777; + text-align: right; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-transform: capitalize; +} +.profileDataItem__value { + display: inline-block; + flex: 1; + min-height: 32px; + max-width: 100%; + padding: 0 8px; +} +.profileDataItem__value a { + display: inline-block; + max-width: 100%; + margin-right: 8px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.profileDataItem__value small { + color: white; + background: #2178ff; + border-radius: 6px; + padding: 2.8px 5px; + font-size: 0.65em; + vertical-align: middle; +} + +a.proofUrl { + color: #777; +} +a.proofQR { + line-height: 0; + background-color: #499539; + border-radius: 2px; +} +a.proofQR:hover { + background-color: #6abb5a; +} + +#form-generate-signature-profile { + margin-bottom: 2em; + font-size: 0.9rem; +} + +#qrcode { + display: flex; + justify-content: center; + max-width: 100%; + width: auto !important; + height: auto !important; + margin: 32px auto; +} + +noscript { + display: block; + margin-bottom: 2rem; + padding: 8px; + background-color: #f0e68c; + text-align: center; +} +noscript p { + margin: 0; + font-size: 1rem; +} + +@media (max-width: 680px) { + #profileHeader { + flex-direction: column; + } + #profileAvatar { + margin: 0; + } + #profileName { + font-size: 1.2em; + } + .profileDataItem { + flex-direction: column; + margin-bottom: 8px; + } + .profileDataItem__label { + max-width: 100%; + min-height: 28px; + text-align: left; + } + .profileDataItem__value { + min-height: 28px; + } + .profileDataItem--noLabel .profileDataItem__label { + display: none; + } + + #profileData .profileDataItem__value a:first-child { + max-width: 85%; + } + + #profileData #profileProofs .profileDataItem__value a:first-child { + display: block; + } + + + input[type="text"] { + width: 100%; + } +} diff --git a/static/styles.css b/static/styles.css index a08e984..24308de 100644 --- a/static/styles.css +++ b/static/styles.css @@ -1,439 +1,494 @@ -/* -Copyright (C) 2021 Yarmo Mackenbach +:root { + --grey-500: hsl(0, 0%, 50%); + --grey-600: hsl(0, 0%, 40%); + --grey-700: hsl(0, 0%, 30%); + --grey-900: hsl(0, 0%, 10%); + --green-300: hsl(110, 45%, 70%); + --green-400: hsl(110, 45%, 60%); + --green-600: hsl(110, 45%, 40%); + --red-400: hsl(10, 60%, 60%); + --blue-500: hsl(201, 80%, 59%); + --blue-700: hsl(201, 90%, 30%); + --purple-50: hsl(250, 30%, 98%); + --purple-100: hsl(250, 48%, 95%); + --purple-200: hsl(250, 48%, 90%); + --purple-300: hsl(250, 48%, 85%); + --purple-400: hsl(250, 48%, 70%); + --purple-500: hsl(250, 48%, 65%); + --purple-600: hsl(250, 48%, 60%); + --purple-700: hsl(250, 48%, 55%); + --purple-900: hsl(250, 38%, 45%); + --yellow-100: hsl(56, 100%, 95%); + --yellow-200: hsl(56, 100%, 90%); + --yellow-500: hsl(56, 100%, 65%); +} -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU Affero General Public License as published by the Free -Software Foundation, either version 3 of the License, or (at your option) -any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -details. - -You should have received a copy of the GNU Affero General Public License along -with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - -If your software can interact with users remotely through a computer network, -you should also make sure that it provides a way for users to get its source. -For example, if your program is a web application, its interface could display -a "Source" link that leads users to an archive of the code. There are many -ways you could offer source, and different solutions will be better for different -programs; see section 13 for the specific requirements. - -You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. For -more information on this, and how to apply and follow the GNU AGPL, see . -*/ * { - box-sizing: border-box; + box-sizing: border-box; } body { - margin: 0; - color: #444; - font-family: sans-serif; - background-color: #9dd3f0; - background-image: url('/static/img/background.svg'); - background-repeat: repeat; - background-size: 512px; - background-position: -16px -16px; + display: flex; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 1.6rem 0 0; + line-height: 1.6rem; + font-family: sans-serif; + color: var(--grey-900); + /* background-color: var(--purple-100); */ } + header { - height: 64px; - padding: 8px; - margin: 0 0 48px; - font-size: 1.1em; - background-color: #fff; - box-shadow: 0 8px 16px rgba(0,0,0,0.15); -} -header .container { - display: flex; - align-items: center; - height: 100%; + display: flex; + justify-content: center; + align-items: center; + margin: 0 1.6rem 4.8rem; + padding: 0.8rem; + /* background-color: var(--purple-50); */ + border-radius: 16px; } header a.logo { - display: inline-block; - height: 100%; + width: 64px; + margin: 0 0.8rem; + font-size: 1.6rem; + text-transform: uppercase; + text-decoration: none; + color: var(--purple-700); } -header .logo img { - height: 100%; - margin: 0 1em 0 0; +header a.logo img { + width: 100%; } -nav a { - margin-left: 12px; +header .container { + display: flex; +} +header nav { + flex: 1; + display: flex; + align-items: center; +} +nav a.text { + font-size: 0.9em; + margin: 0; + padding: 0.5em 1em; + text-transform: uppercase; + text-decoration: none; + color: var(--purple-700); + border-radius: 4px; +} +nav a.text:hover { + color: #fff; + background-color: var(--purple-500); +} +main { + flex: 1; + margin: 0 1.6rem; } footer { - color: #777; - margin: 64px 0; - padding: 0 32px; - font-size: 0.9em; - overflow-wrap: break-word; -} -footer a { - color: #777; + margin: 4.8rem 0 0; + padding: 0 1.6rem 1.6rem; + background-color: var(--purple-900); + color: var(--purple-200); } + .container { - /*max-width: 720px;*/ - max-width: 770px; - width: 100%; - margin: 0 auto; + width: 100%; + max-width: 1440px; + margin: 0 auto; } -.content { - padding: 16px 32px 32px; - background-color: #fff; - border-radius: 8px; - box-shadow: 0 8px 16px rgba(0,0,0,0.15); +section.long_form { + width: 100%; + max-width: 720px; + margin: 0 auto; +} +section.profile, .demo { + max-width: 720px; + margin: 0 auto; +} +section.profile .card, .demo .card { + background-color: transparent; + border: 0; + box-shadow: 0 0 0 transparent; +} +section.profile p, .demo p { + /* margin: 0.8rem 0; */ + font-size: 1.2rem; +} +.demo { + margin: 9.6rem auto; + font-size: 1.6rem; +} + +.card { + margin: 0 0 1.6rem; + padding: 0 1.6rem; + background-color: #fff; + background-color: var(--purple-50); + border: 2px solid var(--purple-200); + box-shadow: 0 4px 12px var(--purple-100); +} +.card--profileHeader { + display: flex; + flex-wrap: wrap; + gap: 2rem; + /* text-align: center; */ +} +.card--profileHeader p, .card--profileHeader small { + margin: 0 0 1.2rem; +} +.card--small-profile { + display: flex; + flex-direction: column; + padding-top: 1rem; + text-align: center; + border-radius: 4px; +} +.card--small-profile-dummy { + opacity: 0.5; + border: 0; + box-shadow: unset; +} +.card--small-profile .name { + font-size: 1.4em; + /* font-weight: bold; */ +} +.card--small-profile p { + margin-top: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + +} +.card--small-profile a { + display: block; + padding: 0.4rem 0.8rem; + /* color: #fff; */ + text-decoration: none; + text-transform: uppercase; + background-color: #fff; + border: solid 1px var(--blue-700); + border-radius: 4px; +} +.card--small-profile a:hover { + /* background-color: rgba(255, 255, 255, 0.3); */ + background-color: var(--blue-700); + color: #fff; +} +#profileName { + font-size: 2rem; + color: var(--grey-700); +} +#profileURLFingerprint { + font-size: 1rem; + margin: 0 0 1.2rem; +} + +.hcards { + display: grid; + grid-gap: 1.6rem; + grid-template-columns: repeat(auto-fit, minmax(256px, 1fr)); + margin-bottom: 1.6rem; +} +.hcards--features { + text-align: center; +} +.hcards .card { + margin: 0; +} +.hcards--col-1-2, .hcards--col-2-1 { + grid-template-columns: repeat(auto-fit, minmax(256px, 1fr)); +} +.hcards--col-1-2 .card, .hcards--col-2-1 .card { + grid-column: 1 / 2; +} +@media screen and (min-width: 1024px) { + .hcards--max-3 { + grid-template-columns: 1fr 1fr 1fr; + } + .hcards--col-1-2, .hcards--col-2-1 { + grid-template-columns: repeat(3, 1fr); + } +} +@media screen and (min-width: 720px) { + .hcards--col-2-1 .card:nth-of-type(1) { + grid-column: 1 / -2; + } + .hcards--col-2-1 .card:nth-of-type(2) { + grid-column: -2 / -1; + } + .hcards--col-1-2 .card:nth-of-type(1) { + grid-column: 1 / 2; + } + .hcards--col-1-2 .card:nth-of-type(2) { + grid-column: 2 / -1; + } +} +@media screen and (max-width: 640px) { + header { + flex-direction: column; + } + header .spacer { + display: none; + } + header a.logo { + margin-bottom: 1.6rem; + order: -1; + } } .spacer { - flex: 1; + flex: 1; } -.flex-column-container { - display: flex; - flex-wrap: wrap; + +.warning { + padding: calc(0.8rem - 2px) 0.8rem; + background-color: var(--yellow-200); + border: solid 2px var(--yellow-500); } -.flex-column { - flex: 1 0 250px; +.warning p:first-of-type { + margin-top: 0; } -.bigBtn { - display: inline-block; - margin-bottom: 12px; - padding: 6px 12px; - color: #fff; - font-size: 1.1em; - text-transform: uppercase; - text-decoration: none; - background: #3f9acc; - background: linear-gradient(0deg, #3892c2 0%, #6abae5 100%); - border: 0; - border-radius: 8px; - cursor: pointer; +.warning p:last-of-type { + margin-bottom: 0; } -.bigBtn:hover { - color: #fff; - background-color: #72bde6; - background: linear-gradient(0deg, #4da4d2 0%, #82c5ea 100%); + +.claim { + display: flex; + align-items: center; + width: 100%; + margin: 0.8rem 0; + padding: 0.8rem 1.2rem; + background-color: var(--purple-100); + border-radius: 8px; } -.fancyBtn { - display: inline-block; - margin-bottom: 12px; - padding: 8px 24px; - color: #fff; - font-size: 1.1em; - text-transform: uppercase; - text-decoration: none; - font-weight: bold; - background: #8b76f2; - /* background: linear-gradient(90deg, #8b76f2 0%, #6abae5 100%); */ - background: linear-gradient(90deg, #6957c4 0%, #43afea 100%); - border: 0; - border-radius: 64px; - cursor: pointer; +.claim p { + margin: 0; } -.fancyBtn:hover { - color: #fff; - background-color: #a595f4; - /* background: linear-gradient(90deg, #a595f4 0%, #93d9ff 100%); */ - /* background: linear-gradient(90deg, #6957c4 0%, #43afea 100%); */ - background: linear-gradient(90deg, #8b76f2 0%, #6abae5 100%); +.claim .claim__main { + flex: 1; } -.full-width { - width: 100% !important; +.claim .claim__description p { + font-size: 1.4rem; + line-height: 2rem; +} +.claim .claim__links p, p.subtle-links { + display: flex; + flex-wrap: wrap; + font-size: 1rem; + color: var(--grey-700); +} +.claim .claim__links a, .claim .claim__links span, p.subtle-links a { + font-size: 1rem; + margin: 0 10px 0 0; + color: var(--grey-700); +} +/* p.subtle-links a:first-of-type { + margin: 0; +} */ +.claim .serviceProvider { + color: var(--grey-500); +} +.claim .claim__verification { + display: flex; + align-items: center; + justify-content: center; + min-width: 48px; + height: 48px; + border-radius: 100%; + background-color: var(--red-400); + color: #fff; + font-size: 2rem; + user-select: none; +} +.claim .claim__verification--true { + background-color: var(--green-400); +} +@media screen and (max-width: 640px) { + .claim .claim__description p { + font-size: 1.2rem; + } + .claim .claim__links a, p.subtle-links a { + font-size: 0.9rem; + } +} +@media screen and (max-width: 480px) { + .claim .claim__description p { + font-size: 1rem; + } + .claim .claim__verification { + min-width: 36px; + height: 36px; + font-size: 1.6rem; + } +} + +/* .demo { + text-align: center; + margin: 9.6rem 0; + font-size: 1.6rem; +} +.demo .claim { + margin: 1.6rem 0; +} */ + +.avatar { + display: inline-block; + min-width: 96px; + max-width: 128px; + /* margin-top: 1.6rem; */ + text-align: center; +} +.avatar img { + width: 100%; + border-radius: 24px; +} + +.buttons { + /* margin: 2.4rem 0; */ + margin: 1.2rem 0; +} +.buttons a { + display: inline-block; + margin-right: 0.8rem; + padding: 6px 24px; + background-color: #eaeaea; + color: #333; + text-transform: uppercase; + text-decoration: none; + border-radius: 32px; +} +.buttons a:hover { + background-color: #ccc; } h1 { - margin: 0 0 24px 0; - color: #222; - /* background-color: #9dd3f0; */ - background-color: #fff; - text-align: center; - cursor: default; -} -h1::after { - content: ""; - display: block; - width: 100%; - height: 4px; - margin-top: 12px; - background: linear-gradient(90deg, #8b76f2 0%, #6abae5 100%); - border-radius: 2px; -} -h2, h3 { - margin-top: 32px; - color: #222; - cursor: default; + font-size: 1.6em; + margin: 3.2rem 0 1.6rem; + font-weight: normal; + color: var(--purple-500); + cursor: default; } h2 { - padding-right: 32px; + font-size: 1.4em; + margin: 3.2rem 0 1.6rem; + font-weight: normal; + color: var(--purple-500); + cursor: default; } -h2::after { - content: ""; - display: block; - width: 100%; - height: 2px; - /* background: linear-gradient(90deg, #8b76f2 0%, #3892c2 50%, #6abae5 100%); */ - background: linear-gradient(90deg, #3892c2 0%, #6abae5 100%); - border-radius: 1px; +h2 small { + margin-left: 0.8rem; + padding: 3px 6px; + background-color: var(--purple-400); + color: #fff; + border-radius: 4px; +} +h3 { + margin: 1.6rem 0; + font-size: 1.3em; + line-height: 1.6rem; + color: var(--grey-600); + font-weight: normal; + /* text-align: center; */ + cursor: default; +} +h3 small { + margin-left: 0.8rem; + padding: 3px 6px; + background-color: var(--purple-400); + color: #fff; + border-radius: 4px; +} +h4 { + margin: 3.2rem 0 1.6rem 0; + font-size: 1.3em; + line-height: 1.6rem; + color: var(--grey-800); + color: var(--purple-700); + font-weight: normal; + cursor: default; +} +h4 small { + margin-left: 0.8rem; + padding: 3px 6px; + background-color: var(--purple-400); + color: #fff; + border-radius: 4px; } p { - line-height: 1.4em; - font-size: 1.1em; + margin: 1.6rem 0; } a { - color: #3f9acc; -} -a:hover { - color: #6957c4; -} -a.bigBtn { - margin-right: 8px; -} -a.header-anchor { - text-decoration: none; - opacity: 0.5; -} -a.header-anchor:hover { - opacity: 1; + color: var(--blue-700); } ul { - list-style: "- "; + padding-left: 1em; + list-style: '- '; +} +/* .bg--flare { + background: #f12711; + background: -webkit-linear-gradient(to right, #f5af19, #f12711); + background: linear-gradient(to right, #f5af19, #f12711); +} */ +main h1:first-of-type { + margin-top: 1.6rem; +} +.long_form h2 { + /* margin-top: 3.2rem; */ + /* font-size: 1rem; */ + /* font-weight: bold; */ + text-align: left; + color: var(--grey-700); +} +footer h1 { + margin-bottom: 0.8rem; + color: var(--purple-200); + font-size: 1.2rem; + font-weight: bold; + /* border-bottom: solid 1px var(--purple-200); */ +} +footer a { + color: var(--purple-100); +} + +code { + padding: 8px; + background-color: var(--purple-100); + border: 1px solid var(--purple-500); } pre { - white-space: pre-wrap; -} -code { - padding: 2px; - background-color: #eee; - border: solid 1px #ddd; - user-select: all; + padding: 8px 12px; + background-color: var(--purple-100); + border: 1px solid var(--purple-500); + overflow-x: scroll; + line-height: 1.2rem; } pre code { - display: block; - padding: 8px; - word-break: break-word; - /* word-break: break-all; */ + padding: 0; + background-color: 0px; + border: 0px; } + textarea { - width: 100%; - height: 128px; - resize: vertical; - font-size: 0.9rem; -} -input[type="text"] { - margin: 0 12px 12px 0; - width: 45%; - font-size: 0.9rem; -} -input[type="radio"] { - vertical-align: sub; -} -input[type="submit"] { - width: 100%; -} -input[type="submit"][disabled="true"] { - cursor: default; - pointer-events: none; - opacity: 0.3; -} -select { - margin: 0 0 16px 0; -} -.green, a.proofUrl.proofUrl--verified { - color: #499539; -} -.red { - color: red; -} -.label { - display: inline-block; - margin: 0 0 8px; -} -.modes { - display: none; -} -.modes.modes--visible { - display: block; + width: 100%; + height: 128px; + resize: vertical; + font-size: 0.9rem; } -.container--profile { - margin-top: 64px; +form { + margin: 0 0 5.6rem; } -.container--profile .content { - padding-top: 32px; - font-size: 1.2em; -} -.container--profile footer { - text-align: center; -} - -.guides { - display: flex; - flex-wrap: wrap; -} -.guides__section { - flex: 1 0 250px; -} -.guides__section a { - line-height: 1.8em; -} - -#profileHeader { - display: flex; - flex-direction: row; - align-items: center; - margin-bottom: 32px; - background-color: #c4e3f657; - padding: 15px; - border-radius: 15px; -} -#profileAvatar { - width: 100%; - max-width: 128px; - border-radius: 100%; - margin-right: 32px; -} -#profileName { - font-size: 1.6em; - font-weight: bold; - display: inline-block; - max-width: 100%; - white-space: wrap; - overflow: hidden; - text-overflow: ellipsis; -} - -#profileData { - background-color: #dceef957; - padding: 15px; - border-radius: 15px; -} - -.profileDataItem { - position: relative; - display: flex; - flex-wrap: wrap; - justify-content: space-between; -} -.profileDataItem--separator { - margin-top: 1em; - font-weight: bold; -} -.profileDataItem__label { - display: inline-block; - position: relative; - flex: 1; - min-height: 32px; - padding: 0 8px; - max-width: 20%; - font-size: 0.9em; - line-height: 1.6em; - color: #777; - text-align: right; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-transform: capitalize; -} -.profileDataItem__value { - display: inline-block; - flex: 1; - min-height: 32px; - max-width: 100%; - padding: 0 8px; -} -.profileDataItem__value a { - display: inline-block; - max-width: 100%; - margin-right: 8px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.profileDataItem__value small { - color: white; - background: #2178ff; - border-radius: 6px; - padding: 2.8px 5px; - font-size: 0.65em; - vertical-align: middle; -} - -a.proofUrl { - color: #777; -} -a.proofQR { - line-height: 0; - background-color: #499539; - border-radius: 2px; -} -a.proofQR:hover { - background-color: #6abb5a; -} - -#form-generate-signature-profile { - margin-bottom: 2em; - font-size: 0.9rem; -} - -#qrcode { - display: flex; - justify-content: center; - max-width: 100%; - width: auto !important; - height: auto !important; - margin: 32px auto; -} - -noscript { - display: block; - margin-bottom: 2rem; - padding: 8px; - background-color: #f0e68c; - text-align: center; -} -noscript p { - margin: 0; - font-size: 1rem; -} - -@media (max-width: 680px) { - #profileHeader { - flex-direction: column; - } - #profileAvatar { - margin: 0; - } - #profileName { - font-size: 1.2em; - } - .profileDataItem { - flex-direction: column; - margin-bottom: 8px; - } - .profileDataItem__label { - max-width: 100%; - min-height: 28px; - text-align: left; - } - .profileDataItem__value { - min-height: 28px; - } - .profileDataItem--noLabel .profileDataItem__label { - display: none; - } - - #profileData .profileDataItem__value a:first-child { - max-width: 85%; - } - - #profileData #profileProofs .profileDataItem__value a:first-child { - display: block; - } - - - input[type="text"] { - width: 100%; - } +form input[type="submit"] { + display: block; + width: 100%; + padding: 0.4rem 0.8rem; + /* color: #fff; */ + text-decoration: none; + text-transform: uppercase; + background-color: #fff; + border: solid 1px var(--blue-700); + border-radius: 4px; + cursor: pointer; } +form input[type="submit"]:hover { + background-color: var(--blue-700); + color: #fff; +} \ No newline at end of file diff --git a/views-old/404.pug b/views-old/404.pug new file mode 100644 index 0000000..7de3aa3 --- /dev/null +++ b/views-old/404.pug @@ -0,0 +1,6 @@ +extends template.base.pug + +block content + .content + h1 404 + p The requested page could not be found :( diff --git a/views/basic.pug b/views-old/basic.pug similarity index 100% rename from views/basic.pug rename to views-old/basic.pug diff --git a/views/encrypt.pug b/views-old/encrypt.pug similarity index 100% rename from views/encrypt.pug rename to views-old/encrypt.pug diff --git a/views-old/guides.pug b/views-old/guides.pug new file mode 100644 index 0000000..c7d2a79 --- /dev/null +++ b/views-old/guides.pug @@ -0,0 +1,67 @@ +extends template.base.pug + +block content + .content + h1 Guides + .guides + .guides__section + h3 Using Keyoxide + a(href='/guides/verify') Verifying a signature + br + a(href='/guides/encrypt') Encrypting a message + br + a(href='/guides/proofs') Verifying identity proofs + br + a(href='/guides/contributing') Contributing to Keyoxide + br + a(href='/guides/self-hosting-keyoxide') Self-hosting Keyoxide + + .guides__section + h3 OpenPGP and identity proofs + a(href='/guides/openpgp-proofs') How OpenPGP identity proofs work + br + a(href='/guides/web-key-directory') Uploading keys using web key directory + br + a(href='/guides/signature-profiles') Using signature profiles + + .guides__section + h3 Adding proofs + a(href='/guides/devto') Dev.to + br + a(href='/guides/discourse') Discourse + br + a(href='/guides/dns') Domain / DNS + br + a(href='/guides/gitea') Gitea + br + a(href='/guides/github') GitHub + br + a(href='/guides/gitlab') GitLab + br + a(href='/guides/hackernews') Hackernews + br + a(href='/guides/lobsters') Lobste.rs + br + a(href='/guides/mastodon') Mastodon + br + a(href='/guides/owncast') Owncast + br + a(href='/guides/pleroma') Pleroma + br + a(href='/guides/reddit') Reddit + br + a(href='/guides/twitter') Twitter + br + a(href='/guides/xmpp') XMPP+OMEMO + + .guides__section + h3 Other services + a(href='/guides/feature-comparison-keybase') Feature comparison with Keybase + br + a(href='/guides/migrating-from-keybase') Migrating from Keybase + + .guides__section + h3 Managing proofs in GnuPG + a(href='/guides/managing-proofs-listing') Listing proofs + br + a(href='/guides/managing-proofs-deleting') Deleting proofs diff --git a/views-old/index.pug b/views-old/index.pug new file mode 100644 index 0000000..8360818 --- /dev/null +++ b/views-old/index.pug @@ -0,0 +1,103 @@ +extends template.base.pug + +block content + .content + h1 Keyoxide + p + a(href="/") Keyoxide + | is a modern, secure and privacy-friendly platform to establish your + strong decentralized online identity + | . + + a(href="/getting-started").fancyBtn Get started here + + h2 About + p + strong Keyoxide + | allows you to link accounts on various online services and platforms together, prove they belong to you and establish an online identity. This puts + strong you + | , the internet citizen, in charge when it comes to defining who you are on the internet instead of large corporations. + p + | As an example, here's the + a(href='/9f0048ac0b23301e1f77e994909f6bd6f80f485d') developer's Keyoxide profile + | . + p + strong Keyoxide + | is developed by + a(href='https://yarmo.eu') Yarmo Mackenbach + | . The AGPL-v3-licensed code is hosted on + a(href='https://codeberg.org/keyoxide/web') Codeberg + | . It uses + a(href='https://github.com/openpgpjs/openpgpjs') openpgp.js + | for all cryptographic operations. + + h2 Features + + h3 Decentralized online identity proofs + ul + li You decide which accounts are linked together + li You decide where this data is stored + li Keyoxide does not hold your identity data on its servers + li Keyoxide merely verifies the identity proofs and displays them + + h3 Empowering the internet citizen + ul + li A verified identity proof proves ownership of an account and builds trust + li No bad actor can impersonate you as long as your accounts aren't compromised + li Your online identity data is safe from greedy internet corporations + + h3 User-centric platform + ul + li Easily encrypt messages and verify signatures from the profile page + li + | Keyoxide generates QR codes that integrate with + a(href='https://www.openkeychain.org/') OpenKeychain + | and + a(href='https://conversations.im/') Conversations + li Keyoxide fetches the key wherever the user decides to store it + li Keyoxide is self-hostable, meaning you could put it on any server you trust + + h3 Secure and privacy-friendly + ul + li Keyoxide doesn't want your personal data, track you or show you ads + li You never give data to Keyoxide, it simply uses the data you have made public + li + | Keyoxide relies on OpenPGP, a widely used public-key cryptography standard ( + a(href='https://tools.ietf.org/html/rfc4880') RFC-4880 + | ) + li + | Cryptographic operations are performed in-browser by + a(href='https://openpgpjs.org/') OpenPGP.js + | , a library maintained by + a(href='https://protonmail.com/blog/openpgpjs-email-encryption/') ProtonMail + + h3 Free Open Source Software + ul + li + | Keyoxide is licensed under the + a(href='https://codeberg.org/keyoxide/web/src/branch/main/LICENSE') AGPL-v3 license + li + | The source code is hosted on + a(href='https://codeberg.org/keyoxide/web') Codeberg.org + li + | Even the + a(href='https://drone.keyoxide.org/keyoxide/web/') CI/CD activity + | is publicly visible + + .flex-column-container + .flex-column + h2 Cryptographic operations + p + a(href='/verify') Verify PGP signature + br + a(href='/encrypt') Encrypt PGP message + br + a(href='/proofs') Verify distributed identity proofs + .flex-column + h2 Utilities + p + a(href='/util/profile-url') Profile URL generator + br + a(href='/util/wkd') Web Key Directory URL generator + br + a(href='/util/qrfp') Fingerprint QR generator diff --git a/views/partials/key_selector.pug b/views-old/partials/key_selector.pug similarity index 100% rename from views/partials/key_selector.pug rename to views-old/partials/key_selector.pug diff --git a/views-old/profile.pug b/views-old/profile.pug new file mode 100644 index 0000000..fb7ade5 --- /dev/null +++ b/views-old/profile.pug @@ -0,0 +1,41 @@ +doctype html +head + meta(charset='utf-8') + meta(name='viewport' content='width=device-width, initial-scale=1') + meta(name='robots' content='noindex') + link(rel='shortcut icon' href='/favicon.svg') + title Keyoxide + link(rel='stylesheet' href='/static/styles.css') + +main.container.container--profile + .content + noscript + p Keyoxide requires JavaScript to function. + span#profileUid(style='display: none;') #{uid} + span#profileServer(style='display: none;') #{server} + span#profileMode(style='display: none;') #{mode} + if (mode == 'sig') + #profileSigInput + form#form-generate-signature-profile(method='post') + p Please enter the raw profile signature below and press "Generate profile". + textarea#plaintext_input(name='plaintext_input') + input(type='submit', name='submit', value='Generate profile').bigBtn + #profileHeader + img#profileAvatar(src='/static/img/avatar_placeholder.png' alt='avatar' style='display: none') + p#profileName + #profileData + if (mode == 'sig') + p Waiting for input… + else + p Loading keys & verifying proofs… + footer + p + | Generated by + a(href='/') Keyoxide + | ( + a(href="https://codeberg.org/keyoxide/web/releases")= settings.keyoxide_version + | ). + +script(type='application/javascript' src='/static/openpgp.min.js' charset='utf-8') +script(type='application/javascript' src='/static/doip.js' charset='utf-8') +script(type='application/javascript' src='/static/scripts.js' charset='utf-8') diff --git a/views/proofs.pug b/views-old/proofs.pug similarity index 100% rename from views/proofs.pug rename to views-old/proofs.pug diff --git a/views/template.base.pug b/views-old/template.base.pug similarity index 100% rename from views/template.base.pug rename to views-old/template.base.pug diff --git a/views/util/profile-url.pug b/views-old/util/profile-url.pug similarity index 100% rename from views/util/profile-url.pug rename to views-old/util/profile-url.pug diff --git a/views/util/qr.pug b/views-old/util/qr.pug similarity index 100% rename from views/util/qr.pug rename to views-old/util/qr.pug diff --git a/views/util/qrfp.pug b/views-old/util/qrfp.pug similarity index 100% rename from views/util/qrfp.pug rename to views-old/util/qrfp.pug diff --git a/views/util/wkd.pug b/views-old/util/wkd.pug similarity index 100% rename from views/util/wkd.pug rename to views-old/util/wkd.pug diff --git a/views/verify.pug b/views-old/verify.pug similarity index 100% rename from views/verify.pug rename to views-old/verify.pug diff --git a/views/404.pug b/views/404.pug index 7de3aa3..5ad9ea8 100644 --- a/views/404.pug +++ b/views/404.pug @@ -1,6 +1,5 @@ -extends template.base.pug +extends templates/base.pug block content - .content - h1 404 - p The requested page could not be found :( + h1 404 + p The requested page could not be found :( diff --git a/views/guides.pug b/views/guides.pug index c7d2a79..c03ce8f 100644 --- a/views/guides.pug +++ b/views/guides.pug @@ -1,67 +1,71 @@ -extends template.base.pug +extends templates/base.pug block content - .content - h1 Guides - .guides - .guides__section + h1 Guides + .hcards.hcards--guides.hcards--max-3 + .card.guides__section h3 Using Keyoxide - a(href='/guides/verify') Verifying a signature - br - a(href='/guides/encrypt') Encrypting a message - br - a(href='/guides/proofs') Verifying identity proofs - br - a(href='/guides/contributing') Contributing to Keyoxide - br - a(href='/guides/self-hosting-keyoxide') Self-hosting Keyoxide + p + a(href='/guides/verify') Verifying a signature + br + a(href='/guides/encrypt') Encrypting a message + br + a(href='/guides/proofs') Verifying identity proofs + br + a(href='/guides/contributing') Contributing to Keyoxide + br + a(href='/guides/self-hosting-keyoxide') Self-hosting Keyoxide - .guides__section + .card.guides__section h3 OpenPGP and identity proofs - a(href='/guides/openpgp-proofs') How OpenPGP identity proofs work - br - a(href='/guides/web-key-directory') Uploading keys using web key directory - br - a(href='/guides/signature-profiles') Using signature profiles + p + a(href='/guides/openpgp-proofs') How OpenPGP identity proofs work + br + a(href='/guides/web-key-directory') Uploading keys using web key directory + br + a(href='/guides/signature-profiles') Using signature profiles - .guides__section + .card.guides__section h3 Adding proofs - a(href='/guides/devto') Dev.to - br - a(href='/guides/discourse') Discourse - br - a(href='/guides/dns') Domain / DNS - br - a(href='/guides/gitea') Gitea - br - a(href='/guides/github') GitHub - br - a(href='/guides/gitlab') GitLab - br - a(href='/guides/hackernews') Hackernews - br - a(href='/guides/lobsters') Lobste.rs - br - a(href='/guides/mastodon') Mastodon - br - a(href='/guides/owncast') Owncast - br - a(href='/guides/pleroma') Pleroma - br - a(href='/guides/reddit') Reddit - br - a(href='/guides/twitter') Twitter - br - a(href='/guides/xmpp') XMPP+OMEMO + p + a(href='/guides/devto') Dev.to + br + a(href='/guides/discourse') Discourse + br + a(href='/guides/dns') Domain / DNS + br + a(href='/guides/gitea') Gitea + br + a(href='/guides/github') GitHub + br + a(href='/guides/gitlab') GitLab + br + a(href='/guides/hackernews') Hackernews + br + a(href='/guides/lobsters') Lobste.rs + br + a(href='/guides/mastodon') Mastodon + br + a(href='/guides/owncast') Owncast + br + a(href='/guides/pleroma') Pleroma + br + a(href='/guides/reddit') Reddit + br + a(href='/guides/twitter') Twitter + br + a(href='/guides/xmpp') XMPP+OMEMO - .guides__section + .card.guides__section h3 Other services - a(href='/guides/feature-comparison-keybase') Feature comparison with Keybase - br - a(href='/guides/migrating-from-keybase') Migrating from Keybase + p + a(href='/guides/feature-comparison-keybase') Feature comparison with Keybase + br + a(href='/guides/migrating-from-keybase') Migrating from Keybase - .guides__section + .card.guides__section h3 Managing proofs in GnuPG - a(href='/guides/managing-proofs-listing') Listing proofs - br - a(href='/guides/managing-proofs-deleting') Deleting proofs + p + a(href='/guides/managing-proofs-listing') Listing proofs + br + a(href='/guides/managing-proofs-deleting') Deleting proofs diff --git a/views/index.pug b/views/index.pug index 8360818..ef6648c 100644 --- a/views/index.pug +++ b/views/index.pug @@ -1,103 +1,78 @@ -extends template.base.pug +extends templates/base.pug block content - .content - h1 Keyoxide - p - a(href="/") Keyoxide - | is a modern, secure and privacy-friendly platform to establish your - strong decentralized online identity - | . - - a(href="/getting-started").fancyBtn Get started here - - h2 About - p - strong Keyoxide - | allows you to link accounts on various online services and platforms together, prove they belong to you and establish an online identity. This puts - strong you - | , the internet citizen, in charge when it comes to defining who you are on the internet instead of large corporations. - p - | As an example, here's the - a(href='/9f0048ac0b23301e1f77e994909f6bd6f80f485d') developer's Keyoxide profile - | . - p - strong Keyoxide - | is developed by - a(href='https://yarmo.eu') Yarmo Mackenbach - | . The AGPL-v3-licensed code is hosted on - a(href='https://codeberg.org/keyoxide/web') Codeberg - | . It uses - a(href='https://github.com/openpgpjs/openpgpjs') openpgp.js - | for all cryptographic operations. - - h2 Features - - h3 Decentralized online identity proofs - ul - li You decide which accounts are linked together - li You decide where this data is stored - li Keyoxide does not hold your identity data on its servers - li Keyoxide merely verifies the identity proofs and displays them - - h3 Empowering the internet citizen - ul - li A verified identity proof proves ownership of an account and builds trust - li No bad actor can impersonate you as long as your accounts aren't compromised - li Your online identity data is safe from greedy internet corporations - - h3 User-centric platform - ul - li Easily encrypt messages and verify signatures from the profile page - li - | Keyoxide generates QR codes that integrate with - a(href='https://www.openkeychain.org/') OpenKeychain - | and - a(href='https://conversations.im/') Conversations - li Keyoxide fetches the key wherever the user decides to store it - li Keyoxide is self-hostable, meaning you could put it on any server you trust - - h3 Secure and privacy-friendly - ul - li Keyoxide doesn't want your personal data, track you or show you ads - li You never give data to Keyoxide, it simply uses the data you have made public - li - | Keyoxide relies on OpenPGP, a widely used public-key cryptography standard ( - a(href='https://tools.ietf.org/html/rfc4880') RFC-4880 - | ) - li - | Cryptographic operations are performed in-browser by - a(href='https://openpgpjs.org/') OpenPGP.js - | , a library maintained by - a(href='https://protonmail.com/blog/openpgpjs-email-encryption/') ProtonMail - - h3 Free Open Source Software - ul - li - | Keyoxide is licensed under the - a(href='https://codeberg.org/keyoxide/web/src/branch/main/LICENSE') AGPL-v3 license - li - | The source code is hosted on - a(href='https://codeberg.org/keyoxide/web') Codeberg.org - li - | Even the - a(href='https://drone.keyoxide.org/keyoxide/web/') CI/CD activity - | is publicly visible - - .flex-column-container - .flex-column - h2 Cryptographic operations - p - a(href='/verify') Verify PGP signature - br - a(href='/encrypt') Encrypt PGP message - br - a(href='/proofs') Verify distributed identity proofs - .flex-column - h2 Utilities - p - a(href='/util/profile-url') Profile URL generator - br - a(href='/util/wkd') Web Key Directory URL generator - br - a(href='/util/qrfp') Fingerprint QR generator + if highlights.length > 0 + h2 Highlights + .hcards.hcards--highlights + each hl in highlights + .card.card--small-profile + p.name= hl.name + p + span.fingerprint= hl.fingerprint + br + span.details= hl.description + .spacer + p + a(href=`/${hl.fingerprint}`) View profile + - var n = 0 + while n < 3-highlights.length + .card.card--small-profile-dummy + - n++ + + .demo + .card.card--profile + .claim + .claim__main + .claim__description + p @alice@example.instance + .claim__links + p + span Fediverse + a(href="#") View account + a(href="#") View proof + a(href="#") Details + .claim__verification.claim__verification--true ✔ + + h2 About Keyoxide + .hcards.hcards--features.hcards--max-3 + .card + h3 Online identity + p Establish an identity by verifiably linking your online accounts. + .card + h3 Decentralized + p No central server or database. Control how your data is stored and accessed. + .card + h3 Privacy + p No data is collected. Your data is yours and yours only. + .card + h3 Cryptography + p Your online identity verifiably signed with widely-used OpenPGP. + .card + h3 Open Source + p All Keyoxide projects are licensed under AGPL-3.0-or-later. + .card + h3 Funded by donations + p Transparent funding. Keyoxide stands against VC and surveillance capitalism. + + + h2 Links + .hcards + .card + h3 Getting started + p + a(href='/') What is Keyoxide? + br + a(href='/') Getting started + br + a(href='/') Guides + br + a(href='/') FAQ + + .card + h3 Utilities + p + a(href='/util/profile-url') Profile URL generator + br + a(href='/util/wkd') Web Key Directory URL generator + br + a(href='/util/qrfp') Fingerprint QR generator diff --git a/views/long-form-content.pug b/views/long-form-content.pug new file mode 100644 index 0000000..3fa851c --- /dev/null +++ b/views/long-form-content.pug @@ -0,0 +1,6 @@ +extends templates/base.pug + +block content + section.long_form + h1= title + .card !{ content } \ No newline at end of file diff --git a/views/partials/footer.pug b/views/partials/footer.pug new file mode 100644 index 0000000..10341d1 --- /dev/null +++ b/views/partials/footer.pug @@ -0,0 +1,32 @@ +footer + .container + .hcards + div + h1 keyoxide.org + a(href="/") Homepage + br + a(href="/") What is Keyoxide? + br + a(href="/") Getting started + br + a(href="/") Guides + br + a(href="/") FAQ + + div + h1 Keyoxide project + a(href="/") Keyoxide.org + br + a(href="/") Keyoxide on Fediverse + br + a(href="/") Key to Identity Foundation + + div + h1 Development + a(href="/") Source code + br + a(href="/") CI/CD + br + a(href="/") doip.js + + p.copyright © 2021 Keyoxide project contributors diff --git a/views/partials/header.pug b/views/partials/header.pug new file mode 100644 index 0000000..d8cca84 --- /dev/null +++ b/views/partials/header.pug @@ -0,0 +1,11 @@ +header + nav + .spacer + a.text(href='/') About + a.text(href='/getting-started') Getting started + a.logo(href='/') + img(src='/static/img/logo_circle.png' alt='Keyoxide') + nav + a.text(href='/guides') Guides + a.text(href='/faq') FAQ + .spacer diff --git a/views/profile.pug b/views/profile.pug index fb7ade5..dbb84b7 100644 --- a/views/profile.pug +++ b/views/profile.pug @@ -1,41 +1,29 @@ -doctype html -head - meta(charset='utf-8') - meta(name='viewport' content='width=device-width, initial-scale=1') - meta(name='robots' content='noindex') - link(rel='shortcut icon' href='/favicon.svg') - title Keyoxide - link(rel='stylesheet' href='/static/styles.css') - -main.container.container--profile - .content +extends templates/base.pug + +block js + script(type='application/javascript' src='/static/openpgp.min.js' charset='utf-8') + script(type='application/javascript' src='/static/doip.js' charset='utf-8') + script(type='application/javascript' src='/static/scripts.js' charset='utf-8') + +block content + section.profile noscript p Keyoxide requires JavaScript to function. span#profileUid(style='display: none;') #{uid} span#profileServer(style='display: none;') #{server} span#profileMode(style='display: none;') #{mode} - if (mode == 'sig') - #profileSigInput - form#form-generate-signature-profile(method='post') - p Please enter the raw profile signature below and press "Generate profile". - textarea#plaintext_input(name='plaintext_input') - input(type='submit', name='submit', value='Generate profile').bigBtn - #profileHeader - img#profileAvatar(src='/static/img/avatar_placeholder.png' alt='avatar' style='display: none') - p#profileName - #profileData - if (mode == 'sig') - p Waiting for input… - else - p Loading keys & verifying proofs… - footer - p - | Generated by - a(href='/') Keyoxide - | ( - a(href="https://codeberg.org/keyoxide/web/releases")= settings.keyoxide_version - | ). -script(type='application/javascript' src='/static/openpgp.min.js' charset='utf-8') -script(type='application/javascript' src='/static/doip.js' charset='utf-8') -script(type='application/javascript' src='/static/scripts.js' charset='utf-8') + if (mode == 'sig') + #profileSigInput.card + form#form-generate-signature-profile(method='post') + label(for="plaintext_input") Please enter the raw profile signature below and press "Generate profile". + textarea#plaintext_input(name='plaintext_input') + input(type='submit', name='submit', value='Generate profile') + + #profileHeader.card.card--profileHeader + + #profileProofs.card + if (mode == 'sig') + //- p Waiting for input… + else + p Loading keys & verifying proofs… \ No newline at end of file diff --git a/views/templates/base.pug b/views/templates/base.pug new file mode 100644 index 0000000..b49cf8a --- /dev/null +++ b/views/templates/base.pug @@ -0,0 +1,18 @@ +doctype html +head + meta(charset='utf-8') + meta(name='viewport' content='width=device-width, initial-scale=1') + meta(name='theme-color' content='#fff') + link(rel='shortcut icon' href='/favicon.svg') + title= (title ? title : "Keyoxide") + link(rel='stylesheet' href='/static/styles.css') + +include ../partials/header.pug + +main + .container + block content + +include ../partials/footer.pug + +block js