mirror of
https://codeberg.org/keyoxide/keyoxide-web.git
synced 2024-12-22 14:59:29 -07:00
Improve layout and rendering of profiles
This commit is contained in:
parent
9307e1c1dc
commit
e18753b54a
13 changed files with 1070 additions and 1112 deletions
46
index.js
46
index.js
|
@ -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
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const app = express();
|
||||
const env = {};
|
||||
const { stringReplace } = require('string-replace-middleware');
|
||||
require('dotenv').config();
|
||||
const express = require('express')
|
||||
const fs = require('fs')
|
||||
const app = express()
|
||||
const { stringReplace } = require('string-replace-middleware')
|
||||
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('view engine', 'pug');
|
||||
app.set('port', process.env.PORT || 3000);
|
||||
app.set('domain', process.env.DOMAIN || "keyoxide.org");
|
||||
app.set('keyoxide_version', packageData.version);
|
||||
app.set('onion_url', process.env.ONION_URL);
|
||||
app.set('env', process.env.NODE_ENV || "production")
|
||||
app.set('view engine', 'pug')
|
||||
app.set('port', process.env.PORT || 3000)
|
||||
app.set('domain', process.env.DOMAIN || "keyoxide.org")
|
||||
app.set('keyoxide_version', packageData.version)
|
||||
app.set('onion_url', process.env.ONION_URL)
|
||||
|
||||
app.use('/favicon.svg', express.static('favicon.svg'));
|
||||
app.use('/robots.txt', express.static('robots.txt'));
|
||||
app.use('/favicon.svg', express.static('favicon.svg'))
|
||||
app.use('/robots.txt', express.static('robots.txt'))
|
||||
|
||||
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/,
|
||||
}));
|
||||
}))
|
||||
|
||||
app.use('/', require('./routes/main'));
|
||||
app.use('/static', require('./routes/static'));
|
||||
app.use('/server', require('./routes/server'));
|
||||
app.use('/util', require('./routes/util'));
|
||||
app.use('/', require('./routes/profile'));
|
||||
app.use('/', require('./routes/main'))
|
||||
app.use('/static', require('./routes/static'))
|
||||
app.use('/util', require('./routes/util'))
|
||||
app.use('/', require('./routes/profile'))
|
||||
|
||||
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')}`)
|
||||
})
|
||||
|
|
|
@ -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
|
||||
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' })
|
||||
});
|
||||
})
|
||||
|
||||
router.get('/wkd/:input', function(req, res) {
|
||||
res.render('profile', { mode: 'wkd', uid: req.params.input })
|
||||
});
|
||||
router.get('/wkd/:id', async (req, res) => {
|
||||
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) {
|
||||
res.render('profile', { mode: 'hkp', uid: req.params.input })
|
||||
});
|
||||
router.get('/hkp/:id', async (req, res) => {
|
||||
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) {
|
||||
res.render('profile', { mode: 'hkp', uid: req.params.input, server: req.params.server })
|
||||
});
|
||||
router.get('/hkp/:server/:id', async (req, res) => {
|
||||
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}` })
|
||||
});
|
||||
})
|
||||
|
||||
router.get('/:input', function(req, res) {
|
||||
router.get('/:input', async (req, res) => {
|
||||
res.render('profile', { mode: 'auto', uid: req.params.input })
|
||||
});
|
||||
})
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router
|
||||
|
|
|
@ -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
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const express = require('express');
|
||||
const router = require('express').Router();
|
||||
const express = require('express')
|
||||
const router = require('express').Router()
|
||||
|
||||
router.get('/doip.min.js', function(req, res) {
|
||||
res.sendFile(`node_modules/doipjs/dist/doip.min.js`, { root: `${__dirname}/../` })
|
||||
});
|
||||
})
|
||||
router.get('/doip.js', function(req, res) {
|
||||
res.sendFile(`node_modules/doipjs/dist/doip.js`, { root: `${__dirname}/../` })
|
||||
});
|
||||
})
|
||||
|
||||
router.get('/openpgp.min.js', function(req, res) {
|
||||
res.sendFile(`node_modules/openpgp/dist/openpgp.min.js`, { root: `${__dirname}/../` })
|
||||
});
|
||||
})
|
||||
router.get('/openpgp.min.js.map', function(req, res) {
|
||||
res.sendFile(`node_modules/openpgp/dist/openpgp.min.js.map`, { root: `${__dirname}/../` })
|
||||
});
|
||||
})
|
||||
|
||||
router.get('/qrcode.min.js', function(req, res) {
|
||||
res.sendFile(`node_modules/qrcode/build/qrcode.min.js`, { root: `${__dirname}/../` })
|
||||
});
|
||||
})
|
||||
router.get('/qrcode.min.js.map', function(req, res) {
|
||||
res.sendFile(`node_modules/qrcode/build/qrcode.min.js.map`, { root: `${__dirname}/../` })
|
||||
});
|
||||
})
|
||||
|
||||
router.get('/dialog-polyfill.js', function(req, res) {
|
||||
res.sendFile(`node_modules/dialog-polyfill/dist/dialog-polyfill.js`, { root: `${__dirname}/../` })
|
||||
});
|
||||
})
|
||||
router.get('/dialog-polyfill.css', function(req, res) {
|
||||
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
122
server/index.js
Normal 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
122
server/keys.js
Normal 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
36
server/utils.js
Normal 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
205
static/kx-claim.js
Normal 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…`;
|
||||
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
72
static/kx-key.js
Normal 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
206
static/kx-styles.css
Normal 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);
|
||||
}
|
||||
}
|
1155
static/scripts.js
1155
static/scripts.js
File diff suppressed because it is too large
Load diff
|
@ -126,12 +126,14 @@ section.profile p, .demo p {
|
|||
}
|
||||
.card--profileHeader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
/* text-align: center; */
|
||||
}
|
||||
.card--profileHeader p, .card--profileHeader small {
|
||||
margin: 0 0 1.2rem;
|
||||
margin: 0;
|
||||
}
|
||||
.card--small-profile {
|
||||
display: flex;
|
||||
|
@ -172,7 +174,7 @@ section.profile p, .demo p {
|
|||
color: #fff;
|
||||
}
|
||||
#profileName {
|
||||
font-size: 2rem;
|
||||
font-size: 1.6rem;
|
||||
color: var(--grey-700);
|
||||
}
|
||||
#profileURLFingerprint {
|
||||
|
@ -248,96 +250,21 @@ section.profile p, .demo p {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
kx-claim {
|
||||
display: block;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.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 {
|
||||
display: inline-block;
|
||||
min-width: 96px;
|
||||
max-width: 128px;
|
||||
/* margin-top: 1.6rem; */
|
||||
line-height: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.avatar img {
|
||||
width: 100%;
|
||||
border-radius: 24px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* .buttons {
|
||||
|
|
9
views/profile-failed.pug
Normal file
9
views/profile-failed.pug
Normal 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
|
|
@ -1,9 +1,19 @@
|
|||
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
|
||||
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/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')
|
||||
|
||||
block css
|
||||
|
@ -13,9 +23,6 @@ block content
|
|||
section.profile.narrow
|
||||
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}
|
||||
|
||||
dialog#dialog--encryptMessage
|
||||
div
|
||||
|
@ -35,19 +42,23 @@ block content
|
|||
form(method="dialog")
|
||||
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
|
||||
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
|
||||
if (mode == 'sig')
|
||||
//- p Waiting for input…
|
||||
else
|
||||
p Loading keys & verifying proofs…
|
||||
h2 Key
|
||||
kx-key(data-keydata=data.keyData)
|
||||
|
||||
+generateUser(data.keyData.users[data.keyData.primaryUserIndex], true)
|
||||
each user, index in data.keyData.users
|
||||
unless index == data.keyData.primaryUserIndex
|
||||
+generateUser(user, false)
|
||||
|
||||
|
|
Loading…
Reference in a new issue