mirror of
https://codeberg.org/keyoxide/keyoxide-web.git
synced 2024-12-22 23:09: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
|
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')}`)
|
||||||
});
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
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);
|
||||||
|
}
|
||||||
|
}
|
1139
static/scripts.js
1139
static/scripts.js
File diff suppressed because it is too large
Load diff
|
@ -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
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
|
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…
|
kx-key(data-keydata=data.keyData)
|
||||||
else
|
|
||||||
p Loading keys & verifying proofs…
|
+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