Improve layout and rendering of profiles

This commit is contained in:
Yarmo Mackenbach 2021-05-02 12:49:52 +02:00
parent 9307e1c1dc
commit e18753b54a
No known key found for this signature in database
GPG key ID: 37367F4AF4087AD1
13 changed files with 1070 additions and 1112 deletions

View file

@ -27,37 +27,35 @@ 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 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 <https://www.gnu.org/licenses/>. more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/ */
const express = require('express'); const express = require('express')
const fs = require('fs'); const fs = require('fs')
const app = express(); const app = express()
const env = {}; const { stringReplace } = require('string-replace-middleware')
const { stringReplace } = require('string-replace-middleware'); require('dotenv').config()
require('dotenv').config();
let packageData = JSON.parse(fs.readFileSync('package.json')); const packageData = JSON.parse(fs.readFileSync('package.json'))
app.set('env', process.env.NODE_ENV || "production"); app.set('env', process.env.NODE_ENV || "production")
app.set('view engine', 'pug'); app.set('view engine', 'pug')
app.set('port', process.env.PORT || 3000); app.set('port', process.env.PORT || 3000)
app.set('domain', process.env.DOMAIN || "keyoxide.org"); app.set('domain', process.env.DOMAIN || "keyoxide.org")
app.set('keyoxide_version', packageData.version); app.set('keyoxide_version', packageData.version)
app.set('onion_url', process.env.ONION_URL); app.set('onion_url', process.env.ONION_URL)
app.use('/favicon.svg', express.static('favicon.svg')); app.use('/favicon.svg', express.static('favicon.svg'))
app.use('/robots.txt', express.static('robots.txt')); app.use('/robots.txt', express.static('robots.txt'))
app.use(stringReplace({ app.use(stringReplace({
PLACEHOLDER__XMPP_VCARD_SERVER_DOMAIN: process.env.XMPP_VCARD_SERVER_DOMAIN || 'xmpp-vcard.keyoxide.org' PLACEHOLDER__PROXY_HOSTNAME: process.env.PROXY_HOSTNAME || 'null'
}, { }, {
contentTypeFilterRegexp: /application\/javascript/, contentTypeFilterRegexp: /application\/javascript/,
})); }))
app.use('/', require('./routes/main')); app.use('/', require('./routes/main'))
app.use('/static', require('./routes/static')); app.use('/static', require('./routes/static'))
app.use('/server', require('./routes/server')); app.use('/util', require('./routes/util'))
app.use('/util', require('./routes/util')); app.use('/', require('./routes/profile'))
app.use('/', require('./routes/profile'));
app.listen(app.get('port'), () => { app.listen(app.get('port'), () => {
console.log(`Node server listening at http://localhost:${app.get('port')}`); console.log(`Node server listening at http://localhost:${app.get('port')}`)
}); })

View file

@ -27,30 +27,43 @@ 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 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 <https://www.gnu.org/licenses/>. more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/ */
const router = require('express').Router(); const router = require('express').Router()
const kx = require('../server')
router.get('/sig', function(req, res) { router.get('/sig', (req, res) => {
res.render('profile', { mode: 'sig' }) res.render('profile', { mode: 'sig' })
}); })
router.get('/wkd/:input', function(req, res) { router.get('/wkd/:id', async (req, res) => {
res.render('profile', { mode: 'wkd', uid: req.params.input }) const data = await kx.generateWKDProfile(req.params.id)
}); if (data.errors.length > 0) {
return res.render('profile-failed', { data: data })
}
res.render('profile', { data: data })
})
router.get('/hkp/:input', function(req, res) { router.get('/hkp/:id', async (req, res) => {
res.render('profile', { mode: 'hkp', uid: req.params.input }) const data = await kx.generateHKPProfile(req.params.id)
}); if (data.errors.length > 0) {
return res.render('profile-failed', { data: data })
}
res.render('profile', { data: data })
})
router.get('/hkp/:server/:input', function(req, res) { router.get('/hkp/:server/:id', async (req, res) => {
res.render('profile', { mode: 'hkp', uid: req.params.input, server: req.params.server }) const data = await kx.generateHKPProfile(req.params.id, req.params.server)
}); if (data.errors.length > 0) {
return res.render('profile-failed', { data: data })
}
res.render('profile', { data: data })
})
router.get('/keybase/:username/:fingerprint', function(req, res) { router.get('/keybase/:username/:fingerprint', async (req, res) => {
res.render('profile', { mode: 'keybase', uid: `${req.params.username}/${req.params.fingerprint}` }) res.render('profile', { mode: 'keybase', uid: `${req.params.username}/${req.params.fingerprint}` })
}); })
router.get('/:input', function(req, res) { router.get('/:input', async (req, res) => {
res.render('profile', { mode: 'auto', uid: req.params.input }) res.render('profile', { mode: 'auto', uid: req.params.input })
}); })
module.exports = router; module.exports = router

View file

@ -27,37 +27,37 @@ 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 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 <https://www.gnu.org/licenses/>. more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/ */
const express = require('express'); const express = require('express')
const router = require('express').Router(); const router = require('express').Router()
router.get('/doip.min.js', function(req, res) { router.get('/doip.min.js', function(req, res) {
res.sendFile(`node_modules/doipjs/dist/doip.min.js`, { root: `${__dirname}/../` }) res.sendFile(`node_modules/doipjs/dist/doip.min.js`, { root: `${__dirname}/../` })
}); })
router.get('/doip.js', function(req, res) { router.get('/doip.js', function(req, res) {
res.sendFile(`node_modules/doipjs/dist/doip.js`, { root: `${__dirname}/../` }) res.sendFile(`node_modules/doipjs/dist/doip.js`, { root: `${__dirname}/../` })
}); })
router.get('/openpgp.min.js', function(req, res) { router.get('/openpgp.min.js', function(req, res) {
res.sendFile(`node_modules/openpgp/dist/openpgp.min.js`, { root: `${__dirname}/../` }) res.sendFile(`node_modules/openpgp/dist/openpgp.min.js`, { root: `${__dirname}/../` })
}); })
router.get('/openpgp.min.js.map', function(req, res) { router.get('/openpgp.min.js.map', function(req, res) {
res.sendFile(`node_modules/openpgp/dist/openpgp.min.js.map`, { root: `${__dirname}/../` }) res.sendFile(`node_modules/openpgp/dist/openpgp.min.js.map`, { root: `${__dirname}/../` })
}); })
router.get('/qrcode.min.js', function(req, res) { router.get('/qrcode.min.js', function(req, res) {
res.sendFile(`node_modules/qrcode/build/qrcode.min.js`, { root: `${__dirname}/../` }) res.sendFile(`node_modules/qrcode/build/qrcode.min.js`, { root: `${__dirname}/../` })
}); })
router.get('/qrcode.min.js.map', function(req, res) { router.get('/qrcode.min.js.map', function(req, res) {
res.sendFile(`node_modules/qrcode/build/qrcode.min.js.map`, { root: `${__dirname}/../` }) res.sendFile(`node_modules/qrcode/build/qrcode.min.js.map`, { root: `${__dirname}/../` })
}); })
router.get('/dialog-polyfill.js', function(req, res) { router.get('/dialog-polyfill.js', function(req, res) {
res.sendFile(`node_modules/dialog-polyfill/dist/dialog-polyfill.js`, { root: `${__dirname}/../` }) res.sendFile(`node_modules/dialog-polyfill/dist/dialog-polyfill.js`, { root: `${__dirname}/../` })
}); })
router.get('/dialog-polyfill.css', function(req, res) { router.get('/dialog-polyfill.css', function(req, res) {
res.sendFile(`node_modules/dialog-polyfill/dist/dialog-polyfill.css`, { root: `${__dirname}/../` }) res.sendFile(`node_modules/dialog-polyfill/dist/dialog-polyfill.css`, { root: `${__dirname}/../` })
}); })
router.use('/', express.static('static')); router.use('/', express.static('static'))
module.exports = router; module.exports = router

122
server/index.js Normal file
View file

@ -0,0 +1,122 @@
/*
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 <https://www.gnu.org/licenses/>.
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 <https://www.gnu.org/licenses/>.
*/
const doip = require('doipjs')
const openpgp = require('openpgp')
const keys = require('./keys')
const generateWKDProfile = async (id) => {
return keys.fetchWKD(id)
.then(async key => {
let keyData = await doip.keys.process(key.publicKey)
keyData.key.fetchMethod = 'wkd'
keyData.key.uri = key.fetchURL
keyData = processKeyData(keyData)
return {
key: key,
keyData: keyData,
extra: await computeExtraData(key, keyData),
errors: []
}
})
.catch(err => {
return {
key: null,
keyData: null,
extra: null,
errors: [err.message]
}
})
}
const generateHKPProfile = async (id, keyserverDomain) => {
return keys.fetchHKP(id, keyserverDomain)
.then(async key => {
let keyData = await doip.keys.process(key.publicKey)
keyData.key.fetchMethod = 'hkp'
keyData.key.uri = key.fetchURL
keyData = processKeyData(keyData)
return {
key: key,
keyData: keyData,
extra: await computeExtraData(key, keyData),
errors: []
}
})
.catch(err => {
return {
key: null,
keyData: null,
extra: null,
errors: [err.message]
}
})
}
const processKeyData = (keyData) => {
keyData.users.forEach(user => {
// Match claims
user.claims.forEach(claim => {
claim.match()
})
// Sort claims
user.claims.sort((a,b) => {
if (a.matches.length == 0) return 1
if (b.matches.length == 0) return -1
if (a.matches[0].serviceprovider.name < b.matches[0].serviceprovider.name) {
return -1
}
if (a.matches[0].serviceprovider.name > b.matches[0].serviceprovider.name) {
return 1
}
return 0
})
})
return keyData
}
const computeExtraData = async (key, keyData) => {
// Get the primary user
const primaryUser = await key.publicKey.getPrimaryUser()
// Compute hash needed for avatar services
const profileHash = openpgp.util.str_to_hex(openpgp.util.Uint8Array_to_str(await openpgp.crypto.hash.md5(openpgp.util.str_to_Uint8Array(primaryUser.user.userId.email))))
return {
avatarURL: `https://www.gravatar.com/avatar/${profileHash}?s=128&d=mm`
}
}
exports.generateWKDProfile = generateWKDProfile
exports.generateHKPProfile = generateHKPProfile

122
server/keys.js Normal file
View file

@ -0,0 +1,122 @@
/*
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 <https://www.gnu.org/licenses/>.
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 <https://www.gnu.org/licenses/>.
*/
const got = require('got')
const doip = require('doipjs')
const openpgp = require('openpgp')
const utils = require('./utils')
const fetchWKD = (id) => {
return new Promise(async (resolve, reject) => {
let output = {
publicKey: null,
fetchURL: null
}
const [, localPart, domain] = /([^\@]*)@(.*)/.exec(id)
const localEncoded = await utils.computeWKDLocalPart(localPart)
const urlAdvanced = `https://openpgpkey.${domain}/.well-known/openpgpkey/${domain}/hu/${localEncoded}`
const urlDirect = `https://${domain}/.well-known/openpgpkey/hu/${localEncoded}`
let plaintext
try {
plaintext = await got(urlAdvanced).then((response) => {
if (response.statusCode === 200) {
output.fetchURL = urlAdvanced
return new Uint8Array(response.rawBody)
} else {
return null
}
})
} catch (e) {
try {
plaintext = await got(urlDirect).then((response) => {
if (response.statusCode === 200) {
output.fetchURL = urlDirect
return new Uint8Array(response.rawBody)
} else {
return null
}
})
} catch (error) {
reject(new Error("No public keys could be fetched using WKD"))
}
}
if (!plaintext) {
reject(new Error("No public keys could be fetched using WKD"))
}
try {
output.publicKey = (await openpgp.key.read(plaintext)).keys[0]
} catch(error) {
reject(new Error("No public keys could be read from the data fetched using WKD"))
}
if (!output.publicKey) {
reject(new Error("No public keys could be read from the data fetched using WKD"))
}
resolve(output)
})
}
const fetchHKP = (id, keyserverDomain) => {
return new Promise(async (resolve, reject) => {
let output = {
publicKey: null,
fetchURL: null
}
keyserverDomain = keyserverDomain ? keyserverDomain : 'keys.openpgp.org'
let query = ''
if (id.includes('@')) {
query = id
} else {
query = `0x${id}`
}
try {
output.publicKey = await doip.keys.fetchHKP(id, keyserverDomain)
output.fetchURL = `https://${keyserverDomain}/pks/lookup?op=get&options=mr&search=${query}`
} catch(error) {
reject(new Error("No public keys could be fetched using HKP"))
}
if (!output.publicKey) {
reject(new Error("No public keys could be fetched using HKP"))
}
resolve(output)
})
}
exports.fetchWKD = fetchWKD
exports.fetchHKP = fetchHKP

36
server/utils.js Normal file
View file

@ -0,0 +1,36 @@
/*
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 <https://www.gnu.org/licenses/>.
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 <https://www.gnu.org/licenses/>.
*/
const openpgp = require('openpgp')
exports.computeWKDLocalPart = async (message) => {
const data = openpgp.util.str_to_Uint8Array(message.toLowerCase());
const hash = await openpgp.crypto.hash.sha1(data);
return openpgp.util.encodeZBase32(hash);
}

205
static/kx-claim.js Normal file
View file

@ -0,0 +1,205 @@
class Claim extends HTMLElement {
// Specify the attributes to observe
static get observedAttributes() {
return ['data-claim'];
}
constructor() {
// Call super
super();
// Shadow root
this.attachShadow({mode: 'open'});
// Details element
const details = document.createElement('details');
details.setAttribute('class', 'kx-item');
// Summary element
const summary = details.appendChild(document.createElement('summary'));
// Info
const info = summary.appendChild(document.createElement('div'));
info.setAttribute('class', 'info');
// Info > Service provider
const serviceProvider = info.appendChild(document.createElement('p'));
serviceProvider.setAttribute('class', 'subtitle');
// Info > Profile
const profile = info.appendChild(document.createElement('p'));
profile.setAttribute('class', 'title');
// Icons
const icons = summary.appendChild(document.createElement('div'));
icons.setAttribute('class', 'icons');
const icons__verificationStatus = icons.appendChild(document.createElement('div'));
icons__verificationStatus.setAttribute('class', 'verificationStatus');
const icons__verificationStatus__inProgress = icons__verificationStatus.appendChild(document.createElement('div'));
icons__verificationStatus__inProgress.setAttribute('class', 'inProgress');
// Details content
const content = details.appendChild(document.createElement('div'));
content.setAttribute('class', 'content');
// Load CSS stylesheet
const linkCSS = document.createElement('link');
linkCSS.setAttribute('rel', 'stylesheet');
linkCSS.setAttribute('href', '/static/kx-styles.css');
// Attach the elements to the shadow DOM
this.shadowRoot.append(linkCSS, details);
}
attributeChangedCallback(name, oldValue, newValue) {
this.updateContent(newValue);
}
async verify() {
const claim = new doip.Claim(JSON.parse(this.getAttribute('data-claim')));
await claim.verify({
proxy: {
policy: 'adaptive',
hostname: 'PLACEHOLDER__PROXY_HOSTNAME'
}
});
this.setAttribute('data-claim', JSON.stringify(claim));
}
updateContent(value) {
const shadow = this.shadowRoot;
const claim = new doip.Claim(JSON.parse(value));
switch (claim.matches[0].serviceprovider.name) {
case 'dns':
case 'xmpp':
case 'irc':
shadow.querySelector('.info .subtitle').innerText = claim.matches[0].serviceprovider.name.toUpperCase();
break;
default:
shadow.querySelector('.info .subtitle').innerText = claim.matches[0].serviceprovider.name;
break;
}
shadow.querySelector('.info .title').innerText = claim.matches[0].profile.display;
try {
if (claim.status === 'verified') {
shadow.querySelector('.icons .verificationStatus').setAttribute('data-value', claim.verification.result ? 'success' : 'failed');
} else {
shadow.querySelector('.icons .verificationStatus').setAttribute('data-value', 'running');
}
} catch (error) {
shadow.querySelector('.icons .verificationStatus').setAttribute('data-value', 'failed');
}
const elContent = shadow.querySelector('.content');
elContent.innerHTML = ``;
// Handle failed ambiguous claim
if (claim.status === 'verified' && !claim.verification.result && claim.isAmbiguous()) {
shadow.querySelector('.info .subtitle').innerText = '---';
const subsection0 = elContent.appendChild(document.createElement('div'));
subsection0.setAttribute('class', 'subsection');
const subsection0_icon = subsection0.appendChild(document.createElement('img'));
subsection0_icon.setAttribute('src', '/static/img/alert-decagram.png');
const subsection0_text = subsection0.appendChild(document.createElement('div'));
const message = subsection0_text.appendChild(document.createElement('p'));
message.innerHTML = `None of the matched service providers could be verified. Keyoxide is not able to determine which is the correct service provider and why the verification process failed.`;
return;
}
// Links to profile and proof
const subsection1 = elContent.appendChild(document.createElement('div'));
subsection1.setAttribute('class', 'subsection');
const subsection1_icon = subsection1.appendChild(document.createElement('img'));
subsection1_icon.setAttribute('src', '/static/img/link.png');
const subsection1_text = subsection1.appendChild(document.createElement('div'));
const profile_link = subsection1_text.appendChild(document.createElement('p'));
if (claim.matches[0].profile.uri) {
profile_link.innerHTML = `Profile link: <a href="${claim.matches[0].profile.uri}">${claim.matches[0].profile.uri}</a>`;
} else {
profile_link.innerHTML = `Profile link: not accessible from browser`;
}
const proof_link = subsection1_text.appendChild(document.createElement('p'));
if (claim.matches[0].proof.uri) {
proof_link.innerHTML = `Proof link: <a href="${claim.matches[0].proof.uri}">${claim.matches[0].proof.uri}</a>`;
} else {
proof_link.innerHTML = `Proof link: not accessible from browser`;
}
elContent.appendChild(document.createElement('hr'));
// Claim verification status
const subsection2 = elContent.appendChild(document.createElement('div'));
subsection2.setAttribute('class', 'subsection');
const subsection2_icon = subsection2.appendChild(document.createElement('img'));
subsection2_icon.setAttribute('src', '/static/img/decagram.png');
const subsection2_text = subsection2.appendChild(document.createElement('div'));
const verification = subsection2_text.appendChild(document.createElement('p'));
if (claim.status === 'verified') {
verification.innerHTML = `Claim verification has completed.`;
subsection2_icon.setAttribute('src', '/static/img/check-decagram.png');
} else {
verification.innerHTML = `Claim verification is in progress&hellip;`;
return;
}
elContent.appendChild(document.createElement('hr'));
// Result of claim verification
const subsection3 = elContent.appendChild(document.createElement('div'));
subsection3.setAttribute('class', 'subsection');
const subsection3_icon = subsection3.appendChild(document.createElement('img'));
subsection3_icon.setAttribute('src', '/static/img/shield-search.png');
const subsection3_text = subsection3.appendChild(document.createElement('div'));
const result = subsection3_text.appendChild(document.createElement('p'));
result.innerHTML = `The claim <strong>${claim.verification.result ? 'HAS BEEN' : 'COULD NOT BE'}</strong> verified by the proof.`;
// Additional info
if (claim.verification.proof.viaProxy) {
elContent.appendChild(document.createElement('hr'));
const subsection4 = elContent.appendChild(document.createElement('div'));
subsection4.setAttribute('class', 'subsection');
const subsection4_icon = subsection4.appendChild(document.createElement('img'));
subsection4_icon.setAttribute('src', '/static/img/information.png');
const subsection4_text = subsection4.appendChild(document.createElement('div'));
const result_proxyUsed = subsection4_text.appendChild(document.createElement('p'));
result_proxyUsed.innerHTML = `A proxy was used to fetch the proof: <a href="https://PLACEHOLDER__PROXY_HOSTNAME">PLACEHOLDER__PROXY_HOSTNAME</a>`;
}
// TODO Display errors
// if (claim.verification.errors.length > 0) {
// console.log(claim.verification);
// elContent.appendChild(document.createElement('hr'));
// const subsection5 = elContent.appendChild(document.createElement('div'));
// subsection5.setAttribute('class', 'subsection');
// const subsection5_icon = subsection5.appendChild(document.createElement('img'));
// subsection5_icon.setAttribute('src', '/static/img/alert-circle.png');
// const subsection5_text = subsection5.appendChild(document.createElement('div'));
// claim.verification.errors.forEach(message => {
// const error = subsection5_text.appendChild(document.createElement('p'));
// if (message instanceof Error) {
// error.innerText = message.message;
// } else {
// error.innerText = message;
// }
// });
// }
}
}
customElements.define('kx-claim', Claim);

72
static/kx-key.js Normal file
View file

@ -0,0 +1,72 @@
class Key extends HTMLElement {
// Specify the attributes to observe
static get observedAttributes() {
return ['data-keydata'];
}
constructor() {
// Call super
super();
// Shadow root
this.attachShadow({mode: 'open'});
// Details element
const details = document.createElement('details');
details.setAttribute('class', 'kx-item');
// Summary element
const summary = details.appendChild(document.createElement('summary'));
// Info
const info = summary.appendChild(document.createElement('div'));
info.setAttribute('class', 'info');
// Info > Protocol
const serviceProvider = info.appendChild(document.createElement('p'));
serviceProvider.setAttribute('class', 'subtitle');
// Info > Fingerprint
const profile = info.appendChild(document.createElement('p'));
profile.setAttribute('class', 'title');
// Details content
const content = details.appendChild(document.createElement('div'));
content.setAttribute('class', 'content');
// Load CSS stylesheet
const linkCSS = document.createElement('link');
linkCSS.setAttribute('rel', 'stylesheet');
linkCSS.setAttribute('href', '/static/kx-styles.css');
// Attach the elements to the shadow DOM
this.shadowRoot.append(linkCSS, details);
}
attributeChangedCallback(name, oldValue, newValue) {
this.updateContent(newValue);
}
updateContent(value) {
const shadow = this.shadowRoot;
const data = JSON.parse(value);
shadow.querySelector('.info .subtitle').innerText = data.key.fetchMethod;
shadow.querySelector('.info .title').innerText = data.fingerprint;
const elContent = shadow.querySelector('.content');
elContent.innerHTML = ``;
// Link to key
const subsection1 = elContent.appendChild(document.createElement('div'));
subsection1.setAttribute('class', 'subsection');
const subsection1_icon = subsection1.appendChild(document.createElement('img'));
subsection1_icon.setAttribute('src', '/static/img/link.png');
const subsection1_text = subsection1.appendChild(document.createElement('div'));
const profile_link = subsection1_text.appendChild(document.createElement('p'));
profile_link.innerHTML = `Key link: <a href="${data.key.uri}">${data.key.uri}</a>`;
}
}
customElements.define('kx-key', Key);

206
static/kx-styles.css Normal file
View file

@ -0,0 +1,206 @@
* {
box-sizing: border-box;
}
details.kx-item {
width: 100%;
border-radius: 8px;
}
details.kx-item p {
margin: 0;
word-break: break-word;
}
details.kx-item a {
color: var(--blue-700);
}
details.kx-item hr {
border: none;
border-top: 2px solid var(--purple-100);
}
details.kx-item .content {
padding: 12px;
border: solid 3px var(--purple-100);
border-top: 0px;
border-radius: 0px 0px 8px 8px;
}
details.kx-item summary {
display: flex;
align-items: center;
padding: 8px 12px;
background-color: var(--purple-100);
border: solid 3px var(--purple-100);
border-radius: 8px;
list-style: none;
cursor: pointer;
}
details.kx-item summary::-webkit-details-marker {
display: none;
}
details.kx-item summary:hover, summary:focus {
border-color: var(--purple-400);
}
details[open] summary {
border-radius: 8px 8px 0px 0px;
}
details.kx-item summary .info {
flex: 1;
}
details.kx-item summary .info .title {
font-size: 1.1em;
}
details.kx-item summary .claim__description p {
font-size: 1.4rem;
line-height: 2rem;
}
details.kx-item summary .claim__links p, p.subtle-links {
display: flex;
align-items: center;
flex-wrap: wrap;
font-size: 1rem;
color: var(--grey-700);
}
details.kx-item summary .claim__links a, summary .claim__links span, p.subtle-links a {
font-size: 1rem;
margin: 0 10px 0 0;
color: var(--grey-700);
}
details.kx-item summary .subtitle {
color: var(--purple-700);
}
details.kx-item summary .verificationStatus {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-width: 48px;
height: 48px;
border-radius: 100%;
color: #fff;
font-size: 2rem;
user-select: none;
}
details.kx-item summary .verificationStatus::after {
position: absolute;
display: flex;
top: 0;
left: 0;
right: 0;
bottom: 0;
align-items: center;
justify-content: center;
}
details.kx-item summary .verificationStatus .inProgress {
opacity: 0;
transition: all 0.4s ease;
pointer-events: none;
}
details.kx-item summary .verificationStatus[data-value="success"] {
content: "v";
background-color: var(--green-600);
}
details.kx-item summary .verificationStatus[data-value="success"]::after {
content: "✔";
}
details.kx-item summary .verificationStatus[data-value="failed"] {
background-color: var(--red-400);
}
details.kx-item summary .verificationStatus[data-value="failed"]::after {
content: "✕";
}
details.kx-item summary .verificationStatus[data-value="running"] .inProgress {
opacity: 1;
}
details.kx-item .subsection {
display: flex;
align-items: center;
gap: 16px;
}
details.kx-item .subsection > img {
width: 24px;
height: 24px;
opacity: 0.4;
}
@media screen and (max-width: 640px) {
details.kx-item summary .claim__description p {
font-size: 1.2rem;
}
details.kx-item summary .claim__links a, p.subtle-links a {
font-size: 0.9rem;
}
}
@media screen and (max-width: 480px) {
summary .claim__description p {
font-size: 1rem;
}
details.kx-item summary .verificationStatus {
min-width: 36px;
height: 36px;
font-size: 1.6rem;
}
}
details.kx-item .inProgress {
font-size: 10px;
margin: 50px auto;
text-indent: -9999em;
width: 4.8em;
height: 4.8em;
border-radius: 50%;
background: var(--purple-400);
background: -moz-linear-gradient(left, var(--purple-400) 10%, rgba(255, 255, 255, 0) 42%);
background: -webkit-linear-gradient(left, var(--purple-400) 10%, rgba(255, 255, 255, 0) 42%);
background: -o-linear-gradient(left, var(--purple-400) 10%, rgba(255, 255, 255, 0) 42%);
background: -ms-linear-gradient(left, var(--purple-400) 10%, rgba(255, 255, 255, 0) 42%);
background: linear-gradient(to right, var(--purple-400) 10%, rgba(255, 255, 255, 0) 42%);
position: relative;
-webkit-animation: load3 1.4s infinite linear;
animation: load3 1.4s infinite linear;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
details.kx-item .inProgress:before {
width: 50%;
height: 50%;
background: var(--purple-400);
border-radius: 100% 0 0 0;
position: absolute;
top: 0;
left: 0;
content: '';
}
details.kx-item .inProgress:after {
background: var(--purple-100);
width: 65%;
height: 65%;
border-radius: 50%;
content: '';
margin: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
@-webkit-keyframes load3 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load3 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

File diff suppressed because it is too large Load diff

View file

@ -126,12 +126,14 @@ section.profile p, .demo p {
} }
.card--profileHeader { .card--profileHeader {
display: flex; display: flex;
flex-direction: column;
flex-wrap: wrap; flex-wrap: wrap;
gap: 2rem; align-items: center;
gap: 24px;
/* text-align: center; */ /* text-align: center; */
} }
.card--profileHeader p, .card--profileHeader small { .card--profileHeader p, .card--profileHeader small {
margin: 0 0 1.2rem; margin: 0;
} }
.card--small-profile { .card--small-profile {
display: flex; display: flex;
@ -172,7 +174,7 @@ section.profile p, .demo p {
color: #fff; color: #fff;
} }
#profileName { #profileName {
font-size: 2rem; font-size: 1.6rem;
color: var(--grey-700); color: var(--grey-700);
} }
#profileURLFingerprint { #profileURLFingerprint {
@ -248,96 +250,21 @@ section.profile p, .demo p {
margin-bottom: 0; margin-bottom: 0;
} }
.claim { kx-claim {
display: flex; display: block;
align-items: center; margin: 12px 0;
width: 100%;
margin: 0.8rem 0;
padding: 0.8rem 1.2rem;
background-color: var(--purple-100);
border-radius: 8px;
} }
.claim p {
margin: 0;
}
.claim .claim__main {
flex: 1;
}
.claim .claim__description p {
font-size: 1.4rem;
line-height: 2rem;
}
.claim .claim__links p, p.subtle-links {
display: flex;
align-items: center;
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-600);
color: #fff;
font-size: 2rem;
user-select: none;
}
.claim .claim__verification--true {
background-color: var(--green-600);
}
@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 { .avatar {
display: inline-block; display: inline-block;
min-width: 96px; min-width: 96px;
max-width: 128px; max-width: 128px;
/* margin-top: 1.6rem; */ line-height: 0;
text-align: center; text-align: center;
} }
.avatar img { .avatar img {
width: 100%; width: 100%;
border-radius: 24px; border-radius: 50%;
} }
/* .buttons { /* .buttons {

9
views/profile-failed.pug Normal file
View file

@ -0,0 +1,9 @@
extends templates/base.pug
block content
section.profile.narrow
h2 Something went wrong when generating the profile
ul
each error in data.errors
li= error

View file

@ -1,9 +1,19 @@
extends templates/base.pug extends templates/base.pug
mixin generateUser(user, isPrimary)
h2
| #{user.userData.email}
if isPrimary
small.primary primary
each claim in user.claims
kx-claim(data-claim=claim)
block js block js
script(type='application/javascript' src='/static/dialog-polyfill.js' charset='utf-8')
script(type='application/javascript' src='/static/openpgp.min.js' charset='utf-8') 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/doip.js' charset='utf-8')
script(type='application/javascript' src='/static/dialog-polyfill.js' charset='utf-8') script(type='application/javascript' src='/static/kx-claim.js' charset='utf-8')
script(type='application/javascript' src='/static/kx-key.js' charset='utf-8')
script(type='application/javascript' src='/static/scripts.js' charset='utf-8') script(type='application/javascript' src='/static/scripts.js' charset='utf-8')
block css block css
@ -13,9 +23,6 @@ block content
section.profile.narrow section.profile.narrow
noscript noscript
p Keyoxide requires JavaScript to function. p Keyoxide requires JavaScript to function.
span#profileUid(style='display: none;') #{uid}
span#profileServer(style='display: none;') #{server}
span#profileMode(style='display: none;') #{mode}
dialog#dialog--encryptMessage dialog#dialog--encryptMessage
div div
@ -35,19 +42,23 @@ block content
form(method="dialog") form(method="dialog")
input(type="submit" value="Close") input(type="submit" value="Close")
#profileDialogs
if (mode == 'sig')
#profileSigInput.card.card--form
form#formGenerateSignatureProfile(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 #profileHeader.card.card--profileHeader
a.avatar(href="#")
img#profileAvatar(src=data.extra.avatarURL alt="avatar")
p#profileName= data.keyData.users[data.keyData.primaryUserIndex].userData.name
//- p#profileURLFingerprint
//- a(href=data.key.fetchURL)=data.keyData.fingerprint
.buttons
button(onClick="document.querySelector('#dialog--encryptMessage').showModal();") Encrypt message
button(onClick="document.querySelector('#dialog--verifySignature').showModal();") Verify signature
#profileProofs.card #profileProofs.card
if (mode == 'sig') h2 Key
//- p Waiting for input&mldr; kx-key(data-keydata=data.keyData)
else
p Loading keys &amp; verifying proofs&mldr; +generateUser(data.keyData.users[data.keyData.primaryUserIndex], true)
each user, index in data.keyData.users
unless index == data.keyData.primaryUserIndex
+generateUser(user, false)