mirror of
https://codeberg.org/keyoxide/keyoxide-web.git
synced 2024-12-22 14:59:29 -07:00
feat: support profile theming
This commit is contained in:
parent
df48b92732
commit
99217f6a3d
8 changed files with 109 additions and 16 deletions
|
@ -8,7 +8,8 @@
|
||||||
"ajv": "^8.6.3",
|
"ajv": "^8.6.3",
|
||||||
"bent": "^7.3.12",
|
"bent": "^7.3.12",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"doipjs": "^1.2.4",
|
"colorjs.io": "^0.4.5",
|
||||||
|
"doipjs": "^1.2.5",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-http-context2": "^1.0.0",
|
"express-http-context2": "^1.0.0",
|
||||||
|
|
|
@ -32,7 +32,7 @@ import bodyParserImport from 'body-parser'
|
||||||
import { rateLimit } from 'express-rate-limit'
|
import { rateLimit } from 'express-rate-limit'
|
||||||
import { generateSignatureProfile, utils, generateWKDProfile, generateHKPProfile, generateAutoProfile, generateKeybaseProfile } from '../server/index.js'
|
import { generateSignatureProfile, utils, generateWKDProfile, generateHKPProfile, generateAutoProfile, generateKeybaseProfile } from '../server/index.js'
|
||||||
import { Profile } from 'doipjs'
|
import { Profile } from 'doipjs'
|
||||||
import { getMetaFromReq } from '../server/utils.js'
|
import { generateProfileTheme, getMetaFromReq } from '../server/utils.js'
|
||||||
import logger from '../log.js'
|
import logger from '../log.js'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
@ -133,6 +133,7 @@ router.get('/keybase/:username/:fingerprint', profileRateLimiter, async (req, re
|
||||||
|
|
||||||
router.get('/:id', profileRateLimiter, async (req, res) => {
|
router.get('/:id', profileRateLimiter, async (req, res) => {
|
||||||
const data = await generateAutoProfile(req.params.id)
|
const data = await generateAutoProfile(req.params.id)
|
||||||
|
const theme = generateProfileTheme(data)
|
||||||
const title = utils.generatePageTitle('profile', data)
|
const title = utils.generatePageTitle('profile', data)
|
||||||
res.set('ariadne-identity-proof', data.identifier)
|
res.set('ariadne-identity-proof', data.identifier)
|
||||||
res.render('profile', {
|
res.render('profile', {
|
||||||
|
@ -140,6 +141,7 @@ router.get('/:id', profileRateLimiter, async (req, res) => {
|
||||||
data: data instanceof Profile ? data.toJSON() : data,
|
data: data instanceof Profile ? data.toJSON() : data,
|
||||||
enable_message_encryption: false,
|
enable_message_encryption: false,
|
||||||
enable_signature_verification: false,
|
enable_signature_verification: false,
|
||||||
|
theme,
|
||||||
meta: getMetaFromReq(req)
|
meta: getMetaFromReq(req)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -28,6 +28,8 @@ 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/>.
|
||||||
*/
|
*/
|
||||||
import { webcrypto as crypto } from 'crypto'
|
import { webcrypto as crypto } from 'crypto'
|
||||||
|
import { Profile } from 'doipjs'
|
||||||
|
import Color from 'colorjs.io'
|
||||||
|
|
||||||
export async function computeWKDLocalPart (localPart) {
|
export async function computeWKDLocalPart (localPart) {
|
||||||
const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase())
|
const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase())
|
||||||
|
@ -87,3 +89,51 @@ export function getMetaFromReq (req) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateProfileTheme (/** @type {Profile} */ profile) {
|
||||||
|
if (!(profile && profile instanceof Profile)) return null
|
||||||
|
|
||||||
|
if (!profile.personas[profile.primaryPersonaIndex].themeColor) return null
|
||||||
|
|
||||||
|
let base
|
||||||
|
try {
|
||||||
|
base = new Color(profile.personas[profile.primaryPersonaIndex].themeColor)
|
||||||
|
} catch (_) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (base.to('hsl').hsl[0].isNaN) return null
|
||||||
|
if (base.to('hsl').hsl[2] === 0) return null
|
||||||
|
|
||||||
|
const primaryLight = base.to('hsl')
|
||||||
|
primaryLight.hsl[2] = 40
|
||||||
|
const primaryDark = base.to('hsl')
|
||||||
|
primaryDark.hsl[2] = 80
|
||||||
|
|
||||||
|
const primarySubtleLight = base.to('hsl')
|
||||||
|
primarySubtleLight.hsl[2] = 50
|
||||||
|
const primarySubtleDark = base.to('hsl')
|
||||||
|
primarySubtleDark.hsl[2] = 70
|
||||||
|
|
||||||
|
const backgroundLight = base.to('hsl')
|
||||||
|
backgroundLight.hsl[2] = 98
|
||||||
|
const backgroundDark = base.to('hsl')
|
||||||
|
backgroundDark.hsl[1] = 20
|
||||||
|
backgroundDark.hsl[2] = 5
|
||||||
|
|
||||||
|
return {
|
||||||
|
base: base.toString({ format: 'hex' }),
|
||||||
|
primary: {
|
||||||
|
light: primaryLight.toString(),
|
||||||
|
dark: primaryDark.toString()
|
||||||
|
},
|
||||||
|
primarySubtle: {
|
||||||
|
light: primarySubtleLight.toString(),
|
||||||
|
dark: primarySubtleDark.toString()
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
light: backgroundLight.toString(),
|
||||||
|
dark: backgroundDark.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -58,8 +58,9 @@ kx-claim {
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
|
margin: 8px 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 2px solid var(--background-color);
|
border-top: 2px solid var(--header-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|
|
@ -108,6 +108,18 @@ a.button {
|
||||||
border-color: var(--button-border-color-hover);
|
border-color: var(--button-border-color-hover);
|
||||||
color: var(--button-text-color-hover);
|
color: var(--button-text-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.themed {
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: var(--text-color-inverse);
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-color-inverse);
|
||||||
|
background-color: var(--primary-color-subtle);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
|
|
@ -64,14 +64,25 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--primary-color: var(--purple-700);
|
--primary-color-light: var(--purple-700);
|
||||||
--primary-color-subtle: var(--purple-400);
|
--primary-color-dark: var(--purple-300);
|
||||||
|
|
||||||
--body-background-color: #fafafa;
|
--primary-color-subtle-light: var(--purple-400);
|
||||||
|
--primary-color-subtle-dark: var(--purple-500);
|
||||||
|
|
||||||
|
--background-color-light: var(#fafafa);
|
||||||
|
--background-color-dark: var(#0a0a0a);
|
||||||
|
}
|
||||||
|
:root {
|
||||||
|
--primary-color: var(--primary-color-light);
|
||||||
|
--primary-color-subtle: var(--primary-color-subtle-light);
|
||||||
|
|
||||||
|
--body-background-color: var(--background-color-light);
|
||||||
--section-background-color: var(--white);
|
--section-background-color: var(--white);
|
||||||
|
|
||||||
--text-color: var(--grey-800);
|
--text-color: var(--grey-800);
|
||||||
--text-color-subtle: var(--grey-500);
|
--text-color-subtle: var(--grey-500);
|
||||||
|
--text-color-inverse: var(--white);
|
||||||
--h1-color: var(--text-color);
|
--h1-color: var(--text-color);
|
||||||
--h2-color: var(--text-color);
|
--h2-color: var(--text-color);
|
||||||
--h2-small-color: var(--primary-color-subtle);
|
--h2-small-color: var(--primary-color-subtle);
|
||||||
|
@ -82,7 +93,7 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
||||||
|
|
||||||
--link-color: var(--blue-700);
|
--link-color: var(--blue-700);
|
||||||
--link-color-subtle: var(--text-color);
|
--link-color-subtle: var(--text-color);
|
||||||
--link-color-hover: var(--purple-700);
|
--link-color-hover: var(--primary-color);
|
||||||
|
|
||||||
--line-color-subtle: var(--grey-200);
|
--line-color-subtle: var(--grey-200);
|
||||||
|
|
||||||
|
@ -105,14 +116,15 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
|
|
||||||
--primary-color: var(--purple-300);
|
--primary-color: var(--primary-color-dark);
|
||||||
--primary-color-subtle: var(--purple-500);
|
--primary-color-subtle: var(--primary-color-subtle-dark);
|
||||||
|
|
||||||
--body-background-color: #0a0a0a;
|
--body-background-color: var(--background-color-dark);
|
||||||
--section-background-color: var(--grey-900);
|
--section-background-color: var(--grey-900);
|
||||||
|
|
||||||
--text-color: var(--grey-50);
|
--text-color: var(--grey-50);
|
||||||
--text-color-subtle: var(--grey-300);
|
--text-color-subtle: var(--grey-300);
|
||||||
|
--text-color-inverse: var(--grey-800);
|
||||||
--h1-color: var(--text-color);
|
--h1-color: var(--text-color);
|
||||||
--h2-color: var(--text-color);
|
--h2-color: var(--text-color);
|
||||||
--h2-small-color: var(--primary-color-subtle);
|
--h2-small-color: var(--primary-color-subtle);
|
||||||
|
|
|
@ -111,6 +111,16 @@ block content
|
||||||
input(type='submit', name='submit', value='Generate profile')
|
input(type='submit', name='submit', value='Generate profile')
|
||||||
|
|
||||||
unless (isSignature && !signature)
|
unless (isSignature && !signature)
|
||||||
|
if (theme)
|
||||||
|
style
|
||||||
|
| :root {
|
||||||
|
| --primary-color-light: #{theme.primary.light};
|
||||||
|
| --primary-color-dark: #{theme.primary.dark};
|
||||||
|
| --primary-color-subtle-light: #{theme.primarySubtle.light};
|
||||||
|
| --primary-color-subtle-dark: #{theme.primarySubtle.dark};
|
||||||
|
| --background-color-light: #{theme.background.light};
|
||||||
|
| --background-color-dark: #{theme.background.dark};
|
||||||
|
| }
|
||||||
.profile__header
|
.profile__header
|
||||||
img.profile__avatar.u-logo(src=data.personas[data.primaryPersonaIndex].avatarUrl alt="avatar")
|
img.profile__avatar.u-logo(src=data.personas[data.primaryPersonaIndex].avatarUrl alt="avatar")
|
||||||
|
|
||||||
|
@ -132,8 +142,8 @@ block content
|
||||||
+generatePersona(persona, false)
|
+generatePersona(persona, false)
|
||||||
h2 Profile
|
h2 Profile
|
||||||
if data.verifiers.length > 0
|
if data.verifiers.length > 0
|
||||||
button(onClick=`showQR('${data.verifiers[0].url}', 'profile_verifier_url')` aria-label='Show profile ID QR') Keyoxide profile QR
|
button.themed(onClick=`showQR('${data.verifiers[0].url}', 'profile_verifier_url')` aria-label='Show profile ID QR') Keyoxide profile QR
|
||||||
button(onClick=`showQR('${data.identifier}', 'profile_identifier')` aria-label='Show profile ID QR') Profile ID QR
|
button.themed(onClick=`showQR('${data.identifier}', 'profile_identifier')` aria-label='Show profile ID QR') Profile ID QR
|
||||||
|
|
||||||
section
|
section
|
||||||
h2 Profile information
|
h2 Profile information
|
||||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -1579,6 +1579,11 @@ colorette@^2.0.14:
|
||||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
||||||
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
|
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
|
||||||
|
|
||||||
|
colorjs.io@^0.4.5:
|
||||||
|
version "0.4.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/colorjs.io/-/colorjs.io-0.4.5.tgz#7775f787ff90aca7a38f6edb7b7c0f8cce1e6418"
|
||||||
|
integrity sha512-yCtUNCmge7llyfd/Wou19PMAcf5yC3XXhgFoAh6zsO2pGswhUPBaaUh8jzgHnXtXuZyFKzXZNAnyF5i+apICow==
|
||||||
|
|
||||||
colorspace@1.1.x:
|
colorspace@1.1.x:
|
||||||
version "1.1.4"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243"
|
resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243"
|
||||||
|
@ -1837,10 +1842,10 @@ doctypes@^1.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
|
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
|
||||||
integrity sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==
|
integrity sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==
|
||||||
|
|
||||||
doipjs@^1.2.4:
|
doipjs@^1.2.5:
|
||||||
version "1.2.4"
|
version "1.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/doipjs/-/doipjs-1.2.4.tgz#f980ac4a3ca71b4496530a1e877230b7b6a4cec0"
|
resolved "https://registry.yarnpkg.com/doipjs/-/doipjs-1.2.5.tgz#98d67dad67ec0d0afafcda4e4740c93a964eecd3"
|
||||||
integrity sha512-zs/kL/Yc3H4X8SrWW0zf6w6RLWDw2LQ6w/tgx8Kirqf7eMtOO0rur9/0ASCeHvoqRr7O6W7dXnW7nD/I5ZgxDg==
|
integrity sha512-366rS4LpzrGUgf1wfxjLpS46T4whB15m4dW8DfVYLLFYqNhwDVirRhysaexCQ8B4Ns+eCSa7qfpVszSxwGmgkA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@openpgp/hkp-client" "^0.0.3"
|
"@openpgp/hkp-client" "^0.0.3"
|
||||||
"@openpgp/wkd-client" "^0.0.4"
|
"@openpgp/wkd-client" "^0.0.4"
|
||||||
|
|
Loading…
Reference in a new issue